Friday, 3 July 2015

The weird world of .Net Cookies and Forms Authentication

If you've ever had to do anything other than a default implementation of forms authentication in .Net, you might well have also come across some confusing bugs in your application. You might find the site still being logged in after logging out, seeming to be logged out when you just logged in and seeing auth cookies still present when you are not expecting them!

Actually, the system is quite simple but there are some things you need to understand, which will help you to debug your application and what you are doing wrong if it isn't working.

Cookies for Authentication

Firstly, you might have worked out that authentication is not really covered by HTTP. Although you can restrict access to a single resource using the auth header, it is not designed to keep state between requests, which means you have to track people. Why? Because HTTP was designed to be stateless and largely unrestricted. If I want a document, I just ask for it,.. the end. Of course, nowadays, we have applications that function more like desktop applications and totally have to be able to track people across page requests so we know who they are, what they have put in their shopping cart, what their preferences are for the site etc.

The answer is a simple HTTP concept called a cookie - a small text file which can be sent to a client from a server and which will be automatically sent back with each request to the same domain until either cookie expires or if it is a session cookie, until the browser is closed.

In most cases, an authentication mechanism will authenticate the user and then put some kind of encrypted identifier into a cookie. Each time the user comes back to the site, this cookie is sent back, the site can decrypt the contents and see if the user is authenticated. If so, continue, if not, they are sent to the login page.

In fact, in .Net, what is actually put into the auth cookie is an encrypted and encoded packet which describes a System.Web.Security.FormsAuthenticationTicket. This ticket contains a number of fields including the user name, an issue and expiration date and the path used to store the cookie.

When a user logs in, this data is collected, put into an auth cookie and sent to the client. When the client goes to another page, the data needs to be decrypted, verified and then the expiry date check to ensure the auth is still valid. If it IS, then the username is used to login the current user and potentially to load any other data you want to load.

All good so far.

Expiry Dates

There is a problem that surfaces very easily and that relates to COOKIE expiry dates. If you set your forms authentication element in web.config to use, say, 20 days expiry for the cookie but then you create a cookie that only has a 10 day expiry then guess what? The cookie will expire in 10 days and it will look like the auth hasn't correctly remembered the auth duration. If you use the built-in forms authentication, then you shouldn't see this but IF you write your own auth cookies, then you MUST make sure that you set the cookie expiry to the same as the auth expiry in the FormsAuthenticationTicket to ensure they expire at the same time.

If you do not set an expiry date on the cookie then another problem can occur: Session cookies.

Session Cookies

A session cookie (not to be confused with the cookie that stores the session in) is a cookie whose expiry is not explicitly set. What happens if you do this? The cookie will live for as long as the browser is open. This is useful for security reasons since it will help avoid accidental issues with other people logging into your account at a later date. There are two problems with this. Firstly, with multi-tabbed browsers, you have no control over if or when the user will close the browser. They might not close it for weeks, in which case, the auth ticket is still present and can be used - to avoid this, you should set the auth expiry to be a sensible value in the FormsAuthenticationTicket so that even if the browser isn't closed, the auth will still expire after a suitable time.

The second problem is related to deleting cookies.

Deleting Cookies

Lots of people have problems deleting cookies, including the auth cookie. You might have called FormsAuthentication.SignOut() but for some reason, the cookie still seems to be there and when you go back to the site, you are still logged in. This problem is not specific to .Net but is a very easy problem to cause if you use session cookies for auth (either always or as an option).

You can't actually delete a cookie from the server-end. Why? I'm not sure, but the suggested mechanism is to send back another cookie with an expiry date in the past so that the browser should (and usually does) delete the cookie.

The problem you can get is that a browser can hold multiple cookies for the same site which differ on domain or type (session or persistent). What happens if you have a session cookie for auth and then you sign out? .Net will send back a cookie with a negative expiry but guess what? If you have a cookie expiry date set (even an old one), the cookie is automatically a persistent cookie and will NOT replace the session cookie you are trying to delete. What will actually happen is the browser will think that it is a different cookie, will immediately expire the new cookie because of its expiry date and then it looks to you like the auth cookie has not been deleted. In fact, it looks like nothing has worked correctly and you are right!

How do you return an expired cookie for sessions then? You can't! All you can do is to send back an empty cookie with no expiry date set so that it overwrites the session cookie. You will then need to ensure that your system correctly handles a blank auth cookie.

If you allow session OR persistent cookies (perhaps a remember me check box) then you need to handle both cases. One will be taken of for you with FormsAuthentication.SignOut(), the other you need to code.

Some Code

The following code is a sign out function that calls SignOut() to effectively delete any persistent auth cookie and also sends back a blank session cookie to overwrite any session cookies. In this case, it also deletes the session cookie, although this is less important because in my case, the session can be abandoned in code and would effectively be deleted anyway.

internal static void SignOut(Page source)
{
    FormsAuthentication.SignOut();
    source.Session.Abandon();
    HttpCookie cookie1 = new HttpCookie(FormsAuthentication.FormsCookieName, "");
    cookie1.Domain = FormsAuthentication.CookieDomain;
    cookie1.HttpOnly = true;
    cookie1.Secure = true;
    source.Response.Cookies.Add(cookie1);
    HttpCookie cookie2 = new HttpCookie("ASP.NET_SessionId", "");
    cookie2.Expires = DateTime.Now.AddYears(-1);
    source.Response.Cookies.Add(cookie2);
    FormsAuthentication.RedirectToLoginPage();
}

Note, my version is a static method shared between all pages and also, the RedirectToLoginPage will put the current page into the url as the "return url", which you might or might not want. Otherwise, just source.Response.Redirect(..etc..); Also note that you must setup the secure and httponly flags to match your auth cookie so that it matches and overwrites the one that is already there.

Cookies for Session

Something else that people find confusing is the overlap between session and authentication. They are not the same and in most cases cannot be treated as consistent with each other. You can and probably would use session very early on in an application, before you even know who the user is. They might have selected a language, even added items into a basket, whatever, and they haven't authenticated yet. Session is tracked in a similar way to authentication using a cookie (ideally a session identifier which maps onto data stored internally on the server somewhere) but most of the time, a user can have session whether or not they are logged in.

So should these two systems ever interact? Possibly but not definitely. In some articles on security, they suggest that if the user is still logged in as indicated by a valid auth cookie BUT the session is empty, implying the session has expired, then you should log out the user and force them to log back in. This is a mechanism, to avoid the very easy problem of session and auth expiries that don't match. Why not make them match? Because session expiry extends every time you use it whereas auth doesn't by default. Is that always useful? No. If you allow the user to stay logged in for, say, 4 weeks, then their session will very likely be empty when they come back and you don't want them to login again. What you would need to do is to ensure that any important session is repopulated by the system when the user returns.

For security reasons, thought, it is recommended that when a user clicks "log out" then you abandon the session as well as calling SignOut(). This ensures that no-one can hijack the data that might have been put into the session while the user was logged in, which they could do by going back to the same site on a shared computer and the system potentially using the session to populate a load of functionality that the second person shouldn't have access to.

If possible, do not store security or auth data in the session but if you do, keep it to a minimum to reduce the outcome of a session hijack attempt.
Post a Comment