Php generate password reset token

The earlier version of the accepted answer (md5(uniqid(mt_rand(), true))) is insecure and only offers about 2^60 possible outputs -- well within the range of a brute force search in about a week's time for a low-budget attacker:

  • mt_rand() is predictable (and only adds up to 31 bits of entropy)
  • uniqid() only adds up to 29 bits of entropy
  • md5() doesn't add entropy, it just mixes it deterministically

Since a 56-bit DES key can be brute-forced in about 24 hours, and an average case would have about 59 bits of entropy, we can calculate 2^59 / 2^56 = about 8 days. Depending on how this token verification is implemented, it might be possible to practically leak timing information and infer the first N bytes of a valid reset token.

Since the question is about "best practices" and opens with...

I want to generate identifier for forgot password

...we can infer that this token has implicit security requirements. And when you add security requirements to a random number generator, the best practice is to always use a cryptographically secure pseudorandom number generator (abbreviated CSPRNG).


Using a CSPRNG

In PHP 7, you can use bin2hex(random_bytes($n)) (where $n is an integer larger than 15).

In PHP 5, you can use random_compat to expose the same API.

Alternatively, bin2hex(mcrypt_create_iv($n, MCRYPT_DEV_URANDOM)) if you have ext/mcrypt installed. Another good one-liner is bin2hex(openssl_random_pseudo_bytes($n)).

Separating the Lookup from the Validator

Pulling from my previous work on secure "remember me" cookies in PHP, the only effective way to mitigate the aforementioned timing leak (typically introduced by the database query) is to separate the lookup from the validation.

If your table looks like this (MySQL)...

CREATE TABLE account_recovery (
    id INTEGER(11) UNSIGNED NOT NULL AUTO_INCREMENT 
    userid INTEGER(11) UNSIGNED NOT NULL,
    token CHAR(64),
    expires DATETIME,
    PRIMARY KEY(id)
);

... you need to add one more column, selector, like so:

CREATE TABLE account_recovery (
    id INTEGER(11) UNSIGNED NOT NULL AUTO_INCREMENT 
    userid INTEGER(11) UNSIGNED NOT NULL,
    selector CHAR(16),
    token CHAR(64),
    expires DATETIME,
    PRIMARY KEY(id),
    KEY(selector)
);

Use a CSPRNG When a password reset token is issued, send both values to the user, store the selector and a SHA-256 hash of the random token in the database. Use the selector to grab the hash and User ID, calculate the SHA-256 hash of the token the user provides with the one stored in the database using hash_equals().

Example Code

Generating a reset token in PHP 7 (or 5.6 with random_compat) with PDO:

$selector = bin2hex(random_bytes(8));
$token = random_bytes(32);

$urlToEmail = 'http://example.com/reset.php?'.http_build_query([
    'selector' => $selector,
    'validator' => bin2hex($token)
]);

$expires = new DateTime('NOW');
$expires->add(new DateInterval('PT01H')); // 1 hour

$stmt = $pdo->prepare("INSERT INTO account_recovery (userid, selector, token, expires) VALUES (:userid, :selector, :token, :expires);");
$stmt->execute([
    'userid' => $userId, // define this elsewhere!
    'selector' => $selector,
    'token' => hash('sha256', $token),
    'expires' => $expires->format('Y-m-d\TH:i:s')
]);

Verifying the user-provided reset token:

$stmt = $pdo->prepare("SELECT * FROM account_recovery WHERE selector = ? AND expires >= NOW()");
$stmt->execute([$selector]);
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (!empty($results)) {
    $calc = hash('sha256', hex2bin($validator));
    if (hash_equals($calc, $results[0]['token'])) {
        // The reset token is valid. Authenticate the user.
    }
    // Remove the token from the DB regardless of success or failure.
}

These code snippets are not complete solutions (I eschewed the input validation and framework integrations), but they should serve as an example of what to do.

Php generate password reset token

Foreword

This article aims to introduce PHP developers to the thought process of properly implementing security features. We’ve all been novice PHP developers at one point in our lives and thought that if there is an md5() in it, then it must be secure.

