83

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 user@example.com 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:

Stefan Falk
  • 1,047
  • 1
  • 9
  • 12
  • 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). – David says Reinstate Monica Mar 18 '16 at 15:48
  • 9
    @DavidGrinberg If privacy is a concern, OpenId is great and does all things. – wizzwizz4 Mar 18 '16 at 19:43
  • 2
    @wizzwizz4 but does it have a basic arithmetic plugin though? – wchargin Mar 18 '16 at 23:04
  • 2
    **A must read:** [The definitive guide to form-based website authentication](http://stackoverflow.com/q/549/1461424) – sampathsris Mar 19 '16 at 05:09
  • @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. – Stefan Falk May 26 '16 at 10:24
  • @displayname you are right, fb is a bad choice there, but it's not the only choice. Take for example the humble bundle site. They do have their own account service, but when it's time to pay they use a totally different service (forgot the name) – David says Reinstate Monica May 26 '16 at 11:22
  • @DavidGrinberg It's quite a mess. You never know when to use/buy a service or when to write it on your on. I didn't know that I can use an "account service" for my commercial website as well. There's a service for everything it seem :D – Stefan Falk May 26 '16 at 11:24
  • @displayname My general philosophy is that if I dont need to write it, I shouldn't. The more I write, the more I own, the more I have to maintain and pay for.... its a pain. That being said, you do still need to do due diligence and make sure the service you want to use fits your needs – David says Reinstate Monica May 26 '16 at 11:53
  • @DavidGrinberg I can only agree with you :) If I don't have to I don't want to make it myself since it is in general much more error prone .. – Stefan Falk May 26 '16 at 12:25

4 Answers4

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.

SilverlightFox
  • 33,698
  • 6
  • 69
  • 185
  • 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. – Mindwin Remember Monica Mar 18 '16 at 13:32
  • 9
    Care to expand on this - what risk will it present if the site logs the user in? – SilverlightFox Mar 18 '16 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. – sethmlarson Mar 18 '16 at 14:32
  • 3
    @oas Doesn't rotating the session identifier protect against this, as discussed in the linked answer? – SilverlightFox Mar 18 '16 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. – Todd Wilcox Mar 18 '16 at 16:39
  • Thank you! This is insanely helpful. I am impressed how many answers I got here! – Stefan Falk Mar 18 '16 at 18:15
  • 2
    Log in / log out also helps users utilize their browsers' auto-login abilities more easily while the user's memory of their new password is still fresh. – John Dvorak Mar 18 '16 at 23:52
  • *If* you already have a user registration form that discloses whether a certain username/email was registered, then definitely show it in the password reset form clearly whether the entered username/email was valid (nothing is worse UX then acting as if the reset email was send and the user not being able to find it). Do ensure of course that any such access points are listed in your rate limiter (e.g. in my current app 3 strongly rate limited API calls are allowed per minute). – David Mulder Mar 19 '16 at 00:01
  • And why would it be a bad thing to have an already used password reset token in the browsing history? Confusing the user with a manual entry form and multiple choices in the password reset email sounds like even more bad UX design. Especially as any MITM who would be able to intercept a GET request is also able to intercept a POST request if manually entered in a form. – David Mulder Mar 19 '16 at 00:04
  • @david: Yes, it's good to have user enumeration secured everywhere. Well the user may have followed the link but not yet reset their password. Another way is to expire the link on first visit, however some email security software and services automatically visit links to test for malware or redirections to known attacker sites. – SilverlightFox Mar 19 '16 at 08:29
  • @silverlightfox: So we are talking about users who 1) decide to use the form 2) decide to open the page, but do not reset the password 3) are attacked by someone reading their history by a person (as any software can just get the mail directly). And all this at the cost of a fluent UX. I mean, if you started a forgotten password flow you probably want to reset it straight away in virtually all cases. – David Mulder Mar 19 '16 at 11:32
  • Either way, been thinking and one useful addition would be to add a link 'this was not me' which will invalidate the token and show whether the token was used. If it was used reset the password to the original password, inform the user of the issue and provide an option to put the account in lockdown for a certain number of days whilst the user can figure out who or what has access to his email. – David Mulder Mar 19 '16 at 11:35
  • 2
    @dav Yes, a small risk but one that can still be mitigated, which is why it was included in my answer. At the end of the day it is up to the site owner to define their "risk appetite". Security is always a balance of minimising risk whilst not stifling usability and convenience. What's acceptable for one system, is wholly unacceptable for another. – SilverlightFox Mar 19 '16 at 12:59
  • @dav Also remember that referrer leakage may pass the query string to a system not as secure as yours. These can inadvertently be introduced if the site template is updated to include external references which is also used by your reset page. Redirecting to another page storing the token in session or passing it via the POST verb can mitigate this. – SilverlightFox Mar 19 '16 at 13:18
  • 2
    @Mindwin NO! Indeed, you must invalidate all current sessions for the user, but there is absolutely no reason to direct them to the login page. Instead, create a new session as if they had just logged in, so that they don't have to type in their password 3 times. – Shelvacu Mar 19 '16 at 20:48
  • Insisting on matching session identifiers is really annoying. Some users do not have their secure email accounts directly accessible on the system they normally do web-based logins (and likely the reset request) from, and some users are doing the reset from within a "porn mode" (incognito/private-browsing/etc.) browser window because they're resetting a secondary account (and another account is persistently logged in through the non-porn-mode browser profile). – R.. GitHub STOP HELPING ICE Mar 20 '16 at 03:17
  • Since I am currently on it: What if a user has registered and tries to login before she validates her email? Is it okay to reveal that this email still needs validation or should one just reveal "Email not found" or "Invalid email address"? – Stefan Falk May 26 '16 at 10:22
  • @displayname: You may want to [combine signup and forgotten password functionality](http://security.stackexchange.com/a/47748/8340) so that they have to validate their email in order to signup in the first place and to provide their initial password. – SilverlightFox May 26 '16 at 10:33
  • @displayname: To answer your question. I would say this would be fine, but the message should only be displayed upon successful validation of the password. This then poses no user enumeration risk. If no password had been set yet then user enumeration here would be less of a risk as any attacker would only be able to enumerate registered but non-confirmed accounts. If you want to mitigate this risk, then you could display your standard username/password is invalid message in the page, but then email them the token again as a reminder to say that they need to confirm their account before login. – SilverlightFox May 26 '16 at 10:34
  • @SilverlightFox Alright, thank you very much for your help! :) – Stefan Falk May 26 '16 at 10:57
  • The user enumeration link is dead – Ella Rose Jul 25 '16 at 00:28
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: 's.....@o....com');
  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.
