Persistent Login Cookies
By Thom McGrath on
If you're building a login system for a website, your users probably want one of those "Remember Me" or "Keep Me Logged In" checkboxes so they don't have to go through the login process every time. This is a hard thing to get right, so perhaps you've already done some research.
Most login implementation guides will point you towards Charles Miller's Persistent Login Cookie Best Practice, a guide that seems to be treated as the go-to implementation for login cookies. This guide, however, is flawed in practice today so I'm going to explore an alternative method.
What is wrong
Charles Miller's guide makes the assumption that all requests for a given session will come sequentially, completing one before the next begins. Excluding static resources. When the article was written in 2004, this was true. Firefox introduced a tabbed interface just two years earlier and was still picking up steam, and Internet Explorer wouldn't get tabs for another two years. Tabbed browsing meant that more often than before, the same site was open multiple times in one browser. User clicks a link to open a forum thread from their e-mail, and the browser opens a new tab for them. That user may or may not have done that a few hours earlier and still had the forum open. This behavior can, in rare cases, cause a conflict with the persistent login cookie.
Then add in AJAX, the wonderful technology we use to make pages update themselves while we wait for broader WebSocket adoption. With AJAX, you can have multiple requests going on silently in the background. If the page has been open a while, the session may have ended, causing the AJAX to consume the login cookie and retrieve another one, all without even reloading the page. If this happens while another request is running, perhaps in another tab, the login cookie has a high chance of being set back to the last token just consumed. At that point the login cookie is broken, and it's only a matter of time before the user has to log in again.
This isn't just an "in theory" problem. I see it in practice all the time in software I administer. I've read about it happening with Drupal, and even Barry Jaspan, author of a derivative work admits on a Drupal discussion list that this is a problem.
Solution #1: Turn them off
Best thing you can do is stop offering persistent logins. They are inherently insecure due to human nature. If you must offer such a feature though, this isn't really a solution.
Solution #2: Use SSL
One-time-use, disposable tokens aren't going to cut it on today's web. But permanent tokens are a very big problem if stolen. So let's only transmit persistent tokens over SSL. Your login system should be using SSL anyway - if it doesn't, you're reading the wrong document.
When a user logs in with the persistent login feature enabled, issue two cookies. The first is a very simple "remember me" cookie whose value can be anything you like, probably "yes", "true" or "1". This cookie must not be secure, and doesn't even need to be HTTP-only. The second cookie is the worker cookie. When the user logs in, generate a v4 UUID and store it in this second cookie. Store a hash of the UUID in your database. This UUID is essentially a password, so you want to store only the hash in case your database is stolen. Stored with the hashed UUID is the user identifier. By this I mean unique username or a user number. Expect a user to have more than one UUID - one per device - so don't put a unique constraint on the user id. Deliver this UUID as the cookie value unhashed, set to secure-only and HTTP-only. This way, the UUID will only ever be transmitted over SSL, keeping it safe from most forms of attack. Yes, a physically compromised machine is still vulnerable, but once a machine is compromised, all bets are off anyway.
Now it's time for the user to revisit the site. If they visit to a secure connection, great, you already have the UUID cookie available so validate it and update your session accordingly. If they visit over an insecure connection, that's what the first "remember me" cookie is for. When this cookie is presented over and insecure connection, while no user is logged in, the script should redirect to a secure script. This secure script will then have access to the UUID cookie which can validate and update the session. Once complete, redirect back to the original URL. Most users won't even notice it happening.
Before the first redirect from the insecure to the secure, you may want to store the form data in your session for retrieval after the login. In PHP, you can do something like
$_SESSION['STORED_GET'] = $_GET; store the get parameters. Then on the way back, check if the
$_SESSION array has the
STORED_GET key. If so, do the reverse:
$_GET = $_SESSION['STORED_GET']; and do an
unset($_SESSION['STORED_GET']); so the next requests won't do this. Of course, perform the same logic for
$_REQUEST... but not
Also, this advice applies to all persistent login systems: require a password at least once per session to change the password, view / change contact information, make purchases, etc. Make it so that even if the UUID is stolen, what the hacker has access to without a password is still pretty limited.
You may be tempted to bind a UUID to an IP address. Do not do this. Some users are behind corporate networks which send traffic out over multiple different connections at random. Using a GeoIP service to determine the country and binding a UUID to that is not a bad idea, but not necessarily a good idea either. I can't guarantee there aren't users who send their traffic out over multiple proxies spread around the planet.
Resist the urge to go overkill on hashing the UUID. Something like PBKDF2 is probably too much if you're requiring the user to enter a password before performing critical functions. If your database has been stolen, the hackers are after passwords - and as many of them as possible. Put your protection effort there. If your UUIDs are all compromised, it's not a huge deal, just invalidate them all and the stolen ones become useless. Yes, all your users will have to log in again, but that's not a big deal. For this reason, MD5 or SHA-1 is plenty.