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>
This commit is contained in:
saqib mir
2026-09-07 17:26:49 +05:30
co-authored by Claude Sonnet 5
parent b016e77b70
commit 52f1fa3db0
5 changed files with 344 additions and 0 deletions
+37
View File
@@ -0,0 +1,37 @@
# Quickstart: Validating Authentication Hardening
## Scenario 1 — password reset, end to end
1. `POST /auth/password-reset/request` with a real seeded account's email. **Expected**: `200`,
generic message; the server log shows a `password_reset_requested` line with a `resetUrl`
containing the real token.
2. Repeat with an email that doesn't exist. **Expected**: identical `200` response body to
step 1 — diff them to confirm.
3. `POST /auth/password-reset/consume` with the token from step 1's log and a policy-meeting new
password. **Expected**: `200`.
4. Repeat step 3 with the same token. **Expected**: rejected — the token is single-use.
5. `POST /auth/login` with the account's email and the new password from step 3. **Expected**:
`200`. Repeat with the account's old password. **Expected**: `401`.
## Scenario 2 — password strength enforced everywhere
1. `POST /admin/users` (as admin) with a password shorter than `PASSWORD_MIN_LENGTH`.
**Expected**: `400`, naming the actual minimum length.
2. `POST /auth/password-reset/consume` with a valid token and a too-short new password.
**Expected**: the same `400` rejection reason as step 1.
## Scenario 3 — login rate limiting
1. Submit `LOGIN_RATE_LIMIT_MAX_ATTEMPTS` failed login attempts for the same email within
`LOGIN_RATE_LIMIT_WINDOW_SECONDS`. **Expected**: each returns `401` (the existing
identical-failure-response).
2. Submit one more attempt for that same email, still within the window — this time with the
*correct* password. **Expected**: `429`, not `200` — the rate limit is checked before
credentials (FR-007).
3. Submit an attempt for a *different* email within the same window. **Expected**: proceeds
normally (evaluated on its own credentials, not rate-limited).
## What "done" looks like
All three scenarios pass against a real Postgres/Redis, and `POST /admin/users`'s own existing
tests (010-identity-auth) still pass with the added password-strength check in place.