There are a few requirements for a good password reset 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:
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
):
kahing1
kahing1
, the attacker now has a password reset token for their own account:kahing1 14834409451 hash(kahing11483440945XXX)
kahing 114834409451 hash(kahing11483440945XXX)
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)