This week I’ve been working on a problem which at the beginning seemed to be just a matter of configuration but that then took some more time that I expected to resolve… so, here it is. In my ASP.NET 2.0 application I edit my web.config to set slidingExpiration=”true” and cookieless=”true” in Forms authentication. Basically what happens is that after half of the timeout set in <forms> element is elapsed, the user is redirected to the login page; e.g. I set <forms timeout=”3″ cookieless=”UseUri” enableCrossAppRedirects=”true” /> (timeout 3 minutes), if I postback the page after 1 minute and 35 seconds, I’m redirected to the login page (but the Forms authentication ticked should still be valid…). This can’t be a cookie problem because we are not using cookies…
First, a quick review of sliding expiration: “When the SlidingExpiration is set to true, the time interval during which the authentication cookie is valid is reset to the expiration Timeout property value. This happens if the user browses after half of the timeout has expired. For example, if you set an expiration of 20 minutes by using sliding expiration, a user can visit the site at 2:00 PM and receive a cookie that is set to expire at 2:20 PM. The expiration is only updated if the user visits the site after 2:10 PM. If the user visits the site at 2:09 PM, the cookie is not updated because half of the expiration time has not passed. If the user then waits 12 minutes, visiting the site at 2:21 PM, the cookie will be expired“.
You can easilly reproduce the behavior yourself creating an ASP.NET 2.0 application (I used aspnetdb for authentication) with a DropDownList and a Submit button and assure you have the following in your web.config:
<authentication mode="Forms"> <forms name=".MYASPXAUTHTEST" loginUrl="login.aspx" enableCrossAppRedirects="true" protection="All" timeout="3" cookieless="UseUri" path="/" requireSSL="false" slidingExpiration="true" /> </authentication>
You can use the following code for your Default.aspx page:
<%@ Page Language="VB" AutoEventWireup="false" CodeFile="Default.aspx.vb"
Inherits="_Default" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Default test</title> </head> <body> <form id="form1" runat="server"> <div> <table> <tr> <td><asp:DropDownList ID="ddlTest" runat="server"> </asp:DropDownList></td> </tr> <tr><td> </td></tr> <tr> <td><asp:Button ID="btnSubmit" runat="server" Text="Button" />
</td> </tr> </table> </div> </form> </body> </html>
And in your code file:
Partial Class _Default Inherits System.Web.UI.Page Protected Sub Page_Load(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles Me.Load Response.Write("Submit at: " & DateTime.Now.ToString() & "<br>") Response.Write("Next submit at: " & _ DateTime.Now.AddSeconds(90).ToString() & "<br>") Response.Write("IsPostBack = " & IsPostBack) If Not IsPostBack Then ddlTest.Items.Add("<select>") ddlTest.Items.Add("test1") ddlTest.Items.Add("test2") ddlTest.Items.Add("test3") End If End Sub End Class
Remember you also need a login.aspx page with a standard input form to provide username and password; then to reproduce the behavior:
- Browse the Default.aspx (your should be redirected to login.aspx
- Enter your credentials and submit the page
- In Default.aspx choose random value for the DropDownList
- Wait at least 91 seconds but no more that 3 minutes (you can check the “Next Submit” value I added to the top of the page
- Click “Submit” button again… and you should see the DropDownList value is reset to the default; moreover IsPostBack returns false even if that’s a postback! This does not happen if we use cookies…
Well… it turned out that the combination of cookieless authentication and sliding expirations means that users will be randomly redirected back to their site anytime we detect that less than 50% of the TTL remains on the ticket. Because the ticket is cookieless, the only way we can refresh the ticket is to force a redirect with the new ticket placed in the Url used for the redirect.
Nice, but I still have a doubt: if we can renew the authentication ticked during a postback without going through the authentication process, why we can’t do it by changing the ticket in the Url? Isn’t this the same principle? Moreover, why the IsPostBack returns false even if that is a real postback?
The problem is that with cookieless forms authentication, the ticket is actually part of the Url. When the timeout is updated in the ticket, that means the serialized text representation of the ticket also changes. As a result we need to change the Url itself to reflect the new value. Unlike cookies, you can’t rewrite a Url on the server and have the client transparently pick up the new Url. Instead you have to force a redirect because the redirect forces the browser to reload the document and update the Url that you see in the address bar. That redirect process is also why IsPostBack returns false. The redirect is not a postback triggered by a client-side ASP.NET control. As a result, then the GET request for the redirect comes back to the server, the server doesn’t recognize it as a postback.
Thanks to Stefan Schackow for discussing this with me!
Cheers
Carlo