Recovered from DotNetJunkies blog -- Originally Posted: Wednesday, October 25, 2006
As a new approach to some meaningful blogging, as I work on debugging some new bug/issue, the info I collect and my deductions I record right in my LiveWriter window so that I can save any links, code snippets etc. This way, with little effort, at the end of the day I can do some quick editing and post something "useful" (you may choose to retain your own definition of that word). A fringe benefit is that I can find this info again the next time I need it.
So here was the situation:
An ASP.NET application that leverages FormsAuthentication. The timeout is set in the web.config to 20 minutes in the authentication tag something like this:
Users were reporting intermittent timeouts that were significantly less than the 20 minutes. In testing we were unable to duplicate this scenario.
The other important detail, that we have omitted until now, is that we are "rolling our own" authentication ticket in order to have more control over what that ticket contains etc. So in code the default ticket timeout value (set as a constant) was 20 minutes.
So, now onto the mistaken assumptions. I had assumed that the web.config setting would come into effect the first time the ticket was updated. Being that it is a sliding expiration I had thought that the ticket would be updated to the web.config timeout the next time a page was requested. Since the user is redirected from the login page as a matter of course it was assumed (doh!) that the hard-coded timeout value would not last longer than a second or two. So, the first thing I did was set the web.config timeout to be 1 minute so that I could test without waiting 20 minutes. Waited a minute .... posted back ... no dice, still logged in. No matter what I set that value to I could not get kicked out if I tried. Only when I changed to constant to 1 in the code and recompiled was I able to get kicked out as expected. Ok, bad assumption, no big deal, we'll just change the code to read the config value when it sets the original ticket and then all will be in sync, right? Not so easy.
First of all, the values in that section of the config are protected, even from the guy writing the application. The normal system.configuration approach does not expose these values and the FormsAuthentication classes do not expose what you would think is a fairly useful property. So how do you make sure you are in sync with the config file? Doing some googling we find that Scott Hanselman ran into the same thing years ago, no surprise. Scott details his thought process in solving the same issue.
Scott addresses the pain of accessing the config setting from code so that the first creation of the ticket can match the desired setting from the web config. He considers the approach of creating an additional, accessible, config setting that would require being set in two places or creating an invalid situation. (i.e. one setting says expire in 20 minutes, one says 10 etc). He considers reflection, but deems it too ugly. Finally he settles on dynamically "discovering" the web.config, loading into a DOM and xpathing his way to the setting and storing in cache so he only has to do it once. Basically there is no clean work-around so you have to settle for a lesser of the evils approach.
So, armed with that information we start testing with our new found knowledge. We had written a test page to dump out the contents of the ticket to a text box so that we could see what expiration was actually being set. We set the original ticket being written by the code to a nice small value of 4 minutes and started to post the page back. Here comes the strangeness. As we post the page back the expiration time of the ticket does not change. We keep posting the page back every 15 seconds or so and eventually it decided it was time to update the ticket with a new expiration time. Now were really confused, this was, after all, a sliding expiration wasn't it? Every time the page posts back it was supposed to update the time to give another n minutes before it would be timed out. So back to digging, this time we pull up the MSDN documentation on the tag and the element. MSDN documentation on element attributes.
The msdn docs indicate that a sliding expiration does not really slide as you might imagine, and certainly may expire when you do not expect it to. Here is a priceless, if not a bit contradictory bit of MS wisdom: "If the SlidingExpiration attribute is true, the timeout attribute is a sliding value, expiring at the specified number of minutes after the time the last request was received. To prevent compromised performance, and to avoid multiple browser warnings for users that have cookie warnings turned on, the cookie is updated when more than half the specified time has elapsed. This might result in a loss of precision..", I guess! A "loss of precision"? Is that what we call a sliding expiration that doesn't slide and results in an effective timeout of only half what you think you set it to? So, because some people are afraid of the big bad cookie monster we are going to totally hose the well understood concept of a sliding expiration, with no "override this stupid default behavior" attribute in sight? That is insane. Now it's time to pull out Reflector and look for a clever workaround.
Inside the FormsAuthentication class we find the method that does just as described, renews the ticket only if the time left till expiration is greater than the time since the current ticket was issued.
Public Shared Function RenewTicketIfOld(ByVal tOld As FormsAuthenticationTicket) As FormsAuthenticationTicket If (tOld Is Nothing) Then Return Nothing End If Dim time1 As DateTime = DateTime.Now Dim span1 As TimeSpan = DirectCast((time1 - tOld.IssueDate), TimeSpan) Dim span2 As TimeSpan = DirectCast((tOld.Expiration - time1), TimeSpan) If (span2 > span1) Then Return tOld End If Return New FormsAuthenticationTicket(tOld.Version, tOld.Name, time1, _ (time1 + (tOld.Expiration - tOld.IssueDate)), _ tOld.IsPersistent, tOld.UserData, tOld.CookiePath) End Function
Unfortunately the method is completely self contained and is not using any other settings to determine its results. It bases the decision on whether or not to update the ticket completely on the lifespan of the current ticket and the current time. If more then half the time has elapsed then you get a new one, period. So no work-around presents itself.
Something else we learn from this code is that, if you are manually setting the original ticket, the setting in the web.config, if different, never comes into play, since the renew method is basing its renew length on the originally set length of the ticket not your silly little timeout setting. This setting just gets more useless as we go on!
Now, if you use the out-of-the-box authorization that is provided in the framework and set your ticket using the FormsAuthentication.SetAuthCookie(username,isPersistent,cookiePath) method, the config setting will hold true when the ticket is created for you, however the non-sliding expiration issue still holds true.
So, enough pain for one day, how are we going to "fix" this so that the user has the result they expect, a 20 minute timeout? First we have to sync up our code creation of the first ticket with the web.config value. I wasn't fond of Scott's xml loading solution, although it would work. Since we found that, in our case, the web.config setting was moot anyway We opt for adding a new value to the web.config to explicitly set the value the code would use, afterward the framework would continue to re-apply this value as it refreshed the cookie. This does not provide any actual conflict, but it may provide some confusion as the app is deployed and maintained. The nice thing, for once, is that the timeout attribute on the element is optional and defaults to 30 minutes. Since it is never going to be applied, what do we care? So we remove the attribute and thus remove the apparent conflict in settings.
Now, what can we do about the non-sliding expiration? Well, it's been a long enough day, so we opt for the easy: "Just double it stupid" approach. This could, if we were relying completely on the forms authentication for timeouts, allow users, in some cases, to get anywhere from 20 to 40 minutes timeout, which would be considered a problem as well. However, since we are also requiring a fresh login when the session times out, we are covered. We set the config timeout to twice what the session timeout is and we are good. The shortest the ticket will live is 20 minutes between clicks, and if it lives longer than that the session will tank and all is well.
Now you may have a more elegant solution, so feel free to share...
© Copyright 2009 - Andreas Zenker