How to Build a Secure Password Reset and Account Recovery Flow
Practice4 minInnovialabs editorial team
In short
Send a random, single-use, short-lived token stored only as a hash, give the same response whether or not the account exists, rate-limit requests, end all sessions after the reset and never let recovery silently bypass 2FA.
The short answer
A secure reset flow has five properties:
- Unpredictable token generated by a cryptographically secure random generator.
- Short expiry and single use: the link works once and only for a limited time.
- No user enumeration: the response looks the same whether the email exists or not.
- Rate limits on both requesting and using reset links.
- Clean-up after reset: other sessions and tokens are revoked and the user is notified.
Account recovery is an alternative way to log in. If it is weaker than your login, attackers will use it instead of the login.
Step 1: generate and store the token
Use at least 32 random bytes from a secure source, never a timestamp, user ID or Math.random(). Store only a hash of the token, so a leaked database does not hand out working reset links.
import crypto from "node:crypto";
const token = crypto.randomBytes(32).toString("base64url");
const tokenHash = crypto.createHash("sha256").update(token).digest("hex");
// save: user_id, token_hash, expires_at, used_at = null
// send the plain token only inside the email link
A plain SHA-256 is fine here because the token is long and random; slow password hashes are needed only for human-chosen passwords.
Step 2: expiry and single use
- Set a short lifetime, typically minutes to an hour, not days.
- Mark the token as used atomically, so two parallel requests cannot both succeed:
UPDATE password_resets
SET used_at = now()
WHERE token_hash = $1 AND used_at IS NULL AND expires_at > now()
RETURNING user_id;
- When a new reset is requested, invalidate older unused tokens for that user.
- Put the token in the URL path or query of your own domain, and on the reset page set
Referrer-Policy: no-referrerso the token does not leak to third-party scripts or analytics.
Step 3: prevent user enumeration
The "Forgot password" form must not reveal which emails are registered:
- Always show the same message: "If an account exists for this address, we have sent instructions."
- Keep response time similar in both cases. Sending the email in a background job helps.
- Check that the registration form and the login error messages do not leak the same information in another way.
Step 4: rate limits
Limit by email/account and by IP address:
- requests for reset emails, so nobody can flood a user's inbox or your email budget;
- attempts to submit tokens or SMS codes, so short codes cannot be brute-forced;
- add a CAPTCHA or delay only after suspicious activity, not for every user.
Step 5: what happens after the reset
- Hash the new password with a password hashing function (Argon2id, bcrypt or scrypt).
- Invalidate all sessions and refresh tokens, except possibly the current one. If the account was hijacked, this is what kicks the attacker out.
- Revoke remaining reset tokens.
- Send a notification to the account email: "Your password was changed. If this wasn't you, contact support."
- Do not log the user in automatically if the account has 2FA; ask for the second factor first.
Email vs SMS recovery
| Channel | Main risks | Practical advice |
|---|---|---|
| Email link | Compromised mailbox, link forwarded or leaked | Short expiry, single use, notify on change |
| SMS code | SIM swap, number recycling, interception | Short numeric codes need strict attempt limits; avoid SMS as the only factor for high-value accounts |
| Support desk | Social engineering | Written verification procedure, no exceptions "because it's urgent" |
Changing the recovery email or phone is itself a sensitive action: require the current password or 2FA and notify the old address.
How recovery interacts with 2FA
A password reset must not disable 2FA. Otherwise anyone with access to the mailbox gets full access to the account. Typical approach:
- reset changes only the password; login still requires the second factor;
- if the user lost the second factor, they use backup codes issued at 2FA setup;
- without backup codes, use a slower manual process: identity checks, a waiting period and notifications to all known contacts before 2FA is removed.
Common mistakes
- Tokens that never expire or can be reused.
- Storing tokens in plain text next to the user record.
- Different messages for "email not found" and "email sent".
- Keeping old sessions alive after the password change.
- Security questions like "mother's maiden name" — the answers are often public or guessable.