Skip to content
Innovialabs

How to Build a Secure Password Reset and Account Recovery Flow

Practice4 min

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-referrer so 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

ChannelMain risksPractical advice
Email linkCompromised mailbox, link forwarded or leakedShort expiry, single use, notify on change
SMS codeSIM swap, number recycling, interceptionShort numeric codes need strict attempt limits; avoid SMS as the only factor for high-value accounts
Support deskSocial engineeringWritten 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.

FAQ

It is safer not to. After setting the new password, ask the user to log in normally, or at least require the second factor if 2FA is enabled.

We can help with this

If you'd rather get the result than figure it all out yourself — this is what we do every day.

Discuss a project

Read next

Have an idea or a task? Tell us about it

We'll reply within one business day and propose a solution. The consultation is free.