Wednesday, September 5, 2012

Forms Authentication revisited

Forms authentication is an authentication module, known for years and quite reliable. The core idea is that the authentication pipeline takes the so called forms authentication cookie, decrypts the cookie and sets the HttpContext.Current.User for a request. Forms authentication subsystem contains an API which can issue cookies, in particular you can attach a small portion of custom data to the cookie so that the data is available as long as the user is logged in.

Issues

There are two common issues around the Forms authentication module: the custom data cannot be too long and because it is a string – there is no natural way to make it “compound” (contain a structure of few fields).

The second issue can possibly be solved by creating your own structure (XML for example), serializing and deserializing the data. Nothing out of the box.

However, I haven’t found any solution to the the first issue. Let’s face it:

string ReallyLongUserData
{
    get
    {
        Random r = new Random();
 
        StringBuilder sb = new StringBuilder();
 
        while ( sb.Length < 8192 )
            sb.Append( r.Next().ToString() );
 
        return sb.ToString();
    }
}
 
...
 
// create a forms cookie with really long userdata (>8192)
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
    1, txtUserName.Text, DateTime.Now, 
    DateTime.Now.AddMinutes( 20 ), false, ReallyLongUserData );
 
HttpCookie cookie = new HttpCookie( FormsAuthentication.FormsCookieName );
cookie.Value = FormsAuthentication.Encrypt( ticket );
this.Response.AppendCookie( cookie );
 
Response.Redirect( this.Context.Request.QueryString["ReturnUrl"] );

Guess what happens if you run this.

The answer is: the cookie is signed and encrypted and appended to the response. But apparently, the cookie size clearly exceedes the maximum size of the cookie browsers can handle. Most browsers ignore the cookie then! The cookie is missing from subsequent requests.

SessionAuthenticationModule for the rescue!

If there’s a way to replace the FormsModule with a custom module capable of handling multiple cookies, then the cookie size issue would just be gone. Let the module just split the long value to multiple cookies, TheCookie1, TheCookie2, … and then, at the server, join the cookie data and recreate the identity. And what if the module supports multiple userdata entries, a dictionary which maps keys to values possibly?

Sounds great?

Well, there IS such module. It is called SessionAuthenticationModule.

It is part of the Windows Identity Foundation and normally it is used in federation scenarios where identity cookie is issued according to SAML tokens from identity providers (here it doesn’t really matter what a SAML token is). If you work with WIF, you know the module.

However, the SessionAuthenticationModule can be used in a normal application, just to replace the incapable Forms module and provide additional features.

First, the configuration:

<configuration>
 
    <configSections>
        <section name="microsoft.identityModel" type="Microsoft.IdentityModel.Configuration.MicrosoftIdentityModelSection, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    </configSections>
 
    <system.web>
 
        <authentication mode="Forms">
            <forms loginUrl="LoginPage.aspx" />
        </authentication>
        
        <authorization>
            <deny users="?"/>
            <allow users="*"/>
        </authorization>
 
        <httpModules>
            <add name="SessionAuthenticationModule" 
                 type="Microsoft.IdentityModel.Web.SessionAuthenticationModule, 
                       Microsoft.IdentityModel, Version=3.5.0.0, 
                       Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
        </httpModules>
 
        <compilation debug="true" targetFramework="4.0" />
    </system.web>
 
    <system.webServer>
        <validation validateIntegratedModeConfiguration="false" />
        <modules>
            <add name="SessionAuthenticationModule" 
                 type="Microsoft.IdentityModel.Web.SessionAuthenticationModule, 
                       Microsoft.IdentityModel, Version=3.5.0.0, 
                       Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler" />
        </modules>
    </system.webServer>
 
    <microsoft.identityModel>
        <service>
            <federatedAuthentication>
                <cookieHandler name="ADWebFrontEndCookie" requireSsl="false" />
            </federatedAuthentication>
        </service>
    </microsoft.identityModel>
 
</configuration>

Note that I declare that I am using Forms module. This is to utilize the Forms ability to authorize Url access and redirect the browser to the login page in case of insufficient priviledges. The SAM module’s cookie name is also declared in the module’s section.

And there comes the code:

string ReallyLongUserData
 {
     get
     {
         Random r = new Random();
 
         StringBuilder sb = new StringBuilder();
 
         while ( sb.Length < 8192 )
         //while ( sb.Length < 512 )
             sb.Append( r.Next().ToString() );
 
         return sb.ToString();
     }
 }
 
...
SessionAuthenticationModule sam = 
   (SessionAuthenticationModule)
   this.Context.ApplicationInstance.Modules["SessionAuthenticationModule"];
 
IClaimsPrincipal principal = 
   new ClaimsPrincipal( new GenericPrincipal( new GenericIdentity( txtUserName.Text ), null ) );
 
// create any userdata you want. by creating custom types of claims you can have
// an arbitrary number of your own types of custom data
principal.Identities[0].Claims.Add( new Claim( ClaimTypes.Email, "foo@bar.com" ) );
principal.Identities[0].Claims.Add( new Claim( ClaimTypes.UserData, ReallyLongUserData ) );
 
var token = 
    sam.CreateSessionSecurityToken( 
        principal, null, DateTime.Now, DateTime.Now.AddMinutes( 20 ), false );
sam.WriteSessionTokenToCookie( token );
 
Response.Redirect( this.Context.Request.QueryString["ReturnUrl"] );
     

This code replaces the previous code – instead of issuing a Forms cookie, now I am issuing a SAM cookie.

And …. that’s it. The cookie is created, it is automatically split into smaller cookies so that the browser’s limit doesn’t break anything. Also, now I can easily create any type of custom data I want by mapping claim types to values. Note that multiple claims of the same type can be assigned to the identity.

Authorization with the SAM module

Another surprizing benefit of using the SAM module is that it simplifies role management and authorization. Upon each request, the module recreates the identity which is of type ClaimsPrincipal. This class has the IsInRole method already implemented. And the way you feed it with roles is straightforward:

IClaimsPrincipal principal = 
   new ClaimsPrincipal( 
      new GenericPrincipal( new GenericIdentity( txtUserName.Text ), null ) );
principal.Identities[0].Claims.Add( new Claim( ClaimTypes.Email, "foo@bar.com" ) );
principal.Identities[0].Claims.Add( new Claim( ClaimTypes.UserData, ReallyLongUserData ) );
 
// roles, stored in the cookie as claims!
principal.Identities[0].Claims.Add( new Claim( ClaimTypes.Role, "ADMIN" ) );

It turns out then that you don’t really have to change much in your application - the authorization should work, including the static authorization with authorization sections of web.config files. What you only need to provide is to add role claims when you create the principal object.

Happy coding.

No comments: