Password Reset & Account Recovery
Password reset & account recovery helps users regain access—but it’s also a common account takeover path if implemented weakly.
Beginner mental hook: Recovery must be as strong as login. If not, attackers stop attacking login and attack recovery.
Mental model
- Request: user asks to reset (system should not reveal whether the account exists).
- Proof: system sends a short-lived, single-use reset token/OTP to a verified channel.
- Consume: user redeems token once to set new password.
- Invalidate: after reset, old sessions/refresh tokens should be revoked.
- Uniform response: prevents account enumeration.
- Token storage: store only hashed tokens server-side; show raw token once.
- Binding: token tied to account and reset intent; invalidated on use and on password change.
Practical note: these issues usually slip through because everything looks “fine” in happy-path testing — until one weird request hits production.
Common failure modes (what goes wrong)
A) Account enumeration
- Root cause: response differs for “email exists” vs “doesn’t exist”.
- Attacker mindset: build a list of valid accounts, then target them.
- Defense: uniform responses and timing; rate limit requests.
B) Reset token lifecycle bugs
- Root cause: long TTL, reusable tokens, or tokens not invalidated after password change.
- Attacker mindset: find a token that stays valid or can be reused.
- Defense: short TTL, single-use, hash at rest, invalidate on use and after password reset.
C) Weak verification channel
- Root cause: reset delivered to insecure channel or email compromise not considered.
- Defense: step-up for risky accounts, delays, additional verification, and strong notifications.
D) No post-reset revocation
- Root cause: password changes do not revoke active sessions/refresh tokens.
- Impact: attacker keeps access even after victim resets password.
- Defense: revoke all sessions/refresh tokens on password reset and security events.
Defensive patterns (Node.js)
Issue a reset token (store hash, short TTL, uniform response)
import crypto from "crypto";
app.post("/password/reset/request", async (req, res) => {
const email = String(req.body.email || "").toLowerCase();
const user = await db.users.findByEmail(email);
// âś… Uniform response to prevent enumeration
if (!user) return res.json({ ok: true });
const raw = crypto.randomBytes(32).toString("hex"); // raw token (send to user)
const hash = crypto.createHash("sha256").update(raw).digest("hex");
await db.resetTokens.insert({
userId: user.id,
tokenHash: hash,
expiresAt: Date.now() + 15 * 60 * 1000, // 15m
usedAt: null,
});
await emailer.sendResetLink(user.email, raw); // send raw token
res.json({ ok: true });
}); Consume a reset token (single-use + revoke sessions)
app.post("/password/reset/confirm", async (req, res) => {
const raw = String(req.body.token || "");
const newPassword = String(req.body.newPassword || "");
const hash = crypto.createHash("sha256").update(raw).digest("hex");
const token = await db.resetTokens.findValidByHash(hash); // checks expiresAt and usedAt
if (!token) return res.status(400).json({ ok: false, error: "invalid or expired" });
await db.resetTokens.markUsed(token.id);
await db.users.updatePassword(token.userId, newPassword);
// âś… Revoke sessions/refresh tokens after reset
await db.sessions.revokeAllForUser(token.userId);
res.json({ ok: true });
}); Guardrails: short TTL, hash at rest, single-use, uniform responses, and post-reset revocation.
Safe validation (defensive verification)
- Confirm uniform responses and consistent timing (no enumeration).
- Confirm reset tokens are high-entropy, short-lived, and single-use.
- Confirm raw tokens are not stored; only hashes are stored server-side.
- Confirm tokens are invalidated after use and after password changes.
- Confirm sessions/refresh tokens are revoked after reset.
- Confirm rate limiting and monitoring on reset requests and confirmations.
Interview Questions & Answers (Easy → Hard)
Easy
- Why is password reset risky?
A: Plain: it’s a shortcut into accounts. Deep: weak lifecycle becomes primary takeover path. - How do you prevent account enumeration?
A: Plain: uniform responses. Deep: consistent timing + rate limits + generic messages. - Should you store reset tokens?
A: Plain: not raw. Deep: store hashed tokens; show raw token once. - How long should reset tokens live?
A: Plain: short. Deep: minutes with one-time use and invalidation on password change. - What happens after reset?
A: Plain: user gets back in. Deep: revoke sessions/refresh tokens and notify user. - OTP vs link tokens?
A: Plain: both can work. Deep: both need short TTL, rate limits, and strong verification. - What do you log?
A: Plain: events. Deep: requests/confirmations, IP/device metadata; never log tokens.
Medium
- Scenario: token can be reused. Impact?
A: Plain: repeated takeover. Deep: single-use marking required + invalidate on password change. - Scenario: reset doesn’t revoke sessions. Impact?
A: Plain: attacker stays logged in. Deep: revoke all sessions/refresh tokens after reset. - Scenario: reset page leaks token via referrer/analytics. Fix?
A: Plain: don’t leak secrets. Deep: avoid tokens in URLs when possible; scrub referrers; strict CSP; avoid logging. - Follow-up: rate limiting strategy?
A: Plain: slow attackers. Deep: per IP + per account + global thresholds + captcha/risk signals. - Scenario: email account compromised. How do you respond?
A: Plain: protect recovery. Deep: step-up, delays, and alternate verified channels for high-value accounts. - Follow-up: do you notify users?
A: Plain: yes. Deep: notify on request and completion; include “not you?” action steps. - Scenario: multiple reset requests spam. What do you do?
A: Plain: throttle. Deep: cooldown windows, lockouts, and anomaly detection.
Hard
- How do you design high-assurance recovery for VIP accounts?
A: Plain: stronger proof. Deep: step-up, delays, device checks, and manual review with audit trails. - How do you bind reset token to account state?
A: Plain: invalidate on changes. Deep: include a passwordVersion/securityStamp and reject older tokens after change. - What telemetry matters most?
A: Plain: watch abuse. Deep: request spikes, geo anomalies, repeated confirmations, and recovery success rates. - Scenario: phishing targets reset flow. Controls?
A: Plain: reduce success. Deep: short TTL, step-up, user education, and anomaly detection. - How do you test the reset flow safely?
A: Plain: automated tests. Deep: token reuse/expiry tests, session revocation tests, enumeration tests. - Follow-up: what’s a “security stamp” approach?
A: Plain: versioning. Deep: bump a server-side value on password reset to invalidate sessions and old tokens.
Exploitation progression (attacker mindset)
- Enumerate accounts: attackers look for responses that confirm valid users.
- Abuse lifecycle: reusable or long-lived tokens are attractive.
- Target channel: compromise email/phone or exploit weak recovery verification.
- Maintain access: if sessions aren’t revoked, attacker keeps access post-reset.
Checklist
- Uniform responses (no enumeration) + strong rate limiting.
- Reset tokens: high entropy, short TTL, single-use, hashed at rest.
- Invalidate tokens on use and on password changes.
- Revoke all sessions/refresh tokens on reset.
- Notify user of reset request and reset completion.
- Step-up/manual review for high-risk accounts if needed.
Remediation playbook
- Fix enumeration: uniform responses and consistent timing; add throttling.
- Fix token lifecycle: short TTL, single-use, hashed storage, invalidate on reset.
- Revoke sessions/refresh tokens post-reset; add alerts for security events.
- Harden recovery: additional verification for high-risk users; delays and device checks.
- Add tests for token reuse, expiry, and revocation; monitor abuse patterns.