84

For my project I need a "forgot password" functionality.

I am not quite sure how to implement this kind of functionality yet so I was hoping to find some "best practice" on the internet but couldn't find anything useful that treats every important aspect of this quite common feature.


My own thoughts about this are rather straight forward:

If a user wanted to change his password I could create a unique token uniqueTokenForTheUser attached to a "change password request" which would be essentially the identifier for my backend to tell if it was the right user who sent the request.

So for example I'd generate/send an email to [email protected] with a link

http://www.example.com/changePassword?token=uniqueTokenForTheUser

uniqueTokenForTheUser would be stored in a table that gets checked by a thread in a certain interval and removes the record once the token is expired in case the user did not actually change his password.

Although this sounds rather straightforward to me, I wanted to double-check if there were any "best practices" out there e.g. when it comes to generation and deletion of that particular token.

Any suggestions or references to articles/tutorials are welcome!


Additional links suggested by comments:

9
  • 14
    Bit of a left-field option, but you may want to consider just not handling username/password account info and instead shipping that off to a third party (ie how you can log into Stack Exchange via Google/FB/etc). Mar 18, 2016 at 15:48
  • 9
    @DavidGrinberg If privacy is a concern, OpenId is great and does all things.
    – wizzwizz4
    Mar 18, 2016 at 19:43
  • 2
    @wizzwizz4 but does it have a basic arithmetic plugin though?
    – wchargin
    Mar 18, 2016 at 23:04
  • 2
  • @DavidGrinberg In my case I am developing a service which requires payment. I am not sure if people would like to login with their facebook account for services they are paying for but I might be completely wrong here. May 26, 2016 at 10:24

4 Answers 4

77

Use HTTPS only for this, and then we'll get onto details of implementation.

First of all you're going to want to protect against user enumeration.

That is, don't reveal in the page whether the user exists or not. This should only be disclosed in the email itself.

Secondly you will want to avoid referrer leakage. Ensure no external links or external resources are present on your target link. One way to mitigate this is to redirect after the initial link is followed so that the token is no longer in the query string. Be aware that if something goes wrong (e.g. your database is briefly down), and a standard error template is shown, then this may cause the token to be leaked should it contain any external references.

Generate a token with 128bits of entropy with a CSPRNG. Store this server-side using SHA-2 to prevent any data leakage vulnerabilities on your reset table from allowing an attacker to reset passwords. Note that salt is not needed. Expire these tokens after a short time (e.g. a few hours).

As well as providing the non-hashed token to the user in the email link, give the option for the user to navigate to the page manually and then paste in the token value. This can prevent query string values from being logged in browser history, and within any proxy and server logs by default.

Optionally you may want to make sure that the session identifier for the session that initiated the password reset is the one that followed the link. This can help protect an account where the attacker has access to the user's email (e.g. setting up a quick forward all rule with limited access to the email account). However, all this really does is prevent an attacker from opportunistically following a user requested reset - the attacker could still request their own reset token for the victim should they want to target your particular site.

Once the password has been reset to one of the user's choice, you will need to expire the token and send the user an email to let them know this has happened just in case an attacker has somehow managed to reset their password (without necessarily having control of their mail account) - defence in depth. You should also rotate the current session identifier and expire any others for this user account. This negates the need of the user having to log in again, whilst also clearing any sessions ridden by an attacker, although some users like to have the site log them out so they get a comfort blanket of confirmation that their password has actually been reset. Redirecting to the login page also gives some password managers the chance to save the login URL and the new password, although many already detect the new password from the reset page.

You may also wish to consider other out of band options for the reset mechanism and the change notifications. For example, by SMS or mobile phone alerts.

21
  • 14
    I'd add: Do not log in the user right after password change. Invalidate all sessions and route him to the login page. Several sites do this, and is a minor vulnerability. Mar 18, 2016 at 13:32
  • 9
    Care to expand on this - what risk will it present if the site logs the user in? Mar 18, 2016 at 13:53
  • 13
    @SilverlightFox If someone has discovered your password and is logged into your account and the site doesn't invalidate the session tokens then the fraudulently logged in user won't be booted off their session. By invalidating all sessions, this ensures that all other users using your account must re-enter the login credentials to gain access. Mar 18, 2016 at 14:32
  • 3
    @oas Doesn't rotating the session identifier protect against this, as discussed in the linked answer? Mar 18, 2016 at 14:54
  • 14
    From a UX standpoint, when I have to reset a password I like having to log on again right away to "cement" the password in my mind and know for certain that it has been changed and I know what it is changed to. Mar 18, 2016 at 16:39
18

Your line of thinking is on the right track. However, I would suggest describing the flow for your forgotten password functionality from one step earlier. Somebody claims to have forgotten their password, you need to make sure you identify that this person is indeed the owner of the account for which you will start the password recovery procedure.

The flow below assumes a service where people can sign themselves up, such as a website (in other words, a list of registered emails and/or usernames can be built using the sign up functionality).

step one: identify the user

  1. Upon clicking the 'Oops, I forgot my password' link, present the user with a form where they enter their username and email address (optionally give them a hint about the email address in the form of: '[email protected]');
  2. inform them if they email address they entered belongs to the username they entered (yes or no);
  3. If the entered username and password match, continue with next steps.

