10 KiB
phase, plan, subsystem, tags, status, dependency_graph, tech_stack, key_files, decisions, metrics
| phase | plan | subsystem | tags | status | dependency_graph | tech_stack | key_files | decisions | metrics | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 19-local-auth-no-oidc-mode | 03 | auth |
|
complete |
|
|
|
|
|
Phase 19 Plan 03: Auth Routes + Middleware Wiring Summary
One-liner: localAuthMiddleware (cookie→c.set('user')), GET /api/auth/mode pre-auth endpoint, rate-limited POST /api/auth/local/login with timing-safe dummy-hash, logout, index.ts middleware chain wired with OIDC-guard skip-when-user-set and /callback link branch for linkOidcToUser, de-Authelia-ized middleware comments.
Tasks Completed
| Task | RED Commit | GREEN Commit | Key Files |
|---|---|---|---|
| 1: localAuthMiddleware + GET /api/auth/mode | ac32bd4 |
be7a0ae |
localAuthMiddleware.ts, authMode.ts, index.ts |
| 2: POST /api/auth/local/login (rate-limit + lockout) + logout | db66295 |
c437f40 |
localAuth.ts |
| 3: index.ts wiring + /callback link branch + de-Authelia comments | — | 9b569ef |
index.ts, middleware.ts |
What Was Built
Task 1: localAuthMiddleware + GET /api/auth/mode (TDD)
apps/api/src/auth/localAuthMiddleware.ts — exports localAuthMiddleware(): MiddlewareHandler:
- If
c.get('user')already set (devAuthBypass ran first): no-op, calls next() - Calls
verifyLocalSessionCookie(c)— returns null if no cookie / invalid / expired - On null: calls next() WITHOUT c.set('user') — Pitfall-1 guard; OIDC guard fires on absent key
- On valid userId: SELECTs users row; if found,
c.set('user', {...} as typeof DEV_USER)with matching shape
apps/api/src/routes/authMode.ts — exports authModeRouter with GET /mode:
localEnabled: trueunconditionally (D-01)oidcEnabled: Boolean(OIDC_ISSUER env)|| falls back toapp_configoidc_issuer row- No auth gate — pre-auth endpoint
Tests: 8 tests pass (4 middleware + 4 mode tests)
Task 2: POST /api/auth/local/login + logout (TDD)
apps/api/src/routes/localAuth.ts — exports localAuthRouter and loginAttempts:
POST /local/login— zValidator + noEchoHook + per-IP loginAttempts Map- lockedOut check (>= LOCKOUT_FAILURES=10) → 423
{ error: 'Account locked' } - Rate window check (>= RATE_WINDOW_FAILURES=5, within RATE_WINDOW_SECS=60) → increments counter + 429
- Selects local_credentials by username; ALWAYS runs verifyPassword (DUMMY_HASH on unknown username — T-19-12)
- Same 401 body for wrong-password AND unknown-username (no enumeration)
- On success: loginAttempts.delete(ip), issueLocalSessionCookie, 200
{ ok: true }
- lockedOut check (>= LOCKOUT_FAILURES=10) → 423
POST /local/logout+GET /local/logout→ clearLocalSessionCookie → 200{ ok: true }
Tests: 8 tests pass (login success/failure/enumeration/rate-limit/lockout/logout/no-echo)
Task 3: index.ts wiring + /callback link branch + de-Authelia comments
apps/api/src/index.ts changes:
- Pre-auth mounts:
app.route('/api/auth', authModeRouter)+app.route('/api/auth', localAuthRouter)before devAuthBypass app.use('/api/*', localAuthMiddleware())after devAuthBypass, before OIDC guard- OIDC guard wrapper:
oidcHandler = oidcAuthMiddleware()stored once at construction, invoked per-request only whenc.get('user')is falsy (D-03 coexistence seam) /callbackextended: reads URLstateparam, tries Jwt.verify with LOCAL_SESSION_SECRET; iflinkUserIdin payload → call linkOidcToUser after processOAuthCallback; OidcLinkConflictError → redirect/?error=oidc-link-conflict- Authelia references removed from 2 comments (D-06)
apps/api/src/auth/middleware.ts changes:
- Header: "Authelia as the identity provider" → "generic OIDC identity provider" (D-06)
- "Authelia base URL" → "OIDC issuer URL" (D-06)
- "Authelia's refresh_token_lifespan" → "the OIDC provider's refresh_token_lifespan" (D-06)
Full suite: 446/446 tests pass; pnpm --filter @familysync/api typecheck exits 0.
Deviations from Plan
Auto-fixed Issues
1. [Rule 1 - Bug] Rate-limit counter not incrementing during 429 window
- Found during: Task 2 GREEN phase (Test 5 returning 429 instead of 423 for the 11th attempt)
- Issue: After 5 failures, subsequent attempts returned 429 (early return) without incrementing the counter. The counter never reached LOCKOUT_FAILURES=10 because the early-return prevented accumulation.
- Fix: Inside the rate-window 429 branch: increment counter, update lockedUntil, check if lockedOut (returns 423 if so), else return 429. This way brute-force attacks accumulate toward lockout even during the rate window.
- Files modified: apps/api/src/routes/localAuth.ts
- Commit:
c437f40
2. [Rule 1 - Bug] oidcAuthMiddleware() factory called per-request in OIDC guard wrapper
- Found during: Task 3 execution — me.test.ts assertion that oidcAuthMiddleware is called exactly once during app init
- Issue: Original OIDC guard wrapper called
oidcAuthMiddleware()(c, next)per-request; existing testwires oidcAuthMiddleware on /api/* when bypass is not activeassertsoidcMiddlewareSpy.toHaveBeenCalledTimes(1)(factory called once at construction). - Fix: Store
const oidcHandler = oidcAuthMiddleware()at construction time; invokeoidcHandler(c, next)per-request inside the wrapper. - Files modified: apps/api/src/index.ts
- Commit:
9b569ef
3. [Rule 1 - Bug] Type incompatibility: localAuthMiddleware c.set('user') type error
- Found during: Task 3 typecheck
- Issue:
ContextVariableMapmaps 'user' totypeof DEV_USER(narrowas constliteral). The middleware constructs{ id: number; oidcIss: string; ... }which TypeScript rejects as incompatible. - Fix: Add
import type { DEV_USER }and cast withas typeof DEV_USERon the c.set call. - Files modified: apps/api/src/auth/localAuthMiddleware.ts
- Commit:
9b569ef
Threat Surface Scan
All new/modified routes in this plan:
GET /api/auth/mode— pre-auth, no credentials, no sensitive data; reads only env/app_configPOST /api/auth/local/login— new attack surface; mitigated by T-19-11 (rate-limit), T-19-12 (timing-safe dummy hash, no-enumeration 401), T-19-14 (noEchoHook)POST/GET /api/auth/local/logout— clears cookie only; no sensitive data exposed
No new trust boundaries beyond those in the plan's threat model.
TDD Gate Compliance
Task 1 (TDD):
- RED commit:
ac32bd4— test(19-03): add failing tests for localAuthMiddleware and GET /api/auth/mode - GREEN commit:
be7a0ae— feat(19-03): implement localAuthMiddleware, GET /api/auth/mode...
Task 2 (TDD):
- RED commit:
db66295— test(19-03): add failing tests for POST /api/auth/local/login + logout - GREEN commit:
c437f40— feat(19-03): implement POST /api/auth/local/login (rate-limit + lockout) + logout
Task 3 (auto): No TDD cycle required.
Known Stubs
None. All new endpoints return real data and perform real operations.
Self-Check: PASSED
All created files confirmed present on disk:
- FOUND: apps/api/src/auth/localAuthMiddleware.ts
- FOUND: apps/api/src/routes/authMode.ts
- FOUND: apps/api/src/routes/localAuth.ts
- FOUND: apps/api/tests/auth/localAuthMiddleware.test.ts
- FOUND: apps/api/tests/routes/authMode.test.ts
- FOUND: apps/api/tests/routes/localAuth.test.ts
All commits confirmed in git log:
ac32bd4: test(19-03): add failing tests for localAuthMiddleware and GET /api/auth/modebe7a0ae: feat(19-03): implement localAuthMiddleware, GET /api/auth/mode, and pre-auth route mountsdb66295: test(19-03): add failing tests for POST /api/auth/local/login + logoutc437f40: feat(19-03): implement POST /api/auth/local/login (rate-limit + lockout) + logout9b569ef: feat(19-03): wire /callback link branch, OIDC-guard skip, de-Authelia comments
Test results: 446/446 pass (34 test files); pnpm --filter @familysync/api typecheck exits 0.