Big picture thoughts on software and other topics

January 25, 2008

Cookies + Redirects == Nightmares

by Brian Donahue

I just wasted probably 8 hours fiddling with cookies in a Monorail app.  The troublesome cookie was the AuthCookie set when using FormsAuthenticationBroken Link: http://samples.gotdotnet.com/quickstart/aspplus/doc/formsauth.aspx

The primary fault was of course my own, in that I was trying to take control of the Authetication process, but still use some of the built-in FormsAuthentication stuff rather than re-write everything from scratch.  Without boring you with details (and after staring at it for 8 hours, I'm likely to misconstrue facts/timeline), I'll give you the very shortened story of my trials.  There were 2 main issues.  The first - most of the time, the logout button didn't log you out.  The second, sometimes the site seemed to not recognized an authenticated user, and push them back to the login screen, which would recognize them as authenticated (so you'd see a login form, and a nice "logout" button in the header).

I started with logout not working.  Seemed simple enough -  the cookie must not be getting killed appropriately.  After some FiddleringBroken Link: http://fiddler2.com/ (I want to have Fiddler's children!), I realized that in some circumstances the auth cookie was being set with my local application path, "/MyVirtualDirectory", and sometimes with no path, "/".  This actually creates 2 cookies, and the logout was only killing one of them.  The real story is a little trickier in that I was initially trying to check for the existence of the AuthCookie in the Response, and if it was there, expire it.  Checking for a cookie in the response, effectively creates an empty cookie with that name.  Nice.  But this was my own fault because I wasn't remembering that the Response only has cookies in it if you are setting a new cookie, as opposed to the Request, which carries all the cookies for that path every time (or so you'd think, see below).  So, I cleaned up my mess, made sure only one cookie was set, and only one cookie was killed, on logout.  Now, all my cookie usage was nice and clean, and every cookie was being set with the current ApplicationPath (HttpContext.Request.ApplicationPath or "/MyVirtualDir") which should keep things nice and clean right?

Then I moved on to the random authentication failures.  I quickly found out they weren't random (there is some programming addage about nothing being impossible, or truly random, you just haven't looked hard enough).  What was happening was that on POST requests that returned a 302 redirect response, the browser was not including the AuthCookie in the redirected request.  Like so:

POST:  /profile/create - includes AuthCookie
RESPONSE:  302 Redirect to /profile/index
GET
: /profile/index - no AuthCookie!
RESPONSE: 302 Redirect to login page because authentication fails

WHY ?  WHY ME, WHY?!  WHY?!!?  Why wasn't the browser sending the cookie on the redirect?   Well, dear reader, maybe YOU knew this, but I sure didn't... browser's don't send cookies on redirects UNLESS the redirect response itself set the cookie.  I'm sure there is some reason for this, but I don't get it.

So, how was I supposed to send the cookie again, only on redirects?  I didn't know.  So I decided I'd re-set the cookie on every request, which would have the added benefit of letting me use sliding expiration on the cookie, because I could update the expiration time on every request.  I did so. 

SAME PROBLEM.  I saw the cookie getting set in my 302 response, but the subsequent request was NOT passing it along.  WTF!!??!?  I am sparing you a lot of my trial and error, but what I discovered was that the redirected request would only include the cookie if I set the path to the domain root ("/"). 

So, I had to make sure all my cookie setting code (which was very nicely factored into an HttpHelper class, thank you) set the cookie paths to the root path, and EVERYTHING WORKED. 

Cookies are complicated mofos.  They seem so simple (it's just some strings, right!!!?!), but they can bite you badly.  I still don't understand the rules around when browsers do and don't pass along cookies in redirect scenarios, and I wasn't able to find any specs/standards/rules about it.  All I know is I got it to work and I'm happy for now.