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:
- User id.
- Current timestamp. This is useful for knowing the age of the token.
- 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.
- 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.
<token> looks like this:
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:
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:
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.
timestamp can be extracted from the token to determine its age.
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.