feat: authentication with local accounts, sessions, CSRF, RBAC, and OIDC PKCE (phase 3)
- argon2id (t=3, m=64MiB, p=2) PHC hashing honoring embedded params - token-bucket rate limiting (10/15min per IP+email) on login/register - opaque 32B session tokens in Mongo, 30-day sliding expiry, logout-all - CSRF double-submit cookie/header on authenticated mutations - bootstrap admin from env with forced first-login password change - requireAuth/requireRole middleware with disabled-account enforcement - OIDC code flow with PKCE: lazy discovery, account linking only on verified email, auto-created developer accounts - unit tests (RBAC matrix, CSRF, password, rate limiter) + integration suite covering the full auth matrix incl. an in-test fake IdP Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -57,3 +57,28 @@ Spec-silent choices, recorded as required by the build instructions.
|
||||
- **Signed file tokens** are `base64url(exp || hmac-sha256(fileID|exp))` with
|
||||
a key derived as `sha256("bountyboard/file-url/v1" + SESSION_SECRET)` so
|
||||
file tokens can never collide with other uses of the session secret.
|
||||
|
||||
## Phase 3
|
||||
|
||||
- **OIDC linking requires `email_verified=true`.** An unverified IdP email
|
||||
matching an existing local account is rejected (`oidc_email_unverified`)
|
||||
instead of linking or creating a duplicate — linking on unverified email
|
||||
would allow account takeover via a rogue IdP account.
|
||||
- **OIDC provider discovery is lazy** (first login attempt, retried on
|
||||
failure) so an unreachable IdP cannot block app startup.
|
||||
- **CSRF protection applies to authenticated mutations only**; login and
|
||||
register are pre-session and protected by rate limiting + SameSite=Lax.
|
||||
The CSRF cookie is intentionally JS-readable (double-submit pattern).
|
||||
- **Password change revokes all other sessions** of the user (recovery
|
||||
semantics) but keeps the current one. `SetPassword` is deliberately not
|
||||
version-checked: a password change must never lose an optimistic-locking
|
||||
race against a profile edit.
|
||||
- **Forced password change (`mustChange`)** is enforced in `requireAuth` via
|
||||
a path allowlist (`me`, `logout`, `logout-all`, `change-password`).
|
||||
- **Sessions slide at most once per hour** (refreshedAt watermark) instead of
|
||||
updating expiry on every request.
|
||||
- **Login rate-limit key is `clientIP|email`** (and `register|clientIP` for
|
||||
registration), using the trusted-proxy-resolved IP.
|
||||
- **Mongo runs with `--wiredTigerCacheSizeGB 0.5`** in compose — the target
|
||||
host has 4 GB RAM; default cache (≈50% of RAM) caused a restart under the
|
||||
integration-test load.
|
||||
|
||||
Reference in New Issue
Block a user