Reset tokens live only in Redis as a paired key shape (mirrors 010's own revocation-denylist pattern) - never in Postgres, never storing the raw token. Password-strength policy is one shared validator called from both the new reset-consume endpoint and 010's existing POST /admin/users. Login rate-limiting reuses the existing checkRateLimit helper from 002's own inbound trust boundary, keyed by submitted email, checked before any credential verification. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2.0 KiB
Contract: Authentication Hardening
POST /auth/password-reset/request
Auth: None (like login itself — the caller has no session yet).
Request body: { "email": "string" }
Response 200 (always, regardless of whether the account exists):
{ "success": true, "data": { "message": "If that account exists, a reset link has been sent." }, "meta": null }
No token, ever, appears in this response — it's only visible via the stub's own server-side log
line ({ "event": "password_reset_requested", "userId": "...", "resetUrl": "..." }).
POST /auth/password-reset/consume
Auth: None (the token itself is the credential).
Request body: { "token": "string", "newPassword": "string" }
Responses:
200—{ "success": true, "data": { "message": "Password updated." }, "meta": null }400 VALIDATION_ERROR—newPassworddoesn't meetvalidatePasswordStrength.400 INVALID_RESET_TOKEN(or equivalent) — token missing, expired, or already used. The response never distinguishes which of the three — matching data-model.md's own note that a consumer can't otherwise tell "expired" from "already used" from "never existed."
PATCH /admin/users — unchanged route, tightened validation
POST /admin/users (010-identity-auth) now also rejects a password shorter than
PASSWORD_MIN_LENGTH with the same validatePasswordStrength message the reset-consume
endpoint uses — no new route, no schema field change, just a stricter check on the existing
password field.
POST /auth/login — unchanged route, new pre-check
Before this feature: any number of attempts, any speed. After: attempts for the same submitted
email beyond LOGIN_RATE_LIMIT_MAX_ATTEMPTS within LOGIN_RATE_LIMIT_WINDOW_SECONDS receive:
{ "success": false, "error": { "code": "RATE_LIMIT_EXCEEDED", "message": "Too many login attempts. Try again later." } }
with HTTP 429, distinct from the existing 401 identical-failure-response 010 already
returns for wrong credentials.