# Security Forgot Password Controls ## Implemented Controls - Public entry points: - `POST /auth/forgot-password/request` - `POST /auth/forgot-password/confirm` - Enumeration resistance: - Request endpoint always returns `200` with a generic success message. - No account existence signal for unknown/unverified emails. - Verified-account gate: - Reset tokens are issued only when `emailVerified=true`. - Token security: - Reset links contain `uid` and raw `token` in query params. - Server stores only `SHA-256(token)`. - Token type is `password_reset`. - Token must match `uid`, be unused, and be unexpired. - Token is consumed once (`usedAt`) and cannot be reused. - Session invalidation: - Added `User.passwordChangedAt`. - JWT auth middleware rejects tokens with `iat <= passwordChangedAt`. - Reset and authenticated password-change both set `passwordChangedAt`. - Abuse controls: - Request endpoint: per-IP + email-fingerprint route keying. - Confirm endpoint: per-IP + uid-fingerprint route keying. - Endpoint-specific rate limits via env config. - Logging hygiene: - Structured security events for request/email/confirm outcomes. - No plaintext password or raw token in logs. - Misconfiguration resilience: - Email send failures do not leak through API response shape. - Generic response is preserved if SMTP is unavailable. ## Environment Settings - `PASSWORD_RESET_TTL_MINUTES` (default `30`) - `PASSWORD_RESET_RATE_LIMIT_PER_MINUTE` (default `5`) - `PASSWORD_RESET_CONFIRM_RATE_LIMIT_PER_MINUTE` (default `10`) ## Operational Verification 1. Verify `/auth/forgot-password/request` always returns the same JSON for unknown, unverified, and verified addresses. 2. Verify only verified users get `EmailToken(type=password_reset)` rows. 3. Verify `confirm` succeeds once and fails on replay. 4. Verify pre-reset session cookies fail on protected routes after successful reset. 5. Verify security logs contain `auth.password_reset.*` events and no raw token values.