Your password reset flow is probably broken

Three bugs that almost every hand-rolled reset flow has. Here's how to fix each.

· LoginWith team

Password reset is the most abused auth flow in every application. It’s also the one most teams test least, because the happy path is boring. Here are three bugs that are almost certainly in your reset flow right now.

Bug 1: Reset tokens that don’t expire fast enough

A typical reset flow generates a token, emails a link, and the token lives for 24 hours or more. That’s wrong. An email with a reset link can be:

  • Read by anyone with access to the mailbox (shared inbox, forgotten logout)
  • Indexed by Gmail’s search
  • Sitting in archive for months
  • Read by a malicious mail-forwarding rule

The window for legitimate use is minutes. Cap reset token lifetime at 30 minutes max, 15 is better. If a user doesn’t click the link in 15 minutes, they can request another. The minor friction is worth removing the long-tail attack surface.

Bug 2: Tokens that work after the password has been changed

If the user successfully resets their password, the reset token should be invalidated immediately — not just because it’s been used, but because a second use would let an attacker with a copy of the link set a new password again.

The fix is one line: mark the token as used=true in the same transaction that updates the password. On lookup, reject any used=true token even if it’s within its expiration window.

Bug 3: Account enumeration via the reset response

When a user enters an email that doesn’t exist in your system, most reset flows respond with “No account with that email.” That’s an account enumeration oracle — an attacker can probe your reset endpoint with a list of emails and learn which ones have accounts. That information feeds targeted phishing.

The fix: respond identically for existent and non-existent emails. “If an account exists with that email, we’ve sent a reset link.” Send the email when the account exists; do nothing when it doesn’t. Same response either way.

Bonus bug 4: Not invalidating existing sessions

When a user resets their password, it’s often because they lost control of the account. Every active session should be terminated — not just the one doing the reset. Delete all rows in the sessions table for that user ID. Most reset flows forget this, which leaves the attacker’s session intact.

Test the unhappy paths

These bugs show up when you test the unhappy paths: expired token, reused token, wrong email, concurrent sessions. Add those tests, run them in CI, and you’ll catch these regressions the next time someone edits the reset code.

Want auth that just works?

Get started with LoginWith