65

Assuming you already have a website that implements all of the standard login stuff, what is the correct and most secure way to allow users to automatically be logged in for a certain time period (let's say 30 days)? This time period should be strictly enforced; ie, I don't think a valid solution would be giving a cookie an expiration date and hoping the user's browser deletes it in a timely manner.

Details of the existing login system:

  • A user must enter a user name and password
  • The database stores usernames as well as strong_hash_function(password + user_specific_random_salt)
  • Login forms are loaded and submitted over SSL, as are all subsequent pages

There are many examples out there and many with obvious security flaws. Let's use PHP as the target language but the concepts should be applicable to any language.

jlevis
  • 113
  • 2
colithium
  • 863
  • 1
  • 8
  • 10
  • possible duplicate of [Good session practices](http://security.stackexchange.com/questions/41/good-session-practices) – James T Nov 11 '10 at 22:33
  • 1
    of interest: [Is it safe to store the password hash in a cookie and use it for “remember-me” login?](http://security.stackexchange.com/questions/9455/is-it-safe-to-store-the-password-hash-in-a-cookie-and-use-it-for-remember-me-l/9456#9456) – Jacco Nov 30 '12 at 00:46

6 Answers6

34

My answer is incomplete and has a nontrivial flaw, in some situations - please see @Scott's answer instead.


There are two parts to my answer:

First, assuming your threat model does not worry about exposure of client side cookies, you can generate and store a nonce on the server side, hash that with the username and other info (e.g. client ip, computername, timestamp, similar stuff), and send that in the cookie. The nonce should be stored in the database, together with expiry date, and both checked when the cookie comes back.
This should be a "rememberance" cookie only, NOT a session cookie - i.e. when you get that cookie without a session, reissue a new, regular session cookie. Also note that like the session cookie, this should be delivered only over https, and using the secure and httpOnly attributes, and of course have the cookie scoped properly etc.

Second, for users that were remembered, you should (probably, depending on sensitivity) invoke a reauthentication process for certain sensitive operations. That is, sometimes they would need to put their password in again anyway, but seldom, and only for sensitive operations. This is to prevent attacks such as CSRF and shared desktop exposure.

AviD
  • 72,708
  • 22
  • 137
  • 218
  • 6
    See Amazon for an example of the second part! – ase Nov 19 '10 at 11:54
  • 1
    @adamse, nice, I usually refer to LinkedIn. Amazon even better :) – AviD Nov 19 '10 at 12:11
  • Only the hash of the nonce should be stored in the database. – Jacco Nov 30 '12 at 00:44
  • @Jacco you need to store the expiry date, so you know till when it is valid. Assuming you want it to expire after a limited time... – AviD Nov 30 '12 at 00:54
  • 1
    I meant to say that, because the nonce is a password equivalent, instead of the nonce, only the hash of the nonce should be stored in the database. Offcourse you also need to store things like expiration date and/or userId, etc. – Jacco Nov 30 '12 at 13:23
  • Ah, now I understand - but that's wrong, the nonce is not a password equivalent, it needs to be hashed *together* with user details. Otherwise you would need to store IP, computer name, etc on the server. – AviD Nov 30 '12 at 13:39
  • I think we both have a different approach. data such as computer name can be spoofed, IP addresses can change for many reasons. I create a 256bit random number, store the (hash of) this number in the database and set a cookie with the primary key & random number. Whomever presents the cookie is assumed to be the user associated with the DB record. (also see the answer I linked above). – Jacco Dec 02 '12 at 20:53
  • 2
    My answer is incomplete and has a nontrivial flaw, in some situations - please see @Scott's answer, http://security.stackexchange.com/a/109439/33 . – AviD Jan 18 '16 at 23:39
28

I've written at great length about secure logins and "remember me" checkboxes. The accepted answer isn't wrong, but I'd argue that it's a bit more complicated in one respect than it needs to be, and neglects an area where a little bit more complexity is needed.

generate and store a nonce on the server side, hash that with the username and other info (e.g. client ip, computername, timestamp, similar stuff), and send that in the cookie. The nonce should be stored in the database, together with expiry date, and both checked when the cookie comes back.

So an implementation that doesn't expose any information to the client might look like...

// Storage:
setcookie(
    'rememberme',
    hash_hmac('sha256', $username . $_SERVER['REMOTE_ADDR'], $storedNonce),
    time() + 8640000 // 100 days, for example
);
// Validation:
$valid = hash_equals(
    $_COOKIE['rememberme'],
    hash_hmac('sha256', $username . $_SERVER['REMOTE_ADDR'], $storedNonce)
);

The obvious limitation here is that, if your IP address (or other information) changes, your cookie is useless. Some might see this as a good thing, I see it as needlessly hostile towards usability for Tor users.

You can certainly interpret the above quote to mean something different, like a poor-man's JSON Web token, too. However, HTTP cookies are limited to 4 KiB per domain. Space is at a premium.

Another problem: How much information does your database query leak about your application? (Yes, I'm talking about side-channels.)


Paragon Initiative's Secure "Remember Me" Strategy

Storage:

  1. Generate a random 9 byte string from random_bytes() (PHP 7.0+ or via random_compat), base64 encode it to 12. This will be used for database lookups.
  2. Generate another random string, preferably at least 18 bytes long, and once again base64 encode it (to 24+). This will actually be used for authentication.
  3. Store $lookup and hash('sha256, $validator) in the database; of course associated with a specific user account.
  4. Store $lookup . $validator in the user's HTTP cookie (e.g. rememberme).

Validation (Automatic Login):

  1. Split the cookie into $lookup and $validator.
  2. Perform a database lookup based on $lookup; it's okay if there's a timing side-channel here.
  3. Check hash_equals($row['hashedValdator'], hash('sha256', $validator)).
  4. If step 3 returns TRUE, associate the current session with the appropriate user account.

Security Analysis:

  • What side-channels are mitigated?
    Most significantly: it mitigates the impact of timing information leaks on the string comparison operation used in the database lookup.

    If you implemented a naive random token authentication, an attacker could send a lot of requests until they find a valid authentication token for a user. (They probably wouldn't be able to select which victim they're impersonating.)

  • What if an attacker can leak the long-term authentication database table?
    We stored a SHA256 hash of the $validator in the database, but the plaintext for the hash is stored in the cookie. Since the input is a high entropy value, brute-force searching this is unlikely to yield any results. (This also mitigates the need for e.g. bcrypt.)

This strategy is implemented in Gatekeeper.

Scott Arciszewski
  • 855
  • 11
  • 28
  • 1
    Thanks @scott - it took me a while to grok the importance of the timing attack here, but now I see you are correct! Also agree on the other points for the most part, but I'm not sure that's the way I intended it anyway :-). But yeah, the timing attack does make a load of sense, and all the difference! – AviD Jan 18 '16 at 23:36
  • This is a great answer. Will adding a device fingerprint on top of this increase security (e.g., `HTTP_USER_AGENT`)? – IMB Nov 12 '18 at 18:07
  • 2
    I don't think it would help much. You're relying on the security of a random value for challenge-response authentication here. All a user agent check will do is make people get logged out every time their browser auto-updates, which would make them adverse to auto-updates, and keep them insecure. – Scott Arciszewski Nov 12 '18 at 18:49
  • Can you kindly explain why you need to use `base64_encode()` instead of a hash function like SHA*? – IMB Nov 19 '18 at 17:31
  • 9 random bytes -> base64 -> 12-character string; 9 random bytes -> SHA256 -> 64-character string – Scott Arciszewski Nov 19 '18 at 19:13
  • The purpose is just to achieve shorter storage length and not necessarily more security? – IMB Nov 19 '18 at 19:16
  • 1
    9 random bytes is sufficient for this use case, so there's no need to balloon it up to 64 hex characters. So, yes, but there is no security reduction here. 12 characters in the base64 (or base64url) alphabet is fine. – Scott Arciszewski Nov 19 '18 at 23:34
  • @ScottArciszewski Referring to step 4, what if step 3 returns false? I have it setup to delete the users sessions from the database but then you run the risk of someone dossing it. – A Friend Dec 12 '18 at 00:00
  • That's a needlessly expensive DoS attack vector to exploit. You're better off deleting it if a valid selector is found for an invalid prefix. – Scott Arciszewski Dec 12 '18 at 19:55
  • Can you elaborate more on the timing attack? I don't think `$Lookup` is necessary. Let's say the cookie looks like `rememberme=WBWgm2oMFxiR` and in the database, the cryptographic hash of the cookie's value is stored. I understand that database lookups with two dissimilar values will take less time to fail than two similar values. Let's say an attacker guessed a cookie value that its hash almost matches a row in the database. Modifying their guess in anyway will result in a totally different hash according to en.wikipedia.org/wiki/Cryptographic_hash_function – No_name Jun 23 '20 at 09:09
  • Thus, through the timing information, there's no way for an attacker to know they're incrementally getting closer to finding a valid authentication token? – No_name Jun 23 '20 at 10:45
  • "the cryptographic hash of the cookie's value is stored" -- Doesn't the attacker also know the hash of the value they send? – Scott Arciszewski Jul 06 '20 at 17:21
  • Yes, if they know the algorithm used to hash the token to store in the database. Is that an issue though? `$validator` in this answer is also stored plainly in the cookie, hashed in the database – No_name Jul 07 '20 at 01:49
  • Even though the attacker knows the hash of the value they are sending, an "almost match" determined by the timing information doesn't help them in any way to getting an exact match since hash operations aren't reversible no? – No_name Jul 07 '20 at 02:05
  • If it takes longer to return false, then they know the leftmost N bytes of H(garbage) = what's stored in the database, which is the same condition as a timing attack. Granted, this still requires computation work to find pieces that match more of the hash, but it reveals information about what's stored on the server. A little bit of separation here means **not having this leak at all**, which is a better security proposition because attackers learn nothing. – Scott Arciszewski Jul 17 '20 at 03:44
  • What benefit does splitting the token in to two parts have over checking the entire token hash in the database AND THEN performing a redundant timing safe hash comparison on two securely generated hashes created from random_bytes and throwing away the result? – jamieburchell Jul 23 '20 at 16:21
  • @ScottArciszewski That makes sense to me. If the attacker is able to figure out a correct hash in the database, they can figure out its value by bruteforcing offline VS if the hash cannot be derived, they're forced to bruteforce online which is easily detectable. Thanks! – No_name Jul 24 '20 at 19:24
  • @ScottArciszewski Ive just implemented your solution and it is working great! I'm a bit new to security so forgive me if this question is naive: what protection is there here if an attacker steals a user's cookie itself? WIth the cookie containing the plain text lookup and validator, couldn't they send that to the server to impersonate a user? – Wold Oct 21 '21 at 16:05
  • This provides no protection against cookie theft. I'm not aware of any schemes that successfully do. Make sure you're using HTTPS and set your cookies' flags correctly (Secure=True, HttpOnly=True). – Scott Arciszewski Jan 10 '22 at 16:42
5

That's an interesting question. I'll start off by saying that I'm not sure it can be done without some weakening of the security of the application, but then depending on the site that may be considered worth the trade-off.

So one approach could be

The session state database will need to record the time that a token is set and the duration that it should be remembered so that it can compare the token presented.

When the user logs in to the application they have a check-box which allows them to stay logged in (ideally with a range of time periods for the user to select)

When the session token is set, that time period is used as the cookie expiry. On subsequent visits the cookie will be presented to the application, and the server will need to check whether it is still valid (ie the expiry period for the remember me function hasn't been hit). If the token is still valid, the user is treated as authenticated, if not then the token is cleared and the user is redirected to the login screen.

Problems with this approach. Shared browsers would mean that the unauthorised access is possible.

Also anyone that can get access to the cookie (either through a vulnerability in the site, a browser issue or through malware planted on the machine) will be able to get unauthorised access to the site. One common suggestion for mitigating this is trying to tie the cookie to a source IP address (encrypted or stored on the server of course) which is checked when the user presents the cookie, however this causes problems with large corporate environments where a user may come from a number of IP addresses, and also may be susceptible to spoofing.

Rory McCune
  • 61,541
  • 14
  • 140
  • 221
4

You don't have to depend on the browser to delete the cookie, you have the site check the expiration date on the cookie.

In fact, for this to be secure, I'd say that's probably what you're going to have to do anyway, because you would want to set the expiration date as a part of the cookie value and encrypt it rather than rely on the plain-text expiration property of the cookie itself.

And you would really want to either mark the cookie as secure, and use a secondary cookie for the duration of an individual session, or protect the entire session (not just the login process) with SSL to prevent the cookie from being stolen off the wire and used by an unauthorized party.

Xander
  • 35,616
  • 27
  • 114
  • 141
0

This article describes an approach that defends against cookie theft and guessing of session IDs:

  • A "remember me" cookie consists of the user ID, a token (big random number) and a sequence token (another big random number).
  • When a user presents the cookie, the database is searched for these three pieces of information.
    • If found, the sequence token is regenerated, stored in the database and sent to the user
    • If only the username and the token are found, it is presumed that someone else has stolen the cookie because the sequence token was already used. The system can then warn the user and/or delete all stored tokens from this user from the database.

As an additional security measure it is advised that critical actions like changing the login data or paying for things request the password, if the user was only authenticated with the "remember me" cookie.

The database table that contains the user id and tokens also contains the expiration date of the tokens. It could also also contain the user agent and IP address so an attacker has to replicate these as well. All tokens should only be stored as hashes because they basically work like passwords and would enable an attacker, who obtained to database, to login.

I have created a sample implementation for PHP.

chiborg
  • 653
  • 1
  • 6
  • 12
0

A common approach would be to use a strong encryption algorithm to store a cookie containing the following information on the user's computer:

AES([Expiry date] + [random salt] + [Username] + [Password])

That would allow the user to invalidate the cookie (if it was compromised) by simply changing their password. Encrypting it allows the site operator to verify that it has not been tampered with (extending the expiry date, for example), as well as protecting the clear text password from compromise. Including the password means that an attacker who gains access to the AES key can still not "mint" tokens to authenticate as arbitrary users.

The downside is then that the site has to protect an AES key.

An alternative approach would simply be to add two fields to your user database, for a cookie value (long/strong/randomly generated value), and an associated expiry date. Any user presenting that cookie would be authenticated, up to the expiry date. One would just need to make sure on the server side that operations such as changing a password removes any "remember me" cookies stored in the database.

Advantages of this approach are that there is no encryption key to manage (or worry about rolling over), and that there is no possibility of the user's password being compromised. Disadvantages are that you need additional storage for your users.

On the further plus side, this approach allows you to provide different "remember me" tokens on different devices (a la Google Account manager), simply by storing the tokens in a separate table and allowing more than one entry per user. This then makes it possible to track where each is used from, and invalidate them individually.

Rogan Dawes
  • 445
  • 2
  • 4
  • 3
    This answer is not correct. Encrypting does not allow a site operator to verify that it has not been tampered with. This is a fundamental and basic confusion about the distinction between confidentiality vs authenticity/integrity. – D.W. Jan 08 '11 at 01:58
  • 3
    -1 You are proposing to transfer the end users password over the wire and store it on his harddisk? **No, don't do that,** it is absolutely wrong even though you use AES encryption. If an attacker can obtain a cookie, you're giving him unlimited attempts at obtaining the end users password in plaintext, i.e. getting of the most security-critical pieces of information. I assume you mean a hash of the password inside the AES (which isn't clear from your writing), and even that is not ideal. –  Apr 25 '11 at 21:17
  • @D.W. Encrypting DOES allow a site operator to verify that it has not been tampered with. If the decryption results in garbage, it has been tampered with. If it results in valid data, it has not been tampered with. – Rogan Dawes Aug 16 '11 at 18:42
  • @Jesper There is an insignificant difference between storing an AES encrypted version of the password, and a hashed version of the password. In both cases, the attacker can attempt to brute force a random AES key. Adding the additional step of then brute forcing the hashed password does not really add all that much, given how easy passwords are to brute force these days. At least a suitably long AES key should keep them busy for a while. – Rogan Dawes Aug 16 '11 at 18:46
  • 2
    @Rogan, [you are wrong](http://security.stackexchange.com/questions/2202/lessons-learned-and-misconceptions-regarding-encryption-and-cryptology/2206#2206). Using cryptography properly is more subtle than you may realize. – D.W. Aug 16 '11 at 18:54
  • Chosen ciphertext attacks. Enough said, really. – Scott Arciszewski Jan 01 '16 at 07:33
  • @JesperMortensen Out of curiosity, what drawbacks would you see if `remember-me-cookie = timestamp|user_id|signature` with timestamp = issue date, user_id = user internal id (number), signature = `base64(sign(hash('remember',timestamp,user_id,password)))`, password = user's bcrypted password retrieved from your DB (already salted and hashed), sign = Elliptic Curve Digital Signature Algorithm (or any other strong signature algorithm), hash = SHA-256. To validate the cookie: `verify(hash('remember',timestamp from cookie,user_id from cookie,password from DB), unbase64(signature))`. – Xavier Dury Apr 23 '19 at 11:02