Forms Authentication is ASP.net is simple but the FormsIdentity and GenericPrincipal/RolePrincipal are a little too simple. All we get are IIdentity.Name and IPrincipal.IsInRole(x)
Most real applications need a bit more, like the user's full name or email address, or domain-specific data.
Custom Principal
The usual way to do this was to create a custom principal, the UserData field in the forms authentication cookie, and the asp.net pipeline event "PostAuthenticateRequest".
Here's our custom principal:
public class UserPrincipal : GenericPrincipal { public UserPrincipal(IIdentity identity, string[] roles) : base(identity, roles) { } public string Email { get; set; } }
Here's the login action. Instead of the normal FormsAuthentication.SetAuthCookie, we do it manually (see below):
[AllowAnonymous] [HttpPost] public ActionResult Login(LoginModel model, string returnUrl) { if (ModelState.IsValid) //Required, string length etc { var userStore = new UserRepository(); var user = userStore.FindUser(model.UserName, model.Password); if (user != null) { //FormsAuthentication.SetAuthCookie(user.Name, false); SetAuthCookie(user); //redirect to returnUrl if (!string.IsNullOrEmpty(returnUrl) && Url.IsLocalUrl(returnUrl) && !returnUrl.Equals("/Error/NotFound", StringComparison.OrdinalIgnoreCase)) { return Redirect(returnUrl); } return Redirect("~/"); } ModelState.AddModelError("UserName", "User or password not found"); } return View(model); }
And here's where we set the authentication cookie, here putting our user object as Json into the userData field of the cookie.
private void SetAuthCookie(User user) { var userData = JsonConvert.SerializeObject(user); var authTicket = new FormsAuthenticationTicket( 1, //version user.Name, DateTime.Now, //issue date DateTime.Now.AddMinutes(30), //expiration false, //isPersistent userData, FormsAuthentication.FormsCookiePath); //cookie path var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(authTicket)); Response.Cookies.Add(cookie); }
Finally, we hook up the PostAuthenticationRequest event. Normal forms authentication will have recognised the authentication cookie and created a GenericPrincipal and FormsIdentity. We unpack the userData field, and create our custom principal.
protected void Application_PostAuthenticateRequest(Object sender, EventArgs e) { var context = HttpContext.Current; if (context.User == null || !context.User.Identity.IsAuthenticated) { return; } var formsIdentity = context.User.Identity as FormsIdentity; if (formsIdentity == null) { return; } var id = formsIdentity; var ticket = id.Ticket; var userData = ticket.UserData; // Get the stored user-data, in this case, our roles var user = JsonConvert.DeserializeObject<User>(userData); var customPrincipal = new UserPrincipal(formsIdentity, user.RolesList.ToArray()); customPrincipal.Email = user.Email; Thread.CurrentPrincipal = Context.User = customPrincipal; }
The userdata is encrypted and safe from tampering, but it can make the cookie rather large.
.Net 4.5 making claims
Now in ASP.net 4.5, we have Windows Identity Foundation (WIF, pronounced "dub-i-f") and claims principals and identities. Usually this is discussed with "federation" and single-sign-on identity providers, but actually claims can be useful in "traditional" stand-alone websites like we've just discussed.
ClaimsPrincipals and Identities have a list of Claims. This can be just a property bag with names and values, but there are many standard claim names, defined by OASIS, in the ClaimTypes enum. In addition to ClaimTypes.Name and ClaimTypes.Role, there are Email, GivenName, Surname, DateOfBirth, MobilePhone and so on. These standard defined types mean libraries can discover these claims without defining common interfaces or contracts. But it is also extensible with application specific claims. The old fixed custom principal is starting to look old-fashioned.
The WIF session authentication module can take over from forms authentication, storing the claims in cookies. You don't need to use the federation aspects. The module handles cookies a little better than forms authentication- if it gets too large, it's chunked over several cookies. There is also a ReferenceMode = true, which keeps the claims data in server side memory and only sends a simple key in the cookie (it's obviously not webfarm safe).
FormsAuthentication with Claims
First, the configuration. You'll need to define the configuration sections. We are still using forms authentication, so keep the authentication mode=Forms. Add the WIF session authentication module, which will handle the cookie.
<configuration>
<configSections>
<section name="system.identityModel"
type="System.IdentityModel.Configuration.SystemIdentityModelSection,
System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
<section name="system.identityModel.services"
type="System.IdentityModel.Services.Configuration.SystemIdentityModelServicesSection,
System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
</configSections>
<system.web>
<compilation debug="true" targetFramework="4.5" />
<httpRuntime targetFramework="4.5" />
<authentication mode="Forms">
<forms loginUrl="~/Account/Login" />
</authentication>
</system.web>
<system.webServer>
<modules>
<add name="SessionAuthenticationModule"
type="System.IdentityModel.Services.SessionAuthenticationModule,
System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
preCondition="managedHandler" />
</modules>
</system.webServer>
If you aren't using SSL, you need to add the following (note the configSection defined above):
<system.identityModel.services> <federationConfiguration> <cookieHandler requireSsl="false" /> </federationConfiguration> </system.identityModel.services>
In the login page, set the user properties into claims.
private void SetClaimsCookie(User user) { var claims = new List<Claim>(); claims.Add(new Claim(ClaimTypes.Name, user.Name)); claims.Add(new Claim(ClaimTypes.Email, user.Email)); foreach (var role in user.RolesList) { claims.Add(new Claim(ClaimTypes.Role, role)); } //needs an authentication issuer otherwise not authenticated var claimsIdentity = new ClaimsIdentity(claims, "Forms"); var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); var sessionAuthenticationModule = FederatedAuthentication.SessionAuthenticationModule; var token = new SessionSecurityToken(claimsPrincipal); sessionAuthenticationModule.WriteSessionTokenToCookie(token); }
You don't need the PostAuthenticateRequest event- the WIF session module is doing that bit.
And that's it! [Authorize("Admin")] attributes work as normal. Retrieving the claims is simple.
var cp = (ClaimsPrincipal)User; var email = cp.FindFirst(ClaimTypes.Email);
Logging out looks like this:
public ActionResult SignOut() { var sessionAuthenticationModule = FederatedAuthentication.SessionAuthenticationModule; sessionAuthenticationModule.CookieHandler.Delete(); //FormsAuthentication.SignOut(); return Redirect("~/"); }