14

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.

3 Answers 3

11

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.

2
  • >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? Feb 15, 2021 at 6: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, 2021 at 7:28
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.

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.

Unlike a JWT, the final token doesn't have any of these parameters in plain text. Only the token's hexdigest is sent out.

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>:<the_token_value>.

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.


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.

2
  • 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, 2021 at 7: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/<user_id>/<token>/. I've updated the answer with more details.
    – xyres
    Jul 18, 2021 at 9:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.