The primary motivation behind this article is to build upon the efforts of the PHP community to make security best practices more visible amongst a sea of old and insecure advice. When I first wrote this out of frustration in June 2017, 9 out of the top 10 results in a Google search for “password recovery php” were insecure implementations. And, 3+ years later, those same results are still there. That’s the advice we, as a community, are giving to all newcomers to the language. That has to change, so this is my humble attempt at trying to help.

Security advice on the internet (and especially PHP security advice) does not age well. If you are reading this a few years from now, then take it with a grain of salt, though the thought process behind it should still apply.

With that out of the way, let’s get to it.

How (not) to implement a password recovery mechanism in PHP

If you’ve been on the internet for any amount of time, you probably have used the password recovery functionality of some site. Standard practice is to ask the user for their email address (which you asked for when they registered in the site), and send an email to that address with a link. That link contains some particular information that lets the application know which user is the password recovery being done for. It then asks for a new password, and we are all set.

If you are thinking “Why don’t I just email them the password they already have?”, it means you are not hashing them in the first place. Go and do that before you continue reading. No joke. PHP provides a secure way to implement password hashing and verification, and the defaults are reasonable. Further reading material can be found here.

You might be thinking “Hey, even I know that you shouldn’t do that!”, but at least a couple of the examples in the top results of that Google search store the password in the database without hashing. So, it would seem that it bears repeating and brings us to our first don’t.

Don’t 1: Don’t store the plaintext passwords in the database.

Is your site hashing your passwords before saving them to the db? Good, let’s move forward.

Going back to emailing the users a link so they can reset their password. Let’s assume that your site’s password recovery url is:

https://your.site/password_recovery.php

A trivial solution (seen, again, the top Google results) might be to do something like:

https://your.site/password_recovery.php?user=

This is not secure. Why? Because if an attacker knows the email of someone, he can change his password. This is our second don’t.

Don’t 2: Don’t use public information as a password recovery token

You might be thinking: “What other information I have from the user that I can use? The user_id maybe?”

https://your.site/password_recovery.php?user_id=13

This is also not secure. Why? While an attacker likely can’t know which user they are changing the password of, he can lock people out of their accounts. Also, if your site has some sort of admin functionality, then user_id=1 will likely be an admin user, and now you’ve gotten yourself into a world of pain.

Don’t 3: Don’t use sequential id numbers as password recovery tokens.

At this point, you probably figured out you can’t use anything out of the database directly. But in an attempt to work around that you think: “Let’s use md5, that should prevent an attacker from guessing the user right?”. So you try something like:

$token = md5($user["email"]);

https://your.site/password_recovery.php?token=$token

You can even look it up directly in the database by using the md5 function in mysql!

This, too, is not secure. Why? Because if I can guess the way the token is generated, eg: by looking at my token (or you’ve published the code in github, or, say, I figured out which of the top 10 google implementations you copy and pasted) then I can generate the token for everyone. As the great Claude Shannon said: “one ought to design systems under the assumption that the enemy will immediately gain full familiarity with them”.

Don’t 4: Don’t make your security depend on the fact that your code is secret. It won’t be.

There are some dishonorable mentions. From two of the top google results:

$token = md5(18247*2567 + $user["id"]);
$salt = "SOME!BIG#RANDOM@STRING1337"
$token = md5($salt.$user["email"])

Both fall into Don’t 4. If I know how your hash is built, I can create recovery tokens for all your users. Even if you are sure Don’t 4 doesn’t apply to you, DON’T USE THESE. They are vulnerable to other attacks that go outside the scope of this article, but feel free to break them yourself.

As an extra note, these all fail the “disgruntled (ex) coworker attack”, i.e.: your coworker, or yourself, can create the tokens for any user you want without interacting with the system. This is not a nice property to have in this kind of system.

Don’t 5: Don’t generate tokens in a way that can also be generated offline by someone with knowledge of the system

“But wait!”, you may think. “What if we use encryption instead of hashing? I can keep the key out of version control and we should prevent the issue from above right?”

While you can do:

$token = encrypt($user["email"],$some_key);

This brings more problems than it solves.

  1. You need to find a way to handle the key securely.
  2. If the key gets compromised, and you want to change it, you will break previous tokens.
  3. Depending on the algorithm and key size, the key might be recoverable, helped by the fact that the plaintext and algorithm are known (See Don’t 4).
  4. If the key is recovered (either by bruteforce or because of another weakness somewhere) then an attacker (or your coworker, see Don’t 5) can generate reset tokens for any user offline.

