Password Reset
Self-service flow via emailed token.
Files
- Schema:
src/lib/db/schema/password-resets.ts(token PK, user_id, expires_at, used_at). - Request endpoint:
POST /api/auth/forgot-password. - Complete endpoint:
POST /api/auth/reset-password. - UIs:
/forgot-password,/reset-password.
Flow
- User visits
/forgot-password, submits email. - Server
POST /api/auth/forgot-password:- Looks up user by email.
- Always returns
{ ok: true }— does not reveal whether the email exists. - If user exists: inserts
password_resetsrow with a 48-char token, 1-hour expiry. Emails the reset link via the app's ownsendEmail()usingSYSTEM_FROM_EMAIL.
- User clicks link →
/reset-password?token=<token>. - User submits new password (min 8 chars).
- Server
POST /api/auth/reset-password { token, password }:- Looks up token — rejects if unknown, expired, or used.
- Hashes password with bcrypt cost 12.
- Updates
users.password_hashand bumpsusers.session_version. - Revokes every
user_sessionsrow and every unrevoked trusted device. - Revokes the account's OAuth refresh tokens and the
api_keysrows they minted, then drops the in-memory session cache. - Marks token
used_at. - Writes audit log (
user.password_reset) to every org the user belongs to.
- UI redirects to
/login.
Security
- 1-hour TTL — much shorter than email verification.
- Single-use via
used_at. - 48-char random token (~276 bits).
- Enumeration-resistant: endpoint always returns success regardless of email existence.
- Reset is treated as a compromise response and cuts every door at once:
session_version(rejects older JWTs on the next request),user_sessions(so the Devices panel agrees rather than listing revoked sessions as active), trusted-device cookies (which skip the emailed login code), and OAuth grants — a browser-login grant consented during the compromise holds a 30-day refresh token that keeps minting livesok_live_*keys, and nothing else reaches it. The OAuth sweep runs last, after the cache eviction and the audit, so a failure there cannot discard the rest of the response; it answers 500 with a message naming what was left undone.
Env
SYSTEM_FROM_EMAIL— verified SES identity for the reset email.