context.User NULL after ticket timeout

We encountered a strange bug. Let me give you the context first. We use forms authentication and a role module (http module) for authenticating the user so as to allow role based access to member pages. In our global.asax page, we verify and renew FormsAuthentication ticket in FormsAuthentication_OnAuthenticate event.

What was happening was that after the FormsAuthentication ticket timed out, the user was logging out when any link was clicked. However, if any other link was clicked immediately after that, the user was authenticated and was able to navigate to that page.

Here is what we do in our role module, in the Application_OnPostAcquireRequestState event:

HttpApplication application = source as HttpApplication;
HttpContext context = application.Context;
if (context.User.Identity.IsAuthenticated &&
         (!Roles.CookieRequireSSL || context.Request.IsSecureConnection))
{
  //Read the roles data from the Roles cookie..
  context.User = new CustomPrincipal(context.User.Identity, cookieValue);
  Thread.CurrentPrincipal = context.User;
}

As you can see, if context.User is set, the code inside will get executed and context.User will be re-initialized. However, when the ticket times out, context.User becomes NULL and context.User is not reset. While searching for the issue, I came across the MSDN documentation for FormsAuthentication_OnAuthenticate event handler delegate. It clearly states that:

If you do not specify a value for the User property during the FormsAuthentication_OnAuthenticate event, the identity supplied by the forms authentication ticket in the cookie or URL is used.

This was a great help, as I finally found the root cause. What happens is when the ticket times out, the event tries to decrypt the FormsAuthentication ticket and create a FormsIdentity instance which is then used to initialize context.User. However, in our case, we are not setting ticket.Name with the username. Instead, its an empty string. So, it must be that Authenticate event is not able to set the context.User object by decrypting the renewed ticket.

To solve this, in case ticket is expired, I renew the ticket and create a GenericIdentity object, which I then use to initialize context.User. Here is the code:

if(ticket.Expired)
{
  //renew the ticket..
  IIdentity identity = new GenericIdentity(username, "Forms");
  context.User = new CustomPrincipal(identity);
}
After this, when the role module is called, in the Application_OnPostAcquireRequestState event, context.User is found to be true and the code proceeds normally. The end user, even after ticket timeout, navigates to the page he clicked on.

Related readings: