Skip to content

Replacing forms authentication with WIF’s session authentication module (SAM) to enable claims aware identity

January 26, 2013

Forms authentication was great. For like 10 years it was great. But it’s time for us to move on…

The main issue with Forms authentication is that the forms auth cookie was primarily only designed to keep the user’s username and no additional data (despite the UserData property and the unfortunate lack of APIs to assist populating it and managing the ticket and cookie). So to augment the username with roles or additional identity data (typically from the database) we would use the PostAuthenticateRequest event in ASP.NET/IIS (as discussed here and here). The main issue with PostAuthenticateRequest is the additional round trip to the database on each request into the web server. Caching can help mitigate this, of course, but you have to explicitly do the caching.

Now that .NET 4.5 is claims-aware, I’d submit that using forms authentication is antiquated. Dominick already posted on this a while ago, but I wanted to show what’s involved from a more introductory perspective. I’d suggest reading Dom’s post for more motivation. I’ll show here how to use the SAM directly.

This is what traditional login code could would look like with Forms authentication:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Login(string username, string password)
{
    if (DoDatabaseCheckToValidateCredentials(username, password))
    {
        // set username in cookie, false issues non-persistent cookie
        FormsAuthentication.SetAuthCookie(username, false);
        return RedirectToAction("Index", "Home");
    }
    else
    {
        ModelState.AddModelError("", "Invalid username or password.");
    }
    return View();
}

This would simply log the user in. We’d then have to load their roles or claims on each and every request as such in global.asax:

void Application_PostAuthenticateRequest(object sender, EventArgs e)
{
    var ctx = HttpContext.Current;
    if (ctx.Request.IsAuthenticated)
    {
        string[] roles = LookupRolesForUser(ctx.User.Identity.Name);
        var newUser = new GenericPrincipal(ctx.User.Identity, roles);
        ctx.User = Thread.CurrentPrincipal = newUser;
    }
}

So with WIF 4.5 they have a different http module that will do what the forms authentication http module does, but it’s claims aware. This means it allows you to assign claims (and thus roles) at login time. This identity information is cached in the cookie and therefore you won’t have to re-query the database upon each subsequent request. Here’s the code:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Login(string username, string password)
{
    if (DoDatabaseCheckToValidateCredentials(username, password))
    {
        Claim[] claims = LoadClaimsForUser(username);
        var id = new ClaimsIdentity(claims, "Forms");
        var cp = new ClaimsPrincipal(id);

        var token = new SessionSecurityToken(cp);
        var sam = FederatedAuthentication.SessionAuthenticationModule;
        sam.WriteSessionTokenToCookie(token);

        return RedirectToAction("Index", "Home");
    }
    else
    {
        ModelState.AddModelError("", "Invalid username or password.");
    }

    return View();
}

So as you can see, we do the same check against the database to validate the credentials. We then query the database for the identity information about the user and this is returned as an array of Claim objects. We then create a identity and principal from the claims and then create the token and write it to the response. On subsequent requests the cookie will be read and populate our user object.

A few details to fill in. Here’s what the code to create the claims might look like:

const string OfficeLocationClaimType = "http://brockallen.com/claims/officelocation";

private Claim[] LoadClaimsForUser(string username)
{
    var claims = new Claim[]
    {
        new Claim(ClaimTypes.Name, username),
        new Claim(ClaimTypes.Email, "username@company.com"),
        new Claim(ClaimTypes.Role, "RoleA"),
        new Claim(ClaimTypes.Role, "RoleB"),
        new Claim(OfficeLocationClaimType, "5W-A1"),
    };
    return claims;
}

The idea is that you can create any claims you’d want to model the user’s identity for you application (including custom claims like the last one in the list). To access the claims on subsequent requests, you’d use the old non-claims aware APIs or the new claims-aware APIs (both work against the same identity information):

void DoSomeStuff()
{
    // old style role check
    if (User.IsInRole("RoleA"))
    {
        // old style username check
        var name = User.IsInRole.Name;

        // new style claim check for the email claim
        var email = ClaimsPrincipal.Current.FindFirst(ClaimTypes.Email).Value;

        // use the identity information
        SendUserEmail(name, email);
    }
}

The only last thing to mention is the configuration needed to do all of the above. First your project will need references to System.IdentityModel and System.IdentityModel.Services. And then you’ll need to add some <configSections> to web.config. And lastly you’ll need to add the SAM (session authentication module) to the http modules list:

  <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.webServer>
    <modules>
      <add name="SessionAuthenticationModule" 
            type="System.IdentityModel.Services.SessionAuthenticationModule, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
    </modules>
  </system.webServer>

And there we have it. Happy modern authentication!

