Warning: Sliding Sessions are Closer than they Appear

On a plane between Philadelphia and Oslo: I am flying there for NDC2010, where I have a couple of sessions (on WIF. Why do you ask?:-)). It’s a lifetime that I want to visit Norway, and I can’t tell you how grateful to the NDC guys to have me!

This is literally the 50th flight I am on since last August, the last fiscal year has been cR@Zy. Good crazy, but still crazy. As a result, I am astonishingly behind on my Programming WIF book and it’s now time to wrap the manuscript; I am writing every time I have a spare second, which means I have very little time for any “OOB” activity, including blogging. One example: yesterday I got a mail from Dinesh, a guy who attended the WIF workshop in Redmond,asking me about sliding sessions. That’s definitely worth a blog post, but see above re:time; hence I decided to share here on the blog the DRAFT of the section of the book in which I discuss sliding sessions. That’s yet to be reviewed, both for language and technical scrub, I expect that the final form will have much shorter sentences, less passive forms, consistent pronouns, and in general will be cleansed from all the other flaws of my unscripted style that Peter and the (awesome!) editorial team at MS Press mercilessly rubs my snout in (ok, this one is intentional exactly for making a point… say hi to Godel :-)). Also, the formatting (especially for the code and reader aids like notes) is a complete mess, but hopefully the content will be useful!

More about Sessions

I briefly touched the topic of sessions at the end of Chapter 3, where I showed you how you can keep the size of the session cookie independent from the dimension of its originating token by saving a reference to session state stored server side. WIF’s programming model goes well beyond that, allowing you complete control over how sessions are handled. Here I would like to explore with you two notable examples of that principle in action: sliding sessions and network load-balancer friendly sessions.

Sliding Sessions

By default, WIF will create SessionSecurityTokens whose validity is based on the validity of the incoming token. You can overrule that behavior without writing any code, by adding to the <microsoft.identityModel> element in the web.config something to the effect of the following:

Note: the lifetime property can only restrict the validity expressed by the token to begin with. In the snippet above I set the lifetime to 2 minutes, but if the incoming security token was valid for just 1 minute the session token will have 1 minute validity. If you want to increase the validity beyond what the initial token specified, you need to do so in code (by subclassing SessionSecurityTokenHandler or by handling SessionSecurityTokenReceived).

Now, let’s say that you want to implement a more sophisticated behavior. For example, you want to keep the session alive indefinitely as long as the user is actively working with the pages; however, you want to terminate the session if you did not detect user activity in the last 2 minutes, regardless of the fact that the initial token would still be valid. This is a pretty common requirement for Web sites which display personally identifiable information (PII),control banking operations and the like. Those are cases in which you want to ensure that the user is in front of the machine and the pages are not abandoned at the mercy of anybody walking by.

In Chapter 3 I hinted at the scenario, suggesting that it could be solved by subclassing the SessionAuthenticationModule: that would be the right strategy if you expect to reuse this functionality over and over again across multiple applications, given that it neatly packages it in a class you can include in your codebase. In fact, Sharepoint 2010 offers sliding sessions and implemented those precisely in that way. If instead for you this is an improvement you need to apply only occasionally, or you own just one application, you can obtain the same effect simply by handling the SessionSecurityTokenReceived event. Take a look at the following code.

As you certainly guessed, this is a fragment of the global.asax file of the RP application. SessionSecurityTokenReceived gets called as soon as the session cookie is deserialized (or resolved from the cache if we are in session mode). Here I verify if we are within the second half of the validity window of the session token: if we are, I extend the validity to another 2 minutes, starting from now. The change takes place on the in memory instance of the SessionSecurityToken: setting ReissueToken to true instructs the SessionAuthenticationModule to persist the new settings in the cookie once the execution leaves SessionSecurityTokenReceived. Let’s say that the token is valid between 10:00am and 10:02am: if the current time falls between 10:01am and 10:02am, say 10:01:15, the code sets the new validity boundaries to go from 10:01:15 to 10:02:15 and saves those in the session cookie.

