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:
co-authored by
Claude Sonnet 5
parent
b016e77b70
commit
52f1fa3db0
@@ -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.
|
||||
Reference in New Issue
Block a user