Customizing Forms Authentication in ASP.Net MVC 3
The obvious choice to support this requirement is Forms Authentication. This allows me to use all of the build-in security related features of ASP.NET, like the [Authorize] attribute.
Normally, this would be straightforward. In fact, the default MVC project comes with it enabled and built in by default. However, I don't really want to use the Membership API here - I'd rather use my own custom approach to authenticating the users. At the same time, though, I want it to act as if we are using simple Forms Authentication.
Thinking it through, all Forms Authentication really does is set a cookie upon successfully login. This cookie is then read per request and the built-in GenericPrincipal is rehydrated from the cookie.
So what I want to do is:
- Upon successful user login take over the cookie creation process and ensure a bit of additional information is placed in the cookie.
- For each subsequent request, rehydrate instances of my custom IPrincipal/IIdentity from the cookie and set HttpContext.User and Thread.CurrentPrincipal to my custom instances.
- Add some helper methods to my controllers and views so that I can access the current user.
By doing this, I can be sure that I can continue to use much of the built in security that is provided by MVC, while also having access to user-specific information that I require to satisfy the requirements of the application.
Here is the code for creating the auth cookie (this code resides in my AuthenticationService class):
public void SetAuthCookie(string username, bool rememberMe)
{
var user = _dbContext.Set<User>().FirstOrDefault(u => u.Email == username);
if(user == null)
{
throw new Exception("User cannot be found.");
}
string name = !String.IsNullOrEmpty(user.Nickname) ? user.Nickname : String.Format("{0} {1}", user.FirstName, user.LastName);
string userData = CredentialHandler.CreateUserData(user.Id, name, user.Roles.Select(r => r.Name).ToArray());
var ticket = new FormsAuthenticationTicket(1, username, DateTime.Now,
DateTime.Now.AddMinutes(30), rememberMe, userData, FormsAuthentication.FormsCookiePath);
string encryptedTicket = FormsAuthentication.Encrypt(ticket);
HttpContext.Current.Response.Cookies.Add(new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket));
}
The key point to note is that I set specific user data on the cookie - what that data actually is isn't really important.
Now, when the next request comes in from the authenticated user, I can simply re-construct my custom IPrincipal and IIdentity and set them for the lifetime of the request:
public static void BindCredentials()
{
if (HttpContext.Current != null)
{
var context = HttpContext.Current;
if (!String.IsNullOrEmpty(context.Request.FilePath) &&
IgnoredExtensions.Any(ext => context.Request.FilePath.ToLower().EndsWith(ext.ToLower())))
{
_logger.Debug("Not bothering to create a user for a request to: " + context.Request.FilePath);
return;
}
else
{
_logger.Debug("Creating a user for a request to: " + context.Request.FilePath);
}
var cookie = context.Request.Cookies[FormsAuthentication.FormsCookieName];
ColorMatchRPrincipal principal = null;
Guid? id = null;
if (cookie != null)
{
var ticket = FormsAuthentication.Decrypt(cookie.Value);
var values = ticket.UserData.Split(new[] { '^' }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToList();
id = Guid.Parse(values.First());
string name = values.Skip(1).First();
var roles = values.Skip(2).First().Split(new[] { '$' }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToList();
principal = new ColorMatchRPrincipal(new ColorMatchRIdentity(id, name, roles));
}
else
{
principal = new ColorMatchRPrincipal(new ColorMatchRIdentity(id, String.Empty, new[] { "anonymous" }));
}
Thread.CurrentPrincipal = principal;
context.User = principal;
}
}
That's basically all there is to it. This method is defined on a helper class that I call from a registered HTTP module, but you could also call it from Authenticate_Request in Global.asax
What's great about this approach is that you can now take full advantage of the built in authentication/authorization in MVC with needing to roll your own.
