Dealing with Token Timeout in Windows Identity Foundation

When a Security Token Service (STS) creates a token, that token has an absolute expiration. Usually this is about 60 minutes, after which the Relying Party (RP) has to send the user back to the STS to acquire a new token. When using Windows Identity Foundation (WIF) in ASP.NET (so for passive federation), this is the default behavior, because the SessionAuthenticationModule stores the token in the FedAuth cookie and checks that token on each request.

What’s the problem?

I hear you thinking “Cool, WIF takes care of all that for me”, and that is cool. But there’s also a nasty side effect, one which all web developers have encountered in a different context: sessions. If a user logs in, starts filling out some form, and then gets interrupted by a phone call, chances are that the session expires. This can lead to problems when the user submits the form, because the user has to log in again. The original posted data gets lost in the process, much to the frustration of the user. Because sessions use sliding expiration the problem is minor. But with absolute expiration, expiration can wreak havoc even if the user posts a form just a minute after the form was presented to the user.

Solving the problem

There are several ways to solve the problem outlined above:

  1. Don’t use the SessionAuthenticationModule.
  2. Modify the stored token to implement sliding expiration.
  3. Force reacquiring a token in the background, so you can control when this happens.

Ditching the SessionAuthenticationModule

Not using the SessionAuthenticationModule appears to be simple. Just remove it from web.config and you’re done. However, if you remove it, you have to ensure a user stays logged in after receiving the token, and you need to keep track of the received claims. This means you need to use a cookie or session data to keep track of the user. Basically you would be recreating what the SessionAuthenticationModule does for you for free.

Making token expiration sliding

This is actually easier than ditching the SessionAuthenticationModule. All you have to do is handle the SessionSecurityTokenReceived event and modify the ValidTo property of the token, as shown in this MSDN Forums post.
Tip: You can hookup the event in the Application_Start event in global.asx using FederatedAuthentication.SessionAuthenticationModule to point to the module.

Force reacquiring a token

The above methods work fine, but have some drawbacks. One of these is the fact that Single Sign-On (SSO) no longer works properly if a user spends too long in a single application. This is because the login session with the Identity Provider will expire at some point. Another issue arises if you want to use delegation or impersonation when you call a web service. WS-Trust 1.4 supports delegation (ActAs) and impersonation (OnBehalfOf). Even though WIF officially implements WS-Trust 1.3, WIF does support these constructs. When calling web services from a web application, using delegation is a recommended practice, because it greatly improves the security. The reason is that in order for the web application to make the web service call on your behalf, it needs to acquire a token from the STS, based on the original token. This means a malicious user would need that token in order to make the web service call. Breaking into the web application is not enough.

So, how can you ensure you get a new token once in a while? Although the implementation is somewhat more difficult, the principle is simple: logout of the RP. That triggers the RP to re-authenticate the user by redirecting to the STS. As long as the user is still known in the STS, a new token is transparently given out. You can do this in roughly two ways: in an invisible iframe that does this process under the covers or in the main request stream. The latter is more visible to the user and requires you to bring the user back to where he was before the logout was forced, which is slightly more complicated than a simple redirect. The iframe solution is more elegant, because it does not interrupt the main working process. Basically all you have to do is point the iframe to a handler that logs the user out if necessary, triggering the re-authentication process when necessary and coming back to the handler to produce an empty HTML page. The handler to do this is shown in the code below.

using System;
using System.Configuration;
using System.Globalization;
using System.IdentityModel.Tokens;
using System.Threading;
using System.Web;
using Microsoft.IdentityModel.Claims;
using Microsoft.IdentityModel.Web;
namespace WebApp
{
    /// 
    /// HTTP Handler triggering token renewal when this is necessary.
    /// 
    public class TokenRenewalHandler : IHttpHandler
    {
        /// 
        /// The default number of minutes before the token expires when renewal should be triggerd.
        /// 
        private const int DefaultTokenRenewalWindow = 20;

        /// 
        /// Value container for the TokenRenewalWindow
        /// 
        private int m_TokenRenewalWindow = 0;

        /// 
        /// Number of minutes before the token expires when renewal should be triggerd.
        /// 
        /// Should be less or equal to the session timeout.
        protected int TokenRenewalWindow
        {
            get
            {
                if (m_TokenRenewalWindow == 0)
                {
                    int securityTokenRenewalWindow;
                    if (!int.TryParse(
                           ConfigurationManager.AppSettings["SecurityTokenRenewalWindow"],
                           NumberStyles.Integer, CultureInfo.InvariantCulture,
                           out securityTokenRenewalWindow))
                    {
                        securityTokenRenewalWindow = DefaultTokenRenewalWindow;
                    }
                    m_TokenRenewalWindow = securityTokenRenewalWindow;
                }
                return m_TokenRenewalWindow;
            }
        }

        /// 
        /// Handles the HTTP reqyest and forces a signout to renew the token if necessary.
        /// 
        public void ProcessRequest(HttpContext context)
        {
            SecurityToken token = null;
            IClaimsPrincipal principal = Thread.CurrentPrincipal as IClaimsPrincipal;
            if (principal != null && principal.Identities.Count > 0)
            {
                token = principal.Identities[0].BootstrapToken;
                if (token != null)
                {
                    DateTime tokenExpirationTime = DateTime.Now.ToUniversalTime().AddMinutes(TokenRenewalWindow);
                    if (token.ValidTo.ToUniversalTime().CompareTo(tokenExpirationTime) < 0)
                    {
                        if (FederatedAuthentication.WSFederationAuthenticationModule != null)
                        {
                            // Force sign-out.
                            FederatedAuthentication.WSFederationAuthenticationModule.SignOut(false);
                        }
                        // Redirect to this handler to force a new sign-in request to the STS
                        context.Response.Redirect(context.Request.RawUrl, true);
                    }
                }
            }
            // Return an empty HTML page
            context.Response.ContentType = “text/html”;
            context.Response.Write(“”);
        }

        public bool IsReusable
        {
            get { return false; } }
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *