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:
- 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.
- 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.
- Store
$lookup
and hash('sha256, $validator)
in the database; of course associated with a specific user account.
- Store
$lookup . $validator
in the user's HTTP cookie (e.g. rememberme
).
Validation (Automatic Login):
- Split the cookie into
$lookup
and $validator
.
- Perform a database lookup based on
$lookup
; it's okay if there's a timing side-channel here.
- Check
hash_equals($row['hashedValdator'], hash('sha256', $validator))
.
- 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.