18

I searched for a long time, and never really find any articles or posts that fully explain how a password reset token should be created.

What I know so far:

  • It should be random.
  • Its hash should be stored in the database.
  • It should expire after some time.

What I'm not too sure about:

  1. Should I use a cryptographic method to create the random token? Is Node's crypto.randomBytes enough?
  2. Should I use a cryptographic method (e.g., bcrypt) to hash the token?
  3. Should I include certain information (i.e., expiry time, UUID, etc.) in the token? If yes, is JWT a good way?

I imagine the whole process would look something like this:

  1. Create a random token.
  2. Hash the token and store the hash in the database.
  3. Send the token in plain text to the client.
  4. Once it is used, or if it is not used after a certain time limit, delete it from the database.
  5. When a token in used, check if its hash matches any from the database.
  6. If matched, allow user to reset password. Otherwise, disallow the process.
yqlim
  • 282
  • 1
  • 2
  • 7
  • I've been using MD5(UUID_SHORT()) for tokens. Short and simple. But not sure if this is secure enough – theking2 Oct 18 '22 at 16:17

4 Answers4

13

Password reset tokens aren't very much like passwords. They're short-lived, single-use, and - most relevantly here - machine generated and non-memorable.

  1. You absolutely must use a cryptographically secure random number generator to produce the token. crypto.randomBytes is good enough, yes. Make sure the token is long enough; something on the order of 16 bytes (128 bits) should work.
  2. Because the "preimage" (the value fed into the hash function, in this case the token) is random, there's no need to use a slow hash. Slow hashes are for brute-force protection, because passwords suck and usually don't take long (in machine terms) to guess; 128-bit random values are not brute-forceable for any practical meaning of the term. Just do a single round of SHA-256 or similar.
  3. Bad idea; it just complicates things. Store all that stuff in the DB instead. Yes, you could do it statelessly by putting it in a JWT, but the DB makes much more sense:
    • You'll need to interact with the DB anyhow, to reset the password.
    • You'll need to bypass any caching anyhow; even if you have a distributed system they'll all need to see this change so there's no point making it stateless.
    • You only need to do one DB read to verify the token (both that its value is correct and that it hasn't expired yet, which can be done in a single query), which is a relatively rare event; it's not like needing to do a session token lookup on every request.
    • JWTs have expiry but there's no convenient way to make them single-use (you have to store state saying "these JWTs are no longer valid" on the server until each one expires, which kind of misses the point of JWTs.) Password reset tokens stored in the DB can (and should) be made single-use by deleting them upon verification.
    • You can have a cleanup task that goes through and periodically removes expired tokens (that were never used), if you want. They don't take up that much space in the DB, though (a hash digest and a timestamp on each user, if you only want one per user valid at a time, or a hash digest, a timestamp, and a foreign key into the Users table if you want to allow multiple tokens to be valid at once for a user; both have advantages and disadvantages).
    • JWTs have more potential vulnerabilities (somebody steals your signing key, somebody discovers that the key was generated insecurely and hasn't been rotated since the bug was fixed, your JWT library is vulnerable to key or algorithm confusion, etc.) compared to just checking the DB (which is vulnerable to basically nothing except SQLi, I guess, which hopefully you know how to avoid).

Your basic process makes sense.

CBHacking
  • 42,359
  • 3
  • 76
  • 107
  • >JWTs have more potential vulnerabilities (somebody steals your signing key - Wouldn't the attacker be able just to generate access token in such case, without going through the password reset flow? – Andriy Kharchuk Feb 15 '21 at 06:35
  • @MooseontheLoose If the access token was also a JWT, and specifically if it used the same signing key, then yes they would. Neither of those conditions were specified in the suggestion to use JWTs, though. (Of course, it'd be odd to use JWTs at all yet *not* use them for access tokens, but maybe they already have some system for those and don't want to change it?) – CBHacking Feb 15 '21 at 07:28
  • Why is a hash of the token required? Isn't something like `crypto.randomBytes(16).toString('hex')` good enough? – Erik André Feb 17 '23 at 10:18
  • 1
    @ErikAndré The reset token has to be stored in the DB, generally speaking. You could maybe get away with an in-memory cache but usually it'll go into persistent storage. In that case - and _especially_ if the validity period is more than a few minutes - it's best to hash the random output so that even if somebody has a read-only access to your users table (one-time or persistent), they can't reset the password for other users (and gain control of their accounts). Also prevents timing attacks for linear-time brute-forcing. Minor threats, but cheap to fix. – CBHacking Feb 18 '23 at 00:25
4

1. Yes, you should use a cryptographic method to generate the token. Always use cryptographic randomness whenever you're doing anything that's related to security. Yes, crypto.randomBytes is fine. Use 16 bytes (you could get away with a little less, but don't take risks unless the token will absolutely need to be typed rather than just clicked on or copy-pasted). (16 bytes translates to 24 characters of Base64 or 32 hexadecimal digits.)

2. Hash the token with a cryptographic hash such as SHA-256 or SHA-512. You don't need a password hash here: password hashes such as bcrypt are for when the input is a password that a human remembers, and are useless when the input is a randomly generated string of sufficient less. A password hash is a bit harder to use and a lot slower, and you don't need one here.

3. I don't see any advantage in adding information in the token. Information such as the expiry date and what the token is for needs to be in the database anyway.

Gilles 'SO- stop being evil'
  • 51,415
  • 13
  • 121
  • 180
1

Other two answers seem to express a sort of FUD regarding using HMAC signed token for password resetting.

If the concern, as stated in other answers, is about getting your singing keys stolen from the server, then the attacker already has access to much more than just signing keys, at which point this argument is moot.

Saving token data in the database is not a better (or worse) solution than using signed tokens. Both are reasonable approaches and equally secure. Let this answer be the flip side of the coin.

Django web framework uses this approach. It's one of the most used web frameworks out there, so we can rest assured that this approach is pretty secure.

Django generates a token using these parameters:

  1. User id.
  2. Current timestamp. This is useful for knowing the age of the token.
  3. Hash of current password. If a user generates many reset tokens and resets their password using one token, the hash will change in the database but all the tokens will still have an old hash, and so all the tokens will be automatically invalidated.
  4. Timestamp of last login. If a user generates a reset token, but later logs into their account, this timestamp will update in the database but the token will still have an old timestamp, and so the token will be automatically invalidated.

The final <token> looks like this: <timestamp-hash>.


Unlike a JWT, the final token doesn't have any user related info in plain text. Just a timestamp and a hash.

So, if there's no data sent with the token, how will you know which user this token belongs to?

There are two ways to associate a token with a user.

First: You can send the user's id with the token, such as: <user_id>:<token>.

Now, when the user clicks the link, you can read the user id from the token, re-calculate the hash using the earlier parameters and compare this hash[See note below] with the issued token.

If they match, then you allow the user to reset the password.

Second: The way Django handles this is it doesn't send the user id with the token but in the reset url. So a reset url generate by Django looks like this: /reset-password/<user_id>/<token>.

When the user clicks the link, you can read the user id from the url and then re-calculate the hash using the earlier parameters and compare this hash[See note below] with the issued token.

The timestamp can be extracted from the token to determine its age.


Important Note:

Please don't compare hashes like you compare strings (i.e. using equality operators). Comparing like this is susceptible to timing attack.

In Python, there's secrets.compare_digest function to take care of this. Please find an equivalent in your language to compare hashes.

xyres
  • 113
  • 5
  • Point 3 is nice because the JWT is automatically invalidated. But is it a good idea to expose password hash like that? – yqlim Jul 18 '21 at 07:45
  • 1
    @yqlim Only the signed hash is sent in the link without other details (so it's not exactly a JWT where you also have the token data in plain text). You can, however, send the user id with the token so that you can later use that id to create a token to compare it with the issued token. Django only sends the token but adds the user's id in the url like: `/reset-password///`. I've updated the answer with more details. – xyres Jul 18 '21 at 09:37
  • for point 2, > This is useful for knowing the age of the token. There's no way to use this information since it's passed through the hash right? What is the purpose of including this info? – 1mike12 Oct 30 '22 at 21:17
  • @1mike12 My bad. The reset token contains the timestamp and a hash separated by a hypen: ``. Later, Django splits the token at the hyphen (`-`) to separate the timestamp and the hash. – xyres Oct 30 '22 at 21:46
-1

For a database (mariadb) I've been using MD5(UUID_SHORT()) to create reset tokens.

Storing a timestamp to remove stale tokens in a cron.

This of course needs manual, scripted obsolescence of issued tokens.

Love to hear if I'm slightly out of my mind

theking2
  • 147
  • 7
  • [UUID is not cryptographically secure](https://security.stackexchange.com/q/890/200484), so it is not suitable for use cases where security is important, such as password reset token. [MD5 is not collision resistant](https://crypto.stackexchange.com/q/1434/101576), and therefore not secure as well. So it seems to me that your implementation is a bad idea. – yqlim Oct 19 '22 at 16:32
  • Thanks @yqlim .In my defense I inherited as system that stored passwords verbatim in database. But on your advice I might move to bin2hex(random_bytes(n)) and in order to answer OP question prepend this with some sort of date-time string. – theking2 Oct 20 '22 at 07:26