step two: start recovery procedure

  1. generate a random recovery token (128 bits should be fine);
  2. store a hash of this token (created using a password hashing algorithm), along with its creation timestamp in your database;
  3. send the user an email with a link (containing the recovery token) to a change password form. The URL for the change password form should be HTTPS.
  4. when you receive the change password request, you check: [1]. the validity of the token (using its creation time compared to the current time, typically a 10 minute time window should be enough); [2]. if the token presented is correct (hash the token, compare the resulting hash to the one in your database, just as you would for a password);
  5. if all checks pass, continue with the next step.

Step three: update their password

  1. destroy all 'remember me' information you may have stored for this user;
  2. destroy any and all active sessions associated with this user;
  3. update the user's password;
  4. optionally send an email to their email address notifying them of the change;
  5. log the user in.
3
  • 2
    This answer deserves more upvotes! May 21, 2016 at 11:44
  • 1
    I think your step 1 would leave you vulnerable to email enumeration, which you definitely don’t want. Although it seems user friendly to tell them that the email doesn’t match, it is an attractor for scripts and bots which want to narrow down a billion leaked email addresses to ones which might be usable on your site.
    – Alex White
    Dec 12, 2017 at 10:43
  • 1
    @AlexWhite, the answer is written from the assumption that the users can sign themselves up (as indicated in the answer). Given this, email enumeration is possible by misusing the sign-up functionality (see answer). With this constraint in mind, the given scheme is sufficiently protected.
    – Jacco
    Dec 12, 2017 at 12:19
7

You are close to the best practice with your approach. A small addition would be:

  • You may require for the user name to match the token rather than just deducting the user from the token.

  • You should rather use https than http.

The other questions:

  • As to the generation: A usual way of generating those is using UUIDs, but as comments point out, you should rather use a completely random, sufficiently long string.

  • As to the deletion: You may clean up old, unused tokens regularly as you suggested. Maybe use a time frame that allows the mail get through usual email graylisting, e.g. 15 minutes.

9
  • Hi! Thank you for your answer! HTTPS:// is a good point as well as matching the token by the username. After reading your answer realized that the token could actually have a type as well. For example CHANGE_PW and REGISTER. That way I could use one table for different email verification tasks. Does that make sense? :) Mar 18, 2016 at 11:13
  • Sure. Increasing the available 'space' - or decreasing the valid tokens within always makes sense. But keep in mind that when using UUIDs, you have a vast space, so this might not be worth the hassle to implement.
    – Tobi Nary
    Mar 18, 2016 at 11:16
  • 1
    Note that UUIDs are a bad choice for this - see GUIDs are designed to be unique and not random. Better to generate a 128 bit token using a CSPRNG. Mar 18, 2016 at 11:40
  • @SilverlightFox, thanks. Shame on me. I did edit my answer.
    – Tobi Nary
    Mar 18, 2016 at 11:42
  • Alright, thank you! Based on the number of upvotes I guess I'll have to accept SilverlightFox's but +1 from my side too :) Mar 18, 2016 at 18:14
4

Some thoughts on this:

  • Email is inherently insecure. The token could be stolen anywhere between your server and the users inbox by an attacker. Consider using another medium of communication if possible, such as SMS.
  • If you go by email, the restore link should be over HTTPS, not HTTP. At least that part of the process can be encrypted.
  • To prevent user enumeration attacks, you migh want to display the same message wheater or not the username/email adress the user enters exists or not. (At least do not make the mistake of displaying the email after the user has entered only the username.)
  • Use a good pseudo-random number generator and a long token. Nobody should be able to guess the token. To make token guessing harder, require that the user enters both the username and the token. Limiting the number of attempts (per username and/or IP) will also prevent brute-forcing. Perhaps also a CAPTCHA?
  • You want to limit for how long a token is valid. Email might be slow if it get stuck in filters, but I can see no reason why anyone would need more than say half an hour.
  • Once an account is restored, end all old sessions for that account. The user might want to restore the account because it has been compromised, and then the attacker should be logged out.
  • Only send a one use token, and not a new password! Passwords should never be sent in email.
  • Only allow a token to be used once! Delete it immediately after it is used (even if the user does not actually change the password).

For more reading on the topic, I recommend Troy Hunt.

3
  • Allowing the user to only load the page once is good in theory, but can break things for some users. A GET request (which is what your link will be) should be idempotent, meaning that when called a second time it should give the same thing. This is important when dealing with a not very well written cache, or other problems: brian.pontarelli.com/2006/05/02/…
    – Patrick M
    Mar 20, 2016 at 5:40
  • Woops. I meant a badly written proxy, not cache. Though, a user with a internet connection which is dropping packets might have the same problem.
    – Patrick M
    Mar 20, 2016 at 5:50
  • SMS is also horribly insecure! If email security is a concern then you would be better with using one of the Authenticator apps for 2FA. In all seriousness though, if someone’s email is compromised it is game over for them.
    – Alex White
    Dec 12, 2017 at 10:56

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .