This post is part of the series 'Password management'. Be sure to check out the rest of the blog posts of the series!
- How to store a password in a web application?
- How to implement Password reset feature in a web application? (this post)
- How to store a password on Windows?
- How to prompt for a password on Windows?
- Automatically log in a user on a website using the Credential Management API? (coming soon)
- Helping users to create good passwords (coming soon)
- How to avoid storing secrets in the source code? (coming soon)
Users are humans, so they have memory problems like everyone. Thus, they happen to forget their password from time to time. The purpose of this article is to show how to give them back access to your application. If it's not clear, the post is about what to do when a user clicks on the submit button on this page:
If you are considering this solution, you are not storing passwords in the right way. Indeed, you should not be able to do that. I strongly advise you to read my previous post about storing password and come back here later.
You have probably seen websites asking to enter a question and the associated answer when registering. I don't like it because often questions are not personal enough, and anyone can guess the answer. Sometimes, just going on the Facebook profile can give you the answer. You could look how Sarah Palin's Yahoo account was hacked. In the end, the response to the question should be like a password to be safe, so the snake is biting its tail.
In short, this solution is not very practical and its result is really not guaranteed.
This solution consists in replacing the current password with a new random password.
1st problem: There is no guarantee that it is the user of the account who makes the request. The reset password form simply asks for a username or an email address. You can easily enter the email address of someone else. Therefore, it is possible to prevent someone from logging in by resetting his/her password regularly. To solve this problem we can think of keeping the 2 valid passwords ⇒ it is therefore 2 times simpler to brute force the account then…
2nd problem: The password is clearly visible in the mailbox. Some people do not protect their computers or cell phones with a password. In this case, you can simply get the new password and log in as using it. This can be easily countered by forcing the user to change his/her password upon the first login with the new password. Thus the password present in the email will not be valid after the reset (provided you have a security policy preventing reuse of old passwords). Also, the generated password has no expiration date. When you reset your password, you are expected to do it right now. So, the generated password should not be valid more than 10 minutes. This means someone must be able to read your emails in the next 10 minutes to be able to access your account.
By combining the ideas mentioned above we can create a solution that consists in creating a token that can be used once on the reset password page. The website sends a link similar to
https://example.com/PasswordLost?token=467dc4ad9acf4 by email. When the user navigates to the reset page, the site checks that the token is valid and allows the user to change their passwords.
This solution is similar to the previous one in the sense that we will generate a token which will then be sent by email to the user. The fundamental difference is that this token does not replace the current password, does not match the new password and is long enough to not be brute force. After entering this token in a screen provided for this purpose the user will be forced to change their passwords. To add more security it is possible to limit the validity of the token in time: about ten minutes is largely sufficient. Also, the token must be valid only once.
There are many ways to generate the token. You can generate a random string and store it in a database with the associated email address and the expiration date of the token. Then, you can validate it by querying the database. The other solution that I prefer, is to generate a token that is encrypted by the server. Then, you can decrypt it and validate the data it contains (user email, expiration date and last password changed date). This way you don't have to store anything on the server. This is what ASP.NET Core Identity does.
You now have a reset url, but what you should you include in the email you send?
- Relevant and readable subject and "From" name
- The reset url
- Expiration information for the link (10 minutes? 1 hour?)
- Who requested the reset? (IP Address? Operation system? Browser name?)
- Support contact information
You can check the template made by Postmark: Password reset template
When you send an email with the link to the reset password page, you have to create the absolute URL (i.e. the combination of the domain and the relative path). It's very common to use the domain of the request to create the full URL using for instance
HttpContext.Request.Host. This is very convenient as you don't have to hardcode the host in a configuration file or something similar. However, if your server is not well configured it may listen to the requests from any host, not only those you own. So, a malicious user can send a reset password request with an email address of someone else and using a Host that correspond to one of their websites. In this case, if the user clicks on the link in the email, they will go directly to the website of the pirate instead of your website. This is a way to do phishing.
The malicious user just has to set a custom
Host header when sending the request to your website, so there are no complicated things here. You'll find the documentation of the Host header on MDN.
✓ Be sure to configure your server to only listen on the domains you own, so you know the
Host header has a valid value
✓ Don't use the value of the
Host header, directly or using a property of the framework you use, to compute the full URL. Instead, use a value from a configuration file or hardcode the value.
✓ Never trust user inputs, whatever the input is! And, in this case, it's an http header. Be sure to always validate and sanitize the user inputs.
When the user enter their email address on the reset password page you should not indicate whether there is an account associated to this email address or not. As a user of a website, you don't want anyone to be able to know whether you are using it or not. The confidentiality is more important for some websites than for others. Imagine that your husband/wife could know that you are using a dating service… Instead you can just show a generic message:
You can send an email to inform someone a user account wasn't found. You'll want to let the recipient know that the request was made as well as whether they should be concerned. If they did make the request and the email address or username wasn't found, then they'll need advice to try a different email address. You can use this template for this kind of emails.