101

I want to generate identifier for forgot password . I read i can do it by using timestamp with mt_rand(), but some people are saying that time stamp might not be unique every time. So i am bit of confused here. Can i do it with using time stamp with this ?

Question
What's best practice to generate random/unique tokens of custom length?

I know there are lot of questions asked around here but i am getting more confused after reading different opinion from the different people.

Scott Arciszewski
  • 33,610
  • 16
  • 89
  • 206
keen
  • 3,001
  • 4
  • 34
  • 59
  • @AlmaDoMundo: A computer can't divide time unlimited. – juergen d Sep 20 '13 at 07:15
  • @juergend - sorry, do not get that. – Alma Do Sep 20 '13 at 07:15
  • You will get the same timestamp if you call it for instance a nano second apart. Some time functions for instance can only return time in 100ns steps, some only in seconds step. – juergen d Sep 20 '13 at 07:17
  • @juergend ah, that. Yes. I mentioned 'classic' timestamp with seconds only. But if act like you've said - yes (that only leaves us an option with time machine to get non-unique timestamp) – Alma Do Sep 20 '13 at 07:18
  • tried to generate 2 tokens in a second. – keen Sep 20 '13 at 08:35
  • 1
    Head's up, the accepted answer does not leverage a [CSPRNG](https://paragonie.com/blog/2015/07/how-safely-generate-random-strings-and-integers-in-php). – Scott Arciszewski Jul 14 '15 at 22:47
  • Does this answer your question? [PHP: How to generate a random, unique, alphanumeric string for use in a secret link?](https://stackoverflow.com/questions/1846202/php-how-to-generate-a-random-unique-alphanumeric-string-for-use-in-a-secret-l) – Farhat Aziz Oct 22 '20 at 14:14

5 Answers5

157

In PHP, use random_bytes(). Reason: your are seeking the way to get a password reminder token, and, if it is a one-time login credentials, then you actually have a data to protect (which is - whole user account)

So, the code will be as follows:

//$length = 78 etc
$token = bin2hex(random_bytes($length));

Update: previous versions of this answer was referring to uniqid() and that is incorrect if there is a matter of security and not only uniqueness. uniqid() is essentially just microtime() with some encoding. There are simple ways to get accurate predictions of the microtime() on your server. An attacker can issue a password reset request and then try through a couple of likely tokens. This is also possible if more_entropy is used, as the additional entropy is similarly weak. Thanks to @NikiC and @ScottArciszewski for pointing this out.

For more details see

Community
  • 1
  • 1
Alma Do
  • 37,009
  • 9
  • 76
  • 105
  • 21
    Note that [`random_bytes()`](http://php.net/manual/en/function.random-bytes.php) is only available as of PHP7. For older versions, the answer by @yesitsme seems to be the best option. – Gerald Schneider Jul 26 '15 at 13:36
  • 3
    @GeraldSchneider or [random_compat](https://github.com/paragonie/random_compat), which is the polyfill for these features that has received the most peer review ;) – Scott Arciszewski Aug 13 '15 at 15:36
  • 1
    I made a varchar(64) field in my sql database to store this token. I set $length to 64, but the string returned is 128 characters long. How can I get a string with a fixed size (here, 64 then) ? – gordie Feb 22 '16 at 19:12
  • 3
    @gordie Set the length to 32, each byte is 2 hex characters – JohnHoulderUK Jun 21 '16 at 10:05
  • What should be `$length` ? The id of user? Or what? – stack Jun 24 '16 at 14:41
  • @stack $length=20 should be way more than enough :) (that's about `1461501637330902918203684832716283019655932542976` different possible tokens. i could even argue that $length=10; should suffice. 10 will give you `1208925819614629174706176` possible tokens; btw the math is `2**(10*8)`, and you can do some guesswork-difficulty calculations with https://www.grc.com/haystack.htm ) – hanshenrik Jan 30 '21 at 01:51
73

This answers the 'best random' request:

Adi's answer1 from Security.StackExchange has a solution for this:

Make sure you have OpenSSL support, and you'll never go wrong with this one-liner

$token = bin2hex(openssl_random_pseudo_bytes(16));

1. Adi, Mon Nov 12 2018, Celeritas, "Generating an unguessable token for confirmation e-mails", Sep 20 '13 at 7:06, https://security.stackexchange.com/a/40314/

Community
  • 1
  • 1
YesItsMe
  • 1,709
  • 16
  • 32
  • 26
    `openssl_random_pseudo_bytes($length)` - support: PHP 5 >= 5.3.0 , .......................................................... (For PHP 7 and up, use `random_bytes($length)`) .......................................... (For PHP below 5.3 - don't use PHP below 5.3) – jave.web Jan 03 '16 at 22:46
58

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:

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.

Scott Arciszewski
  • 33,610
  • 16
  • 89
  • 206
  • When you verify the user-provided reset token, why do you use the binary representation of the random token? Do you think it will be possible (and secure?) to: 1) store in DB the hashed hex value of the token with `hash('sha256', bin2hex($token))`, 2) verify with `if (hash_equals(hash('sha256', $validator), $results[0]['token'])) {...`? Thanks! – Guicara Dec 18 '15 at 16:25
  • Yes, comparing hex strings is secure too. It's really a matter of preference. I prefer to do all crypto operations on raw binary and only ever convert to hex/base64 for transmission or storage. – Scott Arciszewski Dec 18 '15 at 18:56
  • Hi Scott, it is basically a question not only for your answer, but for the entire article about the "Remember Me" feature. Why not to use the unique `id` as the selector? I mean, the primary key of the `account_recovery` table. We don't need additional layer of security for the selector, do we? Thanks! – Andre Polykanine May 28 '17 at 21:31
  • `id:secret` is OK. `selector:secret` is OK. `secret` itself is not. The goal is to separate the database query (which is timing-leaky) from the authentication protocol (which should be constant time). – Scott Arciszewski May 29 '17 at 05:50
  • Is there any harm in using `openssl_random_pseudo_bytes` instead `random_bytes` if is running PHP 5.6? Also, shouldn't you append just the selector and not the validator in the querystring of the link? – greg Jul 11 '18 at 14:09
  • [OpenSSL's RNG is harmful](https://github.com/ramsey/uuid/issues/80). Use `paragonie/random_compat` instead for PHP 5.6. – Scott Arciszewski Jul 18 '18 at 14:21
  • @ScottArciszewski thank you for this answer! It's an eye opener. – Serge P Jan 05 '23 at 22:07
7

You can also use DEV_RANDOM, where 128 = 1/2 the generated token length. Code below generates 256 token.

$token = bin2hex(mcrypt_create_iv(128, MCRYPT_DEV_RANDOM));
Graham T
  • 968
  • 9
  • 14
2

This may be helpful whenever you need a very very random token

<?php
   echo mb_strtoupper(strval(bin2hex(openssl_random_pseudo_bytes(16))));
?>
Asclepius
  • 57,944
  • 17
  • 167
  • 143
Ir Calif
  • 460
  • 6
  • 7