I'm trying to build a web app using as much of the out-of-the-box ASP.NET security as I can. I got it up and running pretty easily using the default patterns but now I'm trying to make a simple modification and am getting all sorts of confused. Basically, all I'm trying to do is use a generic userID (a stringified GUID, for instance) instead of a username as the primary key for the user account. Googling yields many threads on how to inherit from MembershipProvider but they all assuming you're overriding the same method signatures like ::CreateUser(string username, string password, ...). I'm kind of lost because I don't know what event is raised that causes ::CreateUser() to be called, nor what method/class to override in order to change how it's called.
Yes, I know I can "pretend" username is userID in this particular scenario but I'd really like to understand how this works in case I want to make addition mods in the future.
Also, I'm trying to customize what info is stored in the authorization cookie but I think I've got a handle on that. Some of the relevant code in its current half-baked condition is shown below. Thanks.
MyMembershipProvider.cs
namespace MyOrg.MyApp.Infrastructure
{
public class MyAppMembershipProvider : MembershipProvider
{
int minPasswordLength = MyOrg.MyApp.Common.Config.minPasswordLength;
public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status)
{
/* WANT TO SWITCH THIS OVER SO IT USES userID INSTEAD OF username */
User user = null;
using (DB db = new DB())
{
DateTime creationDate = DateTime.UtcNow;
user = new User() {
username = username,
email = email,
pwd = MyOrg.Utilities.Misc.PasswordHash.CreateHash(password)
};
int recAdded = db.insertUser(user);
if (recAdded > 0)
{
status = MembershipCreateStatus.Success;
}
else
{
status = MembershipCreateStatus.UserRejected;
}
}
return getMembershipUserFromUser(user);
}
public override MembershipUser GetUser(string username, bool userIsOnline)
{
/* WANT TO SWITCH THIS OVER SO IT USES userID INSTEAD OF username */
User user = null;
using (DB db = new DB())
{
user = db.getUser(username);
}
return getMembershipUserFromUser(user);
}
public override bool ChangePassword(string username, string oldPassword, string newPassword)
{
/* WANT TO SWITCH THIS OVER SO IT USES userID INSTEAD OF username */
User user = null;
using (DB db = new DB())
user = db.getUser(username);
bool result = PasswordHash.ValidatePassword(oldPassword, user.pwd);
if (result)
{
user.pwd = Utilities.Misc.PasswordHash.CreateHash(newPassword);
user.lastPwdChange = DateTime.UtcNow;
using (DB db = new DB())
db.updateUser(user);
}
return result;
}
public static bool ValidateUser(string username, string password, out string userID)
{
/* WANT TO SWITCH THIS OVER SO IT USES userID INSTEAD OF username */
userID = null;
using (DB db = new DB())
{
User user = db.getUserByUsername(username);
if (user == null)
return false;
bool result = MyOrg.Utilities.Misc.PasswordHash.ValidatePassword(password, user.pwd);
if (result)
{
user.failedPwdAttemptCount = 0;
user.failedPwdAnswerAttemptCount = 0;
user.lastLogin = DateTime.UtcNow;
userID = user.id;
}
else
{
user.failedPwdAttemptCount++;
user.failedPwdAttemptWindowStart = DateTime.UtcNow;
}
db.updateUser(user);
return result;
}
}
private MembershipUser getMembershipUserFromUser(User user)
{
return new MembershipUser(
"MyAppMembershipProvider",
user.username,
"dunno",
user.email,
user.pwdQuestion,
user.comment,
user.isApproved,
user.isLockedOut,
user.created,
user.lastLogin,
user.lastActivity,
user.lastPwdChange,
user.lastLockedOut);
}
public override int MinRequiredPasswordLength
{
get { return minPasswordLength; }
}
/* Other inherited methods not implemented*/
}
}
AccountController.cs
namespace MyOrg.MyApp.Controllers
{
public class AccountController : BaseController
{
public ActionResult LogOn()
{
return View();
}
[HttpPost]
public ActionResult LogOn(FormCollection form)
{
string username = form["username"];
string password = form["password"];
bool rememberMe = form["rememberMe"] != null;
string returnUrl = form["returnUrl"];
string userID;
if (MyAppMembershipProvider.ValidateUser(username, password, out userID))
{
CustomPrincipalSerializeModel serializeModel = new CustomPrincipalSerializeModel();
serializeModel.userID = userID;
serializeModel.username = username;
System.Web.Script.Serialization.JavaScriptSerializer serializer = new System.Web.Script.Serialization.JavaScriptSerializer();
string userData = serializer.Serialize(serializeModel);
FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(
1,
userID,
DateTime.Now,
DateTime.Now.AddMinutes(15),
false,
userData);
string encTicket = FormsAuthentication.Encrypt(authTicket);
HttpCookie faCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encTicket);
Response.Cookies.Add(faCookie);
return RedirectToAction("Index", "Home");
}
else
{
ViewBag.validationMsg = "The user name or password provided is incorrect.";
}
// If we got this far, something failed, redisplay form
ViewBag.username = username;
return View();
}
public ActionResult LogOff()
{
FormsAuthentication.SignOut();
return RedirectToAction("Index", "Home");
}
public ActionResult Details()
{
RegisterModel model = new RegisterModel();
User user;
UserProfile profile;
using (DB db = new DB())
{
user = db.getUser(User.userID);
profile = db.getUserProfile(User.userID);
}
model.UserName = user.username;
model.Email = user.email;
model.Zip = profile.get(UserProfile.Properties.zip);
return View(model);
}
public ActionResult Edit()
{
RegisterModel model = new RegisterModel();
model = null;
User user;
UserProfile profile;
using (DB db = new DB())
{
user = db.getUser(User.userID);
profile = db.getUserProfile(User.userID);
}
model.UserName = user.username;
model.Email = user.email;
model.Zip = profile.get(UserProfile.Properties.zip);
return View(model);
}
[HttpPost]
public ActionResult Edit(RegisterModel model)
{
UserProfile profile = new UserProfile();
profile.userID = model.UserName;
profile.set(UserProfile.Properties.zip, model.Zip);
using (DB db = new DB())
{
User user = db.getUser(model.UserName);
user.email = model.Email;
db.updateUser(user);
db.updateUserProfile(profile);
}
return RedirectToAction("Index", "Home");
}
public ActionResult Register()
{
RegisterModel m = new RegisterModel();
return View(m);
}
[HttpPost]
public ActionResult Register(RegisterModel model)
{
using (DB db = new DB())
{
// Check unique username
if (db.getUser(model.UserName) != null)
{
ViewBag.validationMsg = String.Format("The username '{0}' is already taken. Please choose another.", model.UserName);
return View();
}
Membership.CreateUser(model.UserName, model.Password, model.Email);
UserProfile profile = new UserProfile();
profile.userID = model.UserName;
profile.set(UserProfile.Properties.zip, model.Zip);
db.insertUserProfile(profile);
string confirmationCode = Guid.NewGuid().ToString();
db.insertConfirmationCode(model.UserName, confirmationCode);
// Send Email w/ confirmation code
Infrastructure.AccountConfirmationEmail email = new Infrastructure.AccountConfirmationEmail(
model.Email,
model.UserName,
confirmationCode
);
email.send();
}
return View("ConfirmationEmailSent");
}
public ActionResult Confirm(string username, string code)
{
ViewBag.username = username;
ViewBag.code = code;
return View();
}
[HttpPost]
public ActionResult Confirm(string username, string code, string password)
{
string storedCode = null;
using (DB db = new DB())
storedCode = db.getConfirmationCode(username);
if (storedCode == null)
{
// Username not found in confirmation_codes
return View("ConfirmationCodeNotFound");
}
else
{
if (code == storedCode)
{
if (Membership.ValidateUser(username, password))
{
using (DB db = new DB())
{
User user = db.getUser(username);
user.isConfirmed = true;
db.updateUser(user);
db.deleteConfirmationCode(username);
FormsAuthentication.SetAuthCookie(username, false);
return RedirectToAction("Index", "Home");
}
}
else
{
ViewBag.username = username;
ViewBag.code = code;
ViewBag.validationMsg = "Incorrect Password";
return View();
}
}
else
{
return View("ConfirmationCodeNotFound");
}
}
}
[Authorize]
public ActionResult ChangePassword()
{
return View();
}
[Authorize]
[HttpPost]
public ActionResult ChangePassword(string OldPassword, string NewPassword)
{
bool changePasswordSucceeded;
try
{
MembershipUser currentUser = Membership.GetUser(User.Identity.Name, true /* userIsOnline */);
changePasswordSucceeded = currentUser.ChangePassword(OldPassword, NewPassword);
}
catch (Exception)
{
changePasswordSucceeded = false;
}
if (changePasswordSucceeded)
{
return RedirectToAction("ChangePasswordSuccess");
}
else
{
ViewBag.validationMsg = "The old password is incorrect.";
ViewBag.newPassword = NewPassword;
}
// If we got this far, something failed, redisplay form
return View();
}
[Authorize]
public ActionResult ChangePasswordSuccess()
{
return View();
}
}
}
Global.asax.cs
namespace MyOrg.MyApp
{
public class MvcApplication : System.Web.HttpApplication
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
//filters.Add(new HandleErrorAttribute());
filters.Add(new Infrastructure.HandleExceptionsAttribute()); // Implementing custom exception handling
}
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
}
protected void Application_AuthenticateRequest(object sender, EventArgs e)
{
HttpCookie authCookie = Request.Cookies[FormsAuthentication.FormsCookieName];
if (authCookie != null)
{
FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(authCookie.Value);
string userID = ticket.Name;
CustomPrincipal principal = new CustomPrincipal(userID);
HttpContext.Current.User = principal;
}
}
protected void Application_PostAuthenticateRequest(Object sender, EventArgs e)
{
HttpCookie authCookie = Request.Cookies[FormsAuthentication.FormsCookieName];
if (authCookie != null)
{
FormsAuthenticationTicket authTicket = FormsAuthentication.Decrypt(authCookie.Value);
JavaScriptSerializer serializer = new JavaScriptSerializer();
CustomPrincipalSerializeModel serializeModel = serializer.Deserialize<CustomPrincipalSerializeModel>(authTicket.UserData);
CustomPrincipal newUser = new CustomPrincipal(authTicket.Name);
newUser.userID = serializeModel.userID;
newUser.username = serializeModel.username;
HttpContext.Current.User = newUser;
}
}
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
}
}
Web.config
<?xml version="1.0"?>
<configuration>
<connectionStrings>
<add name="***" connectionString="***" />
</connectionStrings>
<appSettings>
<add key="webpages:Version" value="1.0.0.0"/>
<add key="ClientValidationEnabled" value="true"/>
<add key="UnobtrusiveJavaScriptEnabled" value="true"/>
</appSettings>
<system.web>
<compilation debug="true" targetFramework="4.0">
<assemblies>
<add assembly="System.Web.Abstractions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
<add assembly="System.Web.Helpers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
<add assembly="System.Web.Routing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
<add assembly="System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
<add assembly="System.Web.WebPages, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
</assemblies>
</compilation>
<authentication mode="Forms">
<forms loginUrl="~/Account/LogOn" timeout="2880" />
</authentication>
<machineKey
validationKey="***"
decryptionKey="***"
validation="SHA1" />
<membership defaultProvider="MyAppMembershipProvider" userIsOnlineTimeWindow="15">
<providers>
<clear />
<add
name="MyAppMembershipProvider"
type="MyOrg.MyApp.Infrastructure.MyAppMembershipProvider"
connectionStringName="dummy"
enablePasswordReset="true"
requiresQuestionAndAnswer="true"
writeExceptionsToEventLog="true" />
</providers>
</membership>
<roleManager enabled="false">
<providers>
<clear/>
<add name="AspNetSqlRoleProvider" type="System.Web.Security.SqlRoleProvider" connectionStringName="ApplicationServices" applicationName="/" />
<add name="AspNetWindowsTokenRoleProvider" type="System.Web.Security.WindowsTokenRoleProvider" applicationName="/" />
</providers>
</roleManager>
<pages>
<namespaces>
<add namespace="System.Web.Helpers" />
<add namespace="System.Web.Mvc" />
<add namespace="System.Web.Mvc.Ajax" />
<add namespace="System.Web.Mvc.Html" />
<add namespace="System.Web.Routing" />
<add namespace="System.Web.WebPages"/>
</namespaces>
</pages>
</system.web>
<system.webServer>
<validation validateIntegratedModeConfiguration="false"/>
<modules runAllManagedModulesForAllRequests="true"/>
</system.webServer>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" />
<bindingRedirect oldVersion="1.0.0.0-2.0.0.0" newVersion="3.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>