Note: Why renewing the session only during the second half of the validity interval? Well, writing the cookie is not for free; this is just a heuristic for reducing the times in which the session gets refreshed, but you can certainly choose to apply different strategies.

If the current time is outside the validity interval, this implementation of SessionSecurityTokenReceived will have no effect; the SessionAuthenticationModule will take care of handling the expired session right after. Note that an expired session does not elicit any explicit sign out process. If you recall the discussion about SSO and Single Sign-Out just few pages earlier, you’ll realize that if the STS session outlives the RP session the user will just silently re-obtain the authentication token and have the session renewed without even realizing anything ever happened.

Sessions and Network Load Balancers

By default, session cookies written by WIF are protected via DPAPI, taking advantage of the RP’s machine key. Such cookies are completely opaque to the client and anybody else who does not have access to that specific machine key.

This works well when all the requests in the context of a user session are all aimed at the same machine: but what happens when the RP is hosted on multiple machines, for example in a load balanced environment? A session cookie might be created on one machine and sent to a different machine at the next postback: unless the two machines share the same machine key, a cookie originated from machine A will be unreadable from machine B.

There are various solutions to the situation. One obvious one is using sticky sessions, that is to say guaranteeing that a session beginning with machine A will keep referring to A for all the subsequent requests. I am not a big fan of that solution, as it dampen the advantages of using a load balanced environment. Furthermore, you may not always have a say in the matter – if you are hosting your applications on third party infrastructure (such as Windows Azure) your control on the environment will be limited.

Another solution would be synchronizing the machine keys of every machine. I like this better than sticky sessions, but there is one that I like even better. Most often than not your RP application will use SSL, which means that you need to make the certificate and corresponding private key available on every node: it makes perfect sense to use the same cryptographic material for securing the cookie in load balancer friendly way.

WIF makes the process of applying the strategy above in ASP.NET applications really trivial: the following code illustrates how it could be done.

Instead of the usual inline approach, this time I am showing you the codebehind file global.asax.cs. OnServiceConfigurationCreated is, surprise surprise, a handler for the ServiceConfigurationCreated event and fires just after WIF read the configuration: if we make changes here we have the guarantee that will be applied already from the very request coming in.

Note: It is worth noting that, contrary to what various samples out there would lead you to believe, OnServiceConfigurationCreated is pretty much the only WIF event handler that should be associated to its event in the Application_Start. This has to do with the way (and the number of times) in which ASP.NET invokes the handlers though the application lifetime.

The code is pretty self-explanatory. It creates a new list of CookieTransform, which take care of cookie compression, encryption and signature. The last two take advantage of the RsaxxxxCookieTransform, taking in input the certificate defined for the RP in the web.config.

Note: Why do we sign the cookie, wouldn’t be enough to encrypt it? If we use the RP certificate, encryption would not be enough. Remember, the RP certificate is a public key. If we would just encrypt, a crafty client could just discard the session cookie, create a new one with super-privileges in the claims and encrypt it with the RP certificate. If encryption would be the only requirement, the RP would not be able to tell the difference. Adding the signature successfully prevents this attack, as it requires a private key which is not available to the client or anybody else but the RP itself.

The new transformations list is assigned to a new SessionSecurityTokenHandler instance, which is then used for overriding the existing session handler: from now on, all session cookies will be handled using the new strategy. That’s it! As long as you remember to add an entry for the service certificate in the RP configuration, you’ve got NLB-friendly sessions without having to resort on compromises such as sticky sessions.

Great post. I would like to point out one caveat when doing sliding expiration in SessionAuthenticationModule_SessionSecurityTokenReceived. If your asp.net code uses asp.net impersonate via web.config (ours did) and touches any windows secured resource in this event (i.e. sql), it will happen under the identity context of the app pool user and not the asp.net impersonated user. We found this out the hard way and had to ditch using asp.net impersonation and switch to just setting our app identity on the iis application pool. Would be nice if the W.I.F. docs / samples mentioned something to this effect.