There are a few requirements for a good password reset token:
- user should be able to reset their password with the token they receive from in an email
- the token should not be guessable
- the token should expire
- user should not be able to re-use token
Ideally, the web framework of your choice should already have a built-in way to generate reset tokens. However, we use Play and it does not provide a way to do that, so we have to roll our own.
There are no shortage of answers on Stack Overflow about generating password reset tokens, however all of them suggest adding a reset token to the database. Updating the database for an unauthenticated request irks me, so I decided to implement stateless password reset tokens.
In principle, a stateless password reset token is very similar to a stateless session cookie. All the state information is stored in the token itself, and it’s combined with a secret so it’s not possible to generate the token without also knowing the secret. The devil is in the detail.
The naive way to create the token would look something like:
user + " " + expiration time + " " + hash(user + expiration time + secret)
To verify the token, all we need to do is:
- parse out user and expiration time
- check that the token is not expired
- compute the hash and make sure that it matches
Unfortunately, in addition to not satisfying requirement #4 from above, this would also allow a malicious attacker to reset anyone’s password. For example, my username is kahing
, and if an attacker wants to reset my password, what he needs to do is (suppose secret is XXX
):
- sign up for a new account as
kahing1
- reset password for
kahing1
, the attacker now has a password reset token for their own account:kahing1 14834409451 hash(kahing11483440945XXX)
- attacker resets my password with token:
kahing 114834409451 hash(kahing11483440945XXX)
- this token would validate and has an expiration time in the year 2333
This is called a cryptographic splicing attack. WordPress suffered from it at one point so don’t feel too bad if you didn’t spot it yourself.
In addition to adding a delimiter to the hashed portion, there’s another way to fix this problem and also bring our token to satisfy requirement #4: we can add a per-user secret and hash that as well. So the new token becomes:
user + " " + expiration time + " " + hash(user + " " + expiration time + " " + user secret + " " + application secret)
Every time the user resets their password, we generate a new user secret, which would effectively invalidate all the prior tokens. Also, kahing1
will not be able to generate a valid token for kahing
anymore.
It is possible to safely generate a stateless password reset token, just as it is possible to safely generate a stateless session cookie. It helps to read up on best practices and related vulnerabilities for the latter (such as Session cookies for web applications).
Happy Hacking!
(We use SHA256 as the hash in case anyone is curious)