Although the current trend of the security industry is funneling users towards password managing software (and two-factor authentication), they're not ubiquitous and inevitably some of your users will forget their passwords.
How can we design a system that allows users to recover access to their account, securely, without administrator intervention?
We've previously covered user authentication in detail, including [using PHP's built-in session management effectively](https://paragonie.com/blog/2015/04/fast-track-safe-and-secure-php-sessions) and [how to safely store your users' passwords](https://paragonie.com/blog/2016/02/how-safely-store-password-in-2016).
## Help, I Forgot "My" Password
This bears emphasis: **When you give your users the capability to reset their password, you are creating a backdoor into their account.**
This isn't merely a *theoretical* concern; one hacker [famously exploited a password reset process in Yahoo Mail to access Gov. Sarah Palin's email account](https://www.wired.com/2008/09/palin-e-mail-ha/) during the 2008 election cycle. If you're going to implement this capability for your users, it bears doing it right.
Of course, if you'd rather not incur the additional risk of a password reset feature being exploited, simply don't add it to your software. Handle these situations manually (but be on the watch for social engineering attacks) and you can safely ignore the rest of the advice on this page.
Doing it Wrong - Insecure Account Recovery
There are a large number of (sometimes imaginative) ways that any security feature can go wrong in the implementation phase. However, there are a few very common mistakes that are preventable in the design phase.
Don't Send Users Their Old Password
If you're taking security seriously [and properly utilizing a password hashing function](https://paragonie.com/blog/2016/02/how-safely-store-password-in-2016), you shouldn't even be able to do this. Any time a service provider emails you your old password this means they're doing one of two things:
1. Storing your password in a plaintext, which offers zero security if the server is compromised
2. [Encrypting your password](https://paragonie.com/blog/2015/08/you-wouldnt-base64-a-password-cryptography-decoded), which also offers zero security if the server is compromised as the server is necessarily capable of decryption to obtain your plaintext password
If you're already sending users their old password, go read the previously linked article and make sure you're protecting your users' secrets *first*, then come back to here to implement the recovery features.
Don't Send Users A New Password
Fortunately, most developers these days know they should use a password hashing function, so they aren't able to send users their old password. However, many do something that's dangerous in a different capacity: When a password reset request comes in, they randomly generate a new password and email it to the user.
It may not be immediately obvious why this is a bad idea, but there are a lot of things that can go wrong:
* [Email isn't reliably encrypted in transit](https://blog.filippo.io/the-sad-state-of-smtp-encryption/).
* Neither is SMS. In fact, [don't use SMS for any security features, ever](https://www.schneier.com/blog/archives/2016/08/nist_is_no_long.html).
* [Signal](https://play.google.com/store/apps/details?id=org.thoughtcrime.securesms) is fine (especially with identity key pinning), but there aren't yet any libraries capable of communicating with Signal users. You also can't count on your users to also use Signal.
* The email may not arrive (especially if a man-in-the-middle attack is employed), thus locking legitimate users out of their account.
* If you're using an insecure random number generator, your password generator will be predictable to attackers. This means a criminal can request a new password and, without any access to the user's email account or the network traffic, immediately know what their new password is.
* Expecting a new password to arrive by email is a bad user experience (at best) and instills bad habits and expectations in your users (at worst).
* If most users aren't prompted to change their password upon the next login, their password will be lingering in their inbox forever.
Later we'll contrast this with a better alternative: short-lived tokens, generated by a secure random number generator, with some caveats.
Security Questions are Security Snake-Oil
As demonstrated by the Sarah Palin email hack above, it should be clear that security questions aren't a good idea.
The answers to most security questions are like a second password, except they're often disclosed publicly on social media. Even when the answer isn't easily found on someone's Facebook profile or Wikipedia article (e.g. last 4 digits of your social security or credit card number), very few engineers would ever think to treat them like a password (i.e. with [password hashing](https://paragonie.com/blog/2016/02/how-safely-store-password-in-2016)), so they're often lying around databases in plaintext.
There might be some hypothetical corner cases where security questions stop attacks, but, by-and-large, they instead enable them.
Secure Password Resets
Now that we've identified the common pitfalls of password reset features, let's move onto implementing them as securely as possible.
Rule 1: Make it Optional
> ...and ideally, make it opt-in rather than opt-out (if reasonably practical).
It's important to always keep in mind that the ability to recover access to your account when you forget a password is a backdoor created as a trade-off between security and convenience.
Most of your users will appreciate the convenience, but many will feel more comfortable without the unnecessary risk, especially if they're already using a password manager (i.e. KeePassX) and therefore aren't prone to forget their password in the first place.
So, first and foremost, empower your users to choose convenience or security. Ask them, "Do you want to be able to regain access to your account if you ever forget your password?" If your application is designed for high risk users, consider defaulting to "No".
This is a win on two fronts: maximizing user freedom but being secure by default.
Even if you default to "Yes", which isn't as secure by default but reduces your likely tech support burden, you're still giving users the power to turn it off and allowing them to reduce their threat profile.
Note: This shouldn't inhibit administrator capabilities. It only opts them out of the usual automated process being discussed here.
Email Randomly Generated Split-Tokens
Many incumbent password reset implementations do this:
* Generate a random token, sometimes (but not always) from a cryptographically secure pseudo-random number generator (CSPRNG).
* Store the token in the database.
* Email the token (often in a URL for maximum convenience) to the user.
* When the user provides a token, do a database lookup based on the token provided.
* If a match is found and it isn't expired, authenticate as the associated user.
However, this raises a few hard questions:
1. Does the database query leak useful timing information about valid password reset tokens?
2. Would a read-only SQL injection vulnerability give an attacker access to the reset tokens?
For this reason, we prescribe a split-token solution, based on our design for [secure "remember me" cookies](https://paragonie.com/blog/2015/04/secure-authentication-php-with-long-term-persistence#title.2.1). The differences between what was described above and our split-token approach are:
* Always use a cryptographically secure pseudo-random number generator.
* Instead of a token, generate two random strings (or one really long random string then split it): A **selector** and a **verifier**.
* Example parameters: Generate 15 random bytes for the selector and 18 for the verifier, then [base64-encode both values (without side-channels)](https://paragonie.com/blog/2016/06/constant-time-encoding-boring-cryptography-rfc-4648-and-you).
* Email both strings to the user (as if it were one string).
* Store the selector in the database, along with a cryptographic hash (e.g. SHA256) of the verifier (rather than the verifier itself).
* When the user accesses the URL:
1. Do a database lookup based on the **selector** they provide. (This can be the first N bytes of the token provided.)
2. Recalculate the hash of the **verifier** the user provides, then compare it with the hash stored in the database. [In constant-time](https://secure.php.net/hash_equals).
3. If the hashes match, authenticate. If they don't, delete or disable this reset token. Do not allow even a second chance on the verifier.
This solves both problems:
1. There is no useful timing leak.
2. Even if you can dump the tokens via a read-only SQL injection vulnerability, you only have the SHA256 hash of the verifier to work with (rather than the verifier itself).
Encrypt the Outbound Emails with GnuPG
So far, the attacks discussed were only concerned with website security. Even if you implement everything perfectly, if your user's email account is compromised (or their email provider is, or their email provider's ISP is, etc.), all of your security unravels and your user's account on your service will soon follow.
Securing email is a challenging problem, because [STARTTLS doesn't provide robust transport-layer security for email](https://adamcaudill.com/2014/06/27/the-sinking-ship-of-email-security/). Until the Internet gets a significant upgrade (which will be years), the best you can do is to allow your users to provide their GnuPG Public Key, which your server will then use to encrypt the password reset emails.
Earlier this year, Paragon Initiative Enterprises published [GPG-Mailer](https://github.com/paragonie/gpg-mailer), an open source library that wraps Zend\Mail and Crypt_GPG to make sending and signing emails a breeze for PHP developers to implement.
While not every user can be expected to use GnuPG every day (and the ones that do use it would also likely use a password manager and therefore be unlikely to forget their passwords), it's important to make it *possible* for the password reset URL to arrive encrypted with their OpenPGP public key, especially if your users are security-conscious.
This code will encrypt an email such that only our security team can decrypt it:
<?php
use \ParagonIE\GPGMailer\GPGMailer;
use \Zend\Mail\Message;
use \Zend\Mail\Transport\Sendmail;
// First, create a Zend\Mail message as usual:
$message = new Message;
$message->addTo('security@paragonie.com', 'Naively Testing Your Example Code');
$message->setBody('Cleartext for now. Do not worry; this gets encrypted.');
// Instantiate GPGMailer:
$gpgMailer = new GPGMailer(
new Sendmail(),
['homedir' => '/homedir/containing/keyring']
);
// GPG public key for (fingerprint):
$fingerprint = '7F52D5C61D1255C731362E826B97A1C2826404DA';
// Finally:
$gpgMailer->send($message, $fingerprint);
Tokens Should Expire
* Every token should have a reasonable maximum lifespan (e.g. one hour), after which they can no longer be used.
* When a token is successfully used, it should immediately be expired (even if there's still time left).
* When a user's password is changed, all outstanding reset tokens should immediately be expired.
* Additionally, you should immediately invalidate all other active sessions (and, if applicable, "remember me" cookies) for that user.
It's completely unacceptable to leave a login-equivalent URL in a user's inbox until the end of time.
TL;DR
* DO:
* Make automated password resets optional, and opt-in
* Send a short-lived URL to their email address which contains a random token (generated by a CSPRNG)
* Use a split-token strategy to mitigate side-channels and database leaks
* Allow users to upload a PGP public key, and if it's available, use it encrypt the recovery URL
* DO NOT:
* Email the user their old password
* Change the user's password for them and email them the new one
* Rely on security questions/answers, which most attackers can guess for their targets
Finally, if this seems like too much information to digest, a secure account recovery system as described above has already been implemented in [CMS Airship](https://github.com/paragonie/airship).
We Consult and Develop
If you want help to shore up the security of the software you use or produce, [send us an email today](https://paragonie.com/contact). We can do everything from [auditing your source code](https://paragonie.com/security) to developing new features with security in mind.
Updates and Addendums
What About Read/Write SQL Injection Exploits?
The original version of this post prompted [a discussion](https://gist.github.com/paragonie-scott/d5fa1d432acc0342ba7c88686b0c9236) about how to mitigate the impact of an attacker with read/write access to the database (most likely via SQL injection). The solution that's appropriate for you depends how far down the rabbit hole you wish to venture.
Generally, if an sophisticated attacker can perform an SQL injection in your application, you can assume they are only a few milliseconds away from achieving a full system compromise (especially if both the webserver and database are on the same hardware). Consequently, concerns over the impact of SQL injection are best remedied by [preventing SQL injection in the first place](https://paragonie.com/blog/2015/05/preventing-sql-injection-in-php-applications-easy-and-definitive-guide).
If you'd like to add a layer of defense-in-depth to the split-token protocol, don't do anything overly complicated (e.g. [JSON Web Tokens](http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions)) or strange (e.g. adding encryption to an authentication protocol). Instead, use [secret-key authentication](https://paragonie.com/blog/2015/08/you-wouldnt-base64-a-password-cryptography-decoded) (i.e. HMAC-SHA256 instead of just SHA256).
If your database schema looks like this:
CREATE TABLE reset_tokens (
tokenid BIGSERIAL PRIMARY KEY,
selector TEXT,
verifier TEXT,
user BIGINT REFERENCES users (userid),
expires TIMESTAMP
);
...then the most straightforward attack to authenticate as another user is:
1. Issue a reset for an unprivileged user.
2. `UPDATE reset_tokens SET user = {$target} WHERE selector = {$knownToAttacker}`
3. Access the reset URL you received.
4. Now you're authenticated as your target.
To prevent this attack, you would need to make this change to your protocol (in both places):
- $hash = hash('sha256', $verifier);
+ $hash = hash_hmac(
+ 'sha256',
+ json_encode([
+ (int) $user['userid'],
+ (string) $verifier
+ ]),
+ $SOME_HMAC_KEY
+ );
In the example snippet, `$SOME_HMAC_KEY` implicitly refers to a string stored outside of both version control and the database. An attacker would know the selector and the verifier for their reset token, but the HMAC stored in the database would be tied to their user ID. If the attacker cannot recover this key, then they cannot just swap out user IDs at a whim.
However, even if you apply this mitigation, there's nothing preventing an attacker from just replacing one user's password hash with their own (unique constraints on the database column don't help here; simply change your own password then set the target's to your old password hash, for which you know the correct password).
A developer's natural reflex when confronted with this possibility might be to implement "peppering" into their authentication protocol (where the pepper is derived from the user's username and/or database record ID). Peppering your password hashes is [generally a bad move](http://blog.ircmaxell.com/2015/03/security-issue-combining-bcrypt-with.html).
But even if you apply that mitigation too, an attacker with read/write access to your database can already cause a lot of damage.
In the end, you'll likely find yourself with more and more code bloat (and more room for security-affecting bugs) in exchange for the possibility of making exploitation in the presence of a reliable vulnerability marginally more difficult.
If the time and energy being spent on pursuing "defense-in-depth" is not being spent on eliminating the SQL injection vulnerabilities in the first place, then this rabbit hole leads directly to security theater. Feel free to stop at hashing, or maybe HMAC, but venturing any further isn't worth the time or effort. Of course, [your threat model](https://adamcaudill.com/2016/07/20/threat-modeling-for-applications/) may vary.