AZ-010 — Password recovery
Status
In progress
Context
Read README. Login/session/password policy and password change exist. Resend adapter and local Mocking Bird synthetic mail previews exist. There is no self-registration or email change flow; provisioned user's normalized email is the recovery address. Use OWASP recovery guidance and maintained crypto/password libraries rather than inventing a token protocol.
Goal
Deliver mobile forgot/reset-password with non-enumerating requests, short-lived single-use credentials, revocation and real browser/database tests using mocked external email.
Requirements
- Add
/forgot-passwordand/reset-passwordas public, menu-hidden declarations, linked appropriately from login. Use localized MUI vertical forms, autocomplete/input modes, password-manager/paste support, labeled confirmation/reveal controls, field error focus and progress/success/error states from earlier tasks. Preserve safe internal return navigation. - Add
POST /api/auth/forgot-password, accepting normalized email and returning 202 with the same safe generic acknowledgement for existing/nonexistent/disabled users and email delivery failures. Invalid shape gives 400, abuse gives generic 429/Retry-After without exposing address existence. Apply CSRF/origin and IP/account-dimension throttling, including unknown accounts. - Use Node cryptographic randomness for a high-entropy opaque reset credential; store only a cryptographic digest plus user reference, creation/expiry/consumed state in PostgreSQL. This uses standard random/digest primitives, no password-derived or signed custom token scheme. Lifetime 30 minutes, single-use, newest valid request supersedes earlier outstanding tokens. Bound cleanup and request issuance to avoid unbounded rows; never log/store the raw credential except a deliberately guarded synthetic local preview from AZ-008.
- Compose reset URLs from validated configured public
APP_PUBLIC_URL, never incoming Host, Origin or forwarded headers. Staging/production require HTTPS and a real mail mode/sender/key; disabled/mock recovery delivery is forbidden for hosted enabled functionality. Local HTTP is permitted with local classification and explicit mock enablement. Configure test URLs/ports for both host E2E and Docker gateway; no browser Docker hostnames. - Prefer URL fragment for the raw credential to avoid server/proxy query logging. Frontend reads into transient memory, immediately removes fragment with replaceState, and submits credential only in reset JSON body. Document reload behavior (reopen email link if removed credential is lost). No analytics/third-party embeds on reset page; set appropriate Referrer-Policy/no-store metadata and ensure raw credential does not enter frontend logging, route telemetry, query keys, Redux/localStorage or Playwright retained artifacts.
- Reset email uses the application email capability, minimal safe text/HTML and configured brand/app URL; vendor details remain in adapter. Use an explicit supported language choice based on saved account preference or English fallback for the two template variants. Avoid adding a general backend translation framework. If email send fails, invalidate the newly issued token appropriately, log only safe category/correlation, and maintain generic public acknowledgement. Do not report successful delivery beyond provider acceptance.
- Reduce account-enumeration timing: use similar bounded async/padding paths for existence and delivery outcomes without blocking the event loop or creating a durable queue infrastructure. Document the bounded delivery/acknowledgement strategy and limitations; controlled fake clock tests verify branches, not brittle exact network timing. No real email sends in automated tests.
- Add
POST /api/auth/reset-password, accepting credential/new password, 204 on successful atomic consumption/hash update/all-session revocation. Missing/expired/used/malformed tokens have one safe invalid-link outcome; policy violations use safe fields. Do not automatically log in after reset. Clear transient credential/new-password form values and direct to login. - Ensure token consumption and password update/revocation are one concrete transaction: two simultaneous reset attempts cannot both succeed, and failure cannot consume without updating or update without revoking. Password change from AZ-007 invalidates outstanding reset tokens too. Prevent issuing a token based on stale password state during concurrent reset/change; lock/conditional persistence operations must be tested, not assumed.
- Preserve AZ-007's revocation protection against in-flight login/session-store saves; a reset cannot leave a newly resurrected old-credential session valid after the atomic update.
- Invalid/expired link UI offers restart recovery without revealing account information. Rate-limit reset validation attempts. Existing sessions and old password fail after reset; only newly entered credentials can establish a new session. Auth/API error/logging contracts remain compatible, and public message/health stay unaffected.
- Mocking Bird success/rejection/timeout/latency drive recovery tests. Use a synthetic fixture preview to retrieve a reset link in local browser E2E, never a production token-inspection endpoint. Test both active sessions being revoked, expiry/reuse and race/rollback at database level. E2E uses per-project users so mutation/token state cannot collide.
Non-goals
No email address change/verification, self-registration, invitations/MFA, token export, production mail preview, queue workers, bulk notifications, or live provider deployment/signup.
Implementation guidance
Keep token/password use cases independent of Fastify; repositories own concrete transactional
queries, schema/migrations remain in packages/db. Use the current policy/hasher, CSRF boundary,
query/cache clearing and email adapter, not parallel implementations. Frontend wrappers own body and
response parsing. Server no-store/referrer and frontend fragment removal have separate tests.
Mutation/schema logging must not print token request bodies; test sensitive sentinel absence.
Acceptance criteria
- Existing/unknown/disabled/delivery-failure requests have generic matching public response; invalid input and abuse retain safe validated handling.
- Reset URL is configured-origin-only; raw credentials never persist in DB/logs/browser store, general capture history or retained reports.
- Valid token succeeds exactly once within lifetime; expiry/reuse/concurrency/rollback and password-change interactions are enforced through real PostgreSQL.
- Reset revokes all sessions and old password, requires login, and supports accessible mobile success/invalid/expired/restart behavior.
- Complete local browser recovery uses real auth/database with only outbound email mocked; hosted unsafe delivery/developer config is rejected.
Verification
pnpm db:generate
pnpm quality
pnpm test
pnpm build
pnpm test:coverage
pnpm validate:docker
Review generated SQL/journal; migrations need fresh/message-schema-upgrade/rerun proof. Add token policy/cleanup/template unit, injection/error/redaction, real transaction/concurrency, RTL form, and mobile/desktop Playwright recovery cases. Manual phone check: email link opening, fragment removal, keyboard/autofill, refresh/reopen-link guidance, 320px/200% zoom, expired-link recovery. Do not send real mail or reset hosted credentials for local acceptance. Actual configured sender and delivery/TLS/origin are release prerequisites to verify when that work is authorized.
Documentation
Update OpenAPI, auth ADR/architecture (credential lifecycle/transaction/non-enumeration limitations), development (APP_PUBLIC_URL/provider configuration and local recovery), testing (real DB/mock email), Docker/CI fixtures/redaction, current-state/handover. State that provider acceptance is not inbox delivery and that local success supplies no hosted release evidence.
Dependencies
AZ-007 and AZ-009; both include earlier auth, shell, errors/logging and mail capture.