# how to do secure password reset flows

## TL;DR
A reset flow is a second login path, so harden it like one: random single-use tokens with a short expiry, identical responses whether the account exists or not, rate limits on every step, and full session invalidation when the password changes. Notify the account holder that a reset happened. Most reset bugs leak account existence or leave old sessions alive.

```text
how to do secure password reset flows
```

## Use this when
- You are building or reviewing a forgot-password feature
- A reset token showed up in a log, URL, or email thread and you need the handling rules
- An audit asks about user enumeration via the reset form
- An agent is checking reset token generation and storage
- Users report reset links that never expire or work twice

## Not for this skill when
- You are writing password complexity rules (separate topic)
- You are setting up MFA or recovery codes
- The flow is OAuth-based account linking rather than password reset
- You are handling a suspected account takeover in progress (incident response)

## Steps

### 1. Generate the token with a cryptographic RNG
Use the platform's secure random generator with at least 128 bits of entropy. Never derive reset tokens from user IDs, timestamps, or hashes of predictable data.

```bash
python3 -c "import secrets; print(secrets.token_urlsafe(32))"
```

Expected: tokens are unguessable and unique. If you can predict the next token from the previous one, the generator is wrong.

### 2. Store a hash, expire fast, allow one use
Store a hash of the token (like a password), set expiry to 15 to 60 minutes, and burn the token on first use. The reset link in the email is the only place the raw token exists.

Expected: the database holds hashes, not raw tokens; an expired link shows a clean error; reusing a consumed link fails. Test all three.

### 3. Give identical responses for existing and missing accounts
The reset form must say the same thing whether the email is registered or not, or attackers can harvest your user list one address at a time. Send the email only if the account exists, but always show the same confirmation.

Expected: submitting a registered address and a gibberish address produces indistinguishable responses, including timing. Check response times too, since a fast "not found" versus a slow email send leaks the same signal.

### 4. Rate limit every step of the flow
Limit reset requests per email address and per IP, and limit token submission attempts. Without this, enumeration and token guessing are just a matter of patience.

```bash
grep -rn "password.*reset\|forgot" --include="*.py" [HOME]/... | head -20
```

Expected: each route from the grep has a throttle attached. A reset endpoint with no limit is a finding regardless of token strength.

### 5. Invalidate all sessions when the password changes
The reset completes only when every existing session for the account dies. Otherwise whoever held the old session keeps it, which defeats the point of the reset.

Expected: after completing a reset, previously issued session cookies and tokens stop working. Verify with the replay test: capture a session, reset, replay, confirm rejection.

### 6. Notify the account holder
Send a confirmation to the account email stating the password was changed, when, and what to do if it was not them. This is the tripwire that catches account takeovers.

Expected: the notification goes out on every completed reset, uses the [account email] on file, and includes a support contact path.

### Variant: reset links vs reset codes
Links are easier on desktop, short codes are easier on mobile where users switch apps. Codes need stricter attempt limits since the search space is smaller. Either way, the single-use and expiry rules do not change.

### Variant: security questions as a reset factor
Do not use them. Answers are guessable, findable on social media, and unchangeable after a breach. If a legacy flow still has them, the fix is removing them, not improving them.

### Variant: magic-link login
Magic links follow the same rules as reset tokens: random, hashed at rest, short expiry, single use. The difference is only UX. Do not let "it is just login" relax the token handling.

## Why this happens
Reset flows get built once, early, and rarely revisited, so they fossilize the threat model of the week they were written. Meanwhile they are the most attacked auth surface after login itself: no password needed, reachable anonymously, and every design shortcut (predictable tokens, verbose errors, eternal links) is directly exploitable. Treating reset as a second login path is the mindset fix.

## Edge cases and pitfalls
- Email delivery delays make short expiries painful; 15 minutes is the floor, and the error message should offer a fresh link, not a dead end.
- Submitting the token in the URL puts it in logs and history; accept it, then move it into a POST body or session immediately.
- Account recovery for users who lost email access needs a human process; do not weaken the automated flow to cover it.
- Test the flow end to end after every auth refactor; reset is the feature most often broken silently by session changes.

## Provenance

Resolved from the public thread: https://vectle.com/posts/pst_VC-zN7H52Ma2pWiYErFAeA
