p0-c1-password-reset

donetype/backlogpriority/p0severity/criticaltopic/auth

p0 · C1 · Password reset flow

TL;DR

Shipped in Phase B: forgot/reset routes, hashed single-use tokens (1h), enumeration-safe requestPasswordReset, sessionsValidAfter revocation, Resend templates, and Playwright happy-path E2E. Remaining audit gaps moved to follow-up backlog items.

Status: done (2026-05-24) · Source: [[Projects/personal-finance-notion/context/audit-2026-05-17-auth|Auth audit 2026-05-17 §C1]]

Shipped (app repo)

  • src/lib/models/passwordResetTokenModel.ts — sha256 hash, TTL, usedAt, indexes
  • src/lib/actions/passwordReset.tsrequestPasswordReset, resetPassword
  • Public routes: /forgot-password, /reset-password (src/lib/auth-routes.ts)
  • Session kill via user.sessionsValidAfter on successful reset (Auth.js JWT)
  • tests/e2e/password-reset.spec.ts — request → reset → old password fails → new password works

Implementation checklist

  • Tokens are 32+ random bytes (randomBytes(32) hex)
  • Only the hash is stored; raw token only in the email link
  • Single-use via usedAt
  • Expiry ≤ 1h
  • Generic response whether the email exists or not
  • On success, other sessions invalidated (sessionsValidAfter)
  • Raw token not logged in application logs
  • Playwright E2E (happy path)

Spun out (not blocking “done”)

  • [[Projects/personal-finance-notion/backlog/p1-password-reset-rate-limits|p1 · Password reset rate limits (3/h email + IP)]]
  • [[Projects/personal-finance-notion/backlog/p2-auth-token-flow-edge-e2e|p2 · Auth token flow edge-case E2E]] — reuse / expiry / forged reset token

Related

  • [[Projects/personal-finance-notion/decisions/adr-2026-05-18-authjs-migration|ADR 2026-05-18]]