23 Comments leave one →
  1. Robert Mircea permalink
    January 26, 2013 2:43 pm

    Storing claims into the cookie does not really look too efficient. Only storing the username, email address and one role claims created a cookie of almost 1Kb which will be sent on every request from browser. Are there any other ways to store claims which can trim down or offer an alternative to cookies?

    • January 26, 2013 2:56 pm

      @Robert — It’s about tradeoffs. You can certainly come up with a poor design whereby you store too many claims in the cookie. You still have the PostAuthenticateRequest as an option (http://brockallen.com/2013/01/17/adding-custom-roles-to-windows-roles-in-asp-net-using-claims/) so you could have a mix of the two models. Lastly, the SAM supports a flag called “IsReferenceMode” where the cookie only contains an identifier and then the claims are automatically cached in the server (which is customizable as needed for web farms, etc.).

      As I see it, the main benefit is the claims-aware nature of the SAM as the authentication layer.

  2. January 27, 2013 10:40 pm

    I cannot get this sample to write a cookie to the response. Is there a config setting missing? Can you post the source of your sample?

    • January 27, 2013 10:45 pm

      Use SSL or put this in web.config:

      <system.identityModel.services>
      <federationConfiguration>
      <cookieHandler requireSsl=”false”></cookieHandler>
      </federationConfiguration>
      </system.identityModel.services>

      But you really should be using SSL.

  3. January 27, 2013 10:51 pm

    That worked. Can I use this across 2 websites in azure? I am doing so with forms auth and same machine key.

    • January 27, 2013 10:57 pm

      Yep, you can use the cookie across a web farm as long as they’re all using the same key. You need to register the machine key session security token handler:

      http://msdn.microsoft.com/en-us/library/system.identitymodel.services.tokens.machinekeysessionsecuritytokenhandler.aspx

      • David Peden permalink
        January 28, 2013 1:33 pm

        Hey Brock, I have a site hosted in Azure that is using a custom security handler to leverage RSA/SSL instead of machine keys. This works fine.
        However, I have not enabled IsReferenceMode because I’m leery of writing my own SessionSecurityTokenCache implementation. I did find http://code.msdn.microsoft.com/vstudio/Claims-Aware-Web-Farm-088a7a4f but am not interested in taking a dependency on WCF.

        I suppose I could something similar using WebAPI. Do you guys have any plans to do anything in this space with the Thinktecture IdentityServer and IdentityModel?

        P.S. I have watched Dom’s Pluralsight courses and wish he went into more detail on these topics. Maybe there should be a third, advanced course. ;)

        • January 28, 2013 2:26 pm

          Yes, I plan to have a token cache soon in the Thinktecture.IdentityModel project (in the next 2-3 weeks as time permits). Stay tuned.

          • David Peden permalink
            January 28, 2013 2:41 pm

            Fantastic. Thanks for the quick reply.

          • January 29, 2013 10:00 pm

            I would be interested in a token cache using azure’s distributed caching. I am not finding any examples out there

  4. January 28, 2013 4:43 pm

    Brock will this approach work to secure things like elmah and using the web.config to block access? Trying to determine what needs to be replaced if choosing this over Forms

    • January 28, 2013 7:04 pm

      This doesn’t affect how you do authorization. This is only the authentication piece.

  5. condigno permalink
    February 18, 2013 10:57 am

    I’m curious why no mention of RolePrincipal? It uses its own cookie rather than hitting the persistence layer on each request, and is claims aware in .net 4.5. And IIRC, a recent patch has also made .net 4.0 RolePrincipal claims aware. Perhaps the answer is “no point in mentioning a mechanism that violates SRP by tying auth with persistence?”

  6. March 24, 2013 1:23 am

    Brilliant – thanks Brock!

    Just to consider – you might want to note the RequireSsl part in your post, Devs typically don’t use SSL during development. I just spent an hour hitting this wall until something clicked to try RequireSsl = false. :) Thanks again.

  7. djclausen permalink
    April 3, 2013 5:23 pm

    Brock, could I ask a question if/when you have a chance?

    What is your recommendation if someone wants to use the SAM, but also wants two traditional forms auth goodies: redirect to login (and redirect back) + sliding expiration?

    I tried using the FAM for redirects, but things got complicated quickly – and I felt like I was in overkill world, since my app isn’t federated.

    Many thanks.

  8. Nick permalink
    May 13, 2013 4:17 pm

    I tried doing this in the past and had a few issues but after finding your post I’m going to give it another shot. However I found that if I set a name to the cookie handler via the web.config (), my login stops working. Any idea why customizing the cookie name would cause this?

  9. yarxcom permalink
    May 14, 2013 1:51 pm

    I just added your example to an app as a precursor to eventually moving over to IdentityServer as an STS but I’m getting odd behavior from this implementation. I’ve set a duration of 5 seconds on the SessionSecurityToken but it seems to have no impact. Is there some other factor being used to determine the duration of the authentication?

    • May 14, 2013 6:19 pm

      Internally they have a 5 minute clock skew that they use, so if you set the duration for 5 seconds, then it will really be 5 mins and 5 seconds. You can change that clock skew in config.

      • Nick permalink
        May 14, 2013 6:38 pm

        That’s exactly what my problem was. After setting the max clock skew to 0 it behaved as expected. I changed it in the web.config at the following path

        • Nick permalink
          May 14, 2013 6:40 pm

          >system.identityModel<>identityConfiguration<>securityTokenHandlers<>securityTokenHandlerConfiguration maximumClockSkew=”0″< retrying with escaped HTML, feel free to delete if this doesn’t work.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Connecting to %s

%d bloggers like this: