Files
support_backend/specs/013-auth-hardening/contracts/auth-hardening-contract.md
T
saqib mirandClaude Sonnet 5 52f1fa3db0 docs(013-auth-hardening): plan, research, data model, contract, quickstart
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>
2026-09-07 17:26:49 +05:30

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_ERRORnewPassword doesn't meet validatePasswordStrength.
  • 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.