S.L. Barth
  • 5,504
  • 8
  • 39
  • 47
Jacco
  • 7,512
  • 4
  • 32
  • 53
  • 2
    This answer deserves more upvotes! – Stefan Falk May 21 '16 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 '17 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 '17 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.

Tobi Nary
  • 14,352
  • 8
  • 44
  • 58
  • 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? :) – Stefan Falk Mar 18 '16 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 '16 at 11:16
  • 1
    Note that UUIDs are a bad choice for this - see [GUIDs are designed to be unique and not random](https://blogs.msdn.microsoft.com/oldnewthing/20120523-00/?p=7553). Better to generate a 128 bit token using a CSPRNG. – SilverlightFox Mar 18 '16 at 11:40
  • @SilverlightFox, thanks. Shame on me. I did edit my answer. – Tobi Nary Mar 18 '16 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 :) – Stefan Falk Mar 18 '16 at 18:14
  • @SilverlightFox A v4 UUID is random. – Damian Yerrick Mar 19 '16 at 05:09
  • @dam Yes random, bit not necessarily generated from a CSPRNG, which means that they could be predictable. Some implementations do actually use a CSPRNG, however it's best not to have secure code to be at the whim of an implementation that could change at any time in its future development. Might as well write your own code to explicitly call the CSPRNG. – SilverlightFox Mar 19 '16 at 08:34
  • @SilverlightFox: I never understood why they don't use a random number generator to generate UUIDs. Wouldn't that ensure uniqueness? – user541686 Mar 19 '16 at 22:34
  • @Mehrdad, because CSPRNGs are computationally more expensive to use and predictability is of no concern when you only need to asure uniqueness. – Tobi Nary Mar 19 '16 at 22:37
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.

Anders
  • 65,052
  • 24
  • 180
  • 218
  • 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: http://brian.pontarelli.com/2006/05/02/is-your-browser-requesting-a-page-twice/ – Patrick M Mar 20 '16 at 05: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 '16 at 05: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 '17 at 10:56