The Case of the Expiring Auth Ticket

Consider the following scenario: a rich client communicates over the Internet with an ASP.NET service that does not use forms authentication, and also sends requests to a content site that does use forms auth. Initial user authentication is performed by the client and service using a proprietary protocol. The service then creates an authentication ticket for the user that will permit access to the content site. The ticket is created using the FormsAuthentication.GetAuthCookie method, and passed back to the client to be used in later requests. On the content site the web.config file specifies that the authentication ticket timeout is 120 minutes on a sliding reset, however the ticket consistently expires in 30 minutes. What’s wrong?

My colleagues and I recently faced this exact problem on a current development project. In researching the issue we first made sure that our web.config settings were correct. Using the IIS service manager we verified that the values for the timeout, sliding expiration, cookie name, and machine keys were being picked up and used by the server. The timeout was clearly being set to 120 minutes, and everything else was correct, but still the ticket was expiring at 30 minutes. Obviously our config files weren’t at fault.

Configuration can be a tricky thing in ASP.NET, however, and sometimes the properties that end up driving your system are not the ones you thought were at the wheel. Further research led us to a Microsoft support article on forms auth tickets and cookies. The last paragraph in the article contained the following statement:

“If the forms authentication ticket is manually generated, the time-out property of the ticket will override the value that is set in the configuration file.”

Aha! Surely we had the culprit in hand. Someone had set an explicit timeout in the code that generated the cookie, and that timeout was overriding the values set in web.config. It made perfect sense, except for the fact that when we opened up the code the line in question did not set a timeout. In fact we could see no way of specifying a timeout in the call to GetAuthCookie. Lacking an explicit timeout the value in web.config should be used. We were prepared to head back to the drawing board, or at least Google, when one of my smarter coworkers wondered if, in fact, some default timeout was being set during the call to GetAuthCookie? With the help of a friend who had a copy of .NET Reflector handy we peeled System.Web.Security. Ultimately the public versions of FormsAuthentication.GetAuthCookie call a private overload. The disassembly of that method looks like this:

private static HttpCookie GetAuthCookie(string userName, bool createPersistentCookie,
	string strCookiePath, bool hexEncodedTicket)
{
	Initialize();
	if (userName == null)
	{
		userName = string.Empty;
	}
	if ((strCookiePath == null) || (strCookiePath.Length < 1))
	{
		strCookiePath = FormsCookiePath;
	}
	FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(2, userName,
		DateTime.Now, DateTime.Now.AddMinutes((double) _Timeout),
		createPersistentCookie, string.Empty, strCookiePath);
	string str = Encrypt(ticket, hexEncodedTicket);
	if ((str == null) || (str.Length < 1))
	{
		throw new HttpException(SR.GetString("Unable_to_encrypt_cookie_ticket"));
	}
	HttpCookie cookie = new HttpCookie(FormsCookieName, str);
	cookie.HttpOnly = true;
	cookie.Path = strCookiePath;
	cookie.Secure = _RequireSSL;
	if (_CookieDomain != null)
	{
		cookie.Domain = _CookieDomain;
	}
	if (ticket.IsPersistent)
	{
		cookie.Expires = ticket.Expiration;
	}
	return cookie;
}

The part that caught our eye right away was the constructor call to create the FormsAuthenticationTicket. Specifically the fourth argument to the call, which is calculated from DateTime.Now.AddMinutes((double) _Timeout). Hmm, _Timeout, a private field, and obviously relevant to our issue. But where was it getting a value? The first line of GetAuthCookie is a call to FormsAuthentication.Initialize, and so we disassembled that, too. In it we found the following line:

_Timeout = (int) authentication.Forms.Timeout.TotalMinutes;

So the timeout value being passed to the constructor was coming from the forms element of the authentication section in our web.config! But we had already verified the web.config was correct… errrm… actually, we had only verified that the web.config values on the content site were correct. The cookie was being created in the service implementation, a different site altogether, and one with its own configuration. When we looked in that web.config we saw that the forms element did not specify a timeout for the ticket. Since it didn’t the value that was ultimately assigned to _Timeout and passed to the constructor for the ticket was the default, and that is… you guessed it, 30 minutes.

Ultimately the solution that worked was to specify the same timeout value in web.config both on the content site where the ticket would be used, and in the service implementation where it was created. And while having a dependency between those two was less than satisfying, getting this vexing bug to go away more than made up for it.

2 thoughts on “The Case of the Expiring Auth Ticket

  1. One thing to note is that FormsAuthentication.GetAuthCookie is a shortcut method that performs three core tasks:

    1) Generates a Forms Authentication ticket.
    2) Encrypts the Forms Authentication ticket.
    3) Creates an HttpCookie.

    It is possible to perform these steps manually, which allows you to explicitly control the properties of the FormsAuthenticationTicket and the cookie without any interaction with, or dependencies on, the configuration. To do so:

    1) Create a new FormsAuthenticationTicket object, which has a full set of overloaded constructors, and public properties.

    2) Encrypt that ticket by passing it as the parameter to FormsAuthentication.Encrypt. Note that the encryption uses the machine key. Any machines in a web farm or a shared ticket scenario will need to ensure that the machine keys are sync’d.

    3) Create a new HttpCookie object, using the encrypted ticket as the value.

    In my opinion, the documentation around the GetAuthCookie method is poor, and really should make it clear that it is coupled to the configuration.

  2. Good points. After thinking about my conclusions a bit more I’m pretty certain that you only need to have the timeout value and sliding expiration flag set in the web.config on the site that creates the ticket. But in our case we’re using shared forms auth across five applications, and that is supposed to require that the authentication sections are the same for each. So I think we’ll leave it the way it is.

Leave a Reply

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