Don’t 6: Don’t use encryption if you can avoid it. It causes more problems that it solves. And you probably don’t know how to implement it securely anyway

At this point there are probably no more ways to use the data that you already have to generate a reset token. Instead, you’ll have to generate it from some other data and store it somewhere, like a new column in the user table. That does fix the problem of valid tokens being generated offline.

Do 1: Generate tokens that don’t depend on the user data.

From here on out, we are shifting the attack model. Up until now, the attacker was guessing the token associated with a particular user. That is not possible anymore. All the attacker can do is trigger a password reset for a user, and guess what token was generated.

“So how would I go generating this token?”, you ask yourself. Maybe use something PHP already gives us, like uniqid:

php > echo uniqid();
593aceadf16aa

That should be a random, unique id right???

Php generate password reset token

The output of uniqid is generated exclusively from the current server time. The attacker controls when he requests the password change, so even if the resolution it uses is up to microseconds, he can probably narrow the range down to a couple milliseconds, making the id guessable in a couple thousand tries

Don’t 7: Don’t generate your tokens based on time, they are guessable.

Note: Hashing a guessable token doesn’t add any security. It sure as heck looks more secure, but it isn’t. It’s like when guessing someone’s password, you don’t care about what the hash of their password is, if you can just guess that the user is lazy and their password is literally “password”.

At this point, you, being a smart guy/gal, might be saying: “Why would I use uniqid if I wanted something random. I’ll just use the random number functions of PHP!”

$token = rand(); // Value between 0 and 2147483647.

The thing with (many) random number generators is that they are deterministic. If you know several outputs, you can actually know what the next one will be. Yes, you read that right. No, I’m not crazy. If you don’t believe me go read this and this. Don’t forget your tinfoil hat when you return.

Both rand() and mt_rand() are vulnerable to this kind of attack. The less known lcg_value is as well. PHP 7 introduced random_bytes() and random_int(), which return random data in a way that is secure for this kind of applications, i.e. they are Cryptographically Secure Random Number Generators.

Don’t 8: Don’t use rand, mt_rand or lcg_value as a random number source for anything security related.

Do 2: Use random_int or random_bytes for secure random numbers.

So how do you build a random AND unpredictable token?

A simple way is:

$length = 16; // Adjust length to fit your new paranoia level. 16 is probably a sane default and the same length as md5 (if you are migrating from a method that uses it)
$token = bin2hex(random_bytes($length)); // bin2hex output is url safe.

That’s it, we are done. We have a secure token to use for password recovery.

Now you can go ahead and implement the rest of the functionality. There are a couple of extra things you need to keep in mind when you do though.

Reset tokens shouldn’t be valid forever, so you should add a new column next to the token, with the creation date and use that to check if it should be accepted or not. Also, they should be single use tokens. So once the user resets their password, delete the token.

Do 3: Set a lifetime for your reset tokens, the shorter the better. 1hr is probably a sensible default.

Do 4: Discard the reset tokens after use.

Wrapping up / TL;DR

Bringing it all together, your general application flow should look something like this:

1) The user requests a password reset, providing their email

2) Look up the user in the database using the email address

3) Securely create a token, and store it in the database together with its creation time.

eg:

$token = bin2hex(random_bytes(16));

4) Send an email with a link to your password recovery page, and the token as a query string parameter

5) Lookup the user in the database using the token, if found, and not expired, prompt him for a new password

6) Store the new password in the database

7) Delete the used token from the database.

Remember, Don’ts:

1) Don’t store the plaintext passwords in the database.

2) Don’t use public information as a password recovery token.

3) Don’t use sequential id numbers as password recovery tokens.

4) Don’t make your security depend on the fact that your code is secret.

5) Don’t generate tokens in a way that can also be generated offline

6) Don’t use encryption

7) Don’t generate your tokens based on time

8) Don’t use rand, mt_rand or lcg_value as a random number source for anything security related

Remember, Do:

1) Generate tokens that don’t depend on the user data and store them in the database.

2) Use random_int or random_bytes for secure random numbers.

3) Set a lifetime for your reset tokens.

4) Discard the reset tokens after use.