People are forgetful, and most still don’t use a nice password manager. This means that every application handling user accounts needs some means of password recovery. Password recovery is usually handled in a couple of ways: (sorted by stupidness descending)
- Send the user their old password.
- This is NOT ACCEPTABLE.
- If you do this, please learn about password hashing and sort it out today.
- Send the user a new generated password.
- This is a bit better, but still NOT ACCEPTABLE.
- Now access to your app is as secure as their e-mail account.
- Can be acceptable if you FORCE the user to change their password on the first login.
- Send the user a password recovery link.
- This is the state-of-the-art for password recovery.
- Can be done by sending a magic link, or a link to a password recovery form.
The first two exist because people are inherently lazy. Why spend more time on something that can be done quickly? The #2 does not require any 2-way interaction between the user and the system. The system just sends a password and the process is over. With #1, the user needs to click a link, you have to check its validity and to do that you need to handle state. And as we know, state is the root of all evil.
Traditional Password Recovery Process
The diagram below models the usual process for password recovery. The process begins with the user sending a recovery request form with their e-mail. If a user with that e-mail exists in the system, a special token is generated for their account. The token is usually a long random string of characters which can not be guessed. They recieve a password recovery link in their e-mail inbox which contains the token. When they click that link and enter a new password the server receives the new password and the security token.
The server now needs to check if it really generated that code or is the user trying to do something naughty. Since the server stored the recovery token in the database, it just looks it up to check its validity. That database entry also has to contain a reference to the user account that the token belongs to. This way we know for which user we need to update the password without giving the user a chance to lie to us and say they are changing a password for someone else.
Creating another database relation just to handle password recovery? Nah, I’m too lazy for that. Not only is it gonna take away time now but it will be hard to reuse in the future. If I want to reuse it on another project I need to create a new relation and then set-up more logic to make it work with the user system for that project.
We can do better!
Cryptography Based Password Recovery
For a password recovery system to be secure we want the token to have the following properties:
- Expirable
- Single-use
- Counterfeit-resistant
Along with those, as I mentioned before, I want the token to be verifiable without storage. I want to check all of the above without having to manage state and store tokens in a persistent layer.
Inspired by how JWT handles user authentication, I designed a process which handles password recovery in a similar fashion.
Verifiable Without Storage
To have the ability to check the validity of the token without storing it in our database we will use cryptography. By encrypting the following JSON object with our private key and sending it to the user to use as a token we achieved just that.
{ "tokenType": "PasswordReset", "userId": 4 }
The user can not create or edit their own reset token without a private key we own. We know that the token is valid and which user it represents just by decrypting it.
Expirable
The above token has no mechanism to expire it after let’s say 30 minutes. This token will allow the user to change their password forever. This is not good – now they have a permanent link that allows anyone who gets it to take over their account and no way to destroy it.
{ "tokenType": "PasswordReset", "userId": 4, "createdAt": 1531568977 }
This can be easily fixed by adding a Unix timestamp of when the token was created. Now we have full control over the expiration of the token. We just define a time interval after a which the token is no longer acceptable in out password change logic.
Single-use
The above token can be reused infinite amount of times until it expires. This is not a huge deal if the expire time is short, but we can fix it easily as well. Our password recovery logic just needs to check if the user’s password has been changed since the token has been generated. If it has, we do not allow new changes.
We can check that by using an updated field on the user entity or by using a hash of the user’s password in our token which we can compare with the current password hash of the user.
Counterfeit-resistant
This we already have by using an industry standard encryption algorithm. No-one will be able to fake tokens unless they have access to our private key.