Mechanical reformat — no logic changes. 398 files changed, 19125 insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc (singleQuote:true, semi:true, tabWidth:2, trailingComma:all, printWidth:100). Isolated per D-13-08 for reviewability.
14 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 01-foundation-broker-spike | 02 | execute | 2 |
|
|
true |
|
|
After this plan a real user can: hit the app, get redirected to Authelia, log in, and land on a shell that shows their name and their assigned color — with the session persisting across the access-token refresh window. This is AUTH-01/02/03 end to end.
Purpose: Authentication is the gate for every other feature; the identity + color row is consumed by the calendar broker (Plan 03) and all later phases. Output: Working Authelia OIDC login, stable identity + color, /api/me, authenticated PWA shell.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @./CLAUDE.md @.planning/phases/01-foundation-broker-spike/01-CONTEXT.md @.planning/phases/01-foundation-broker-spike/01-RESEARCH.md @.planning/phases/01-foundation-broker-spike/01-01-SUMMARY.md<artifacts_produced>
Artifacts this phase produces (Plan 02)
New files: apps/api/src/auth/middleware.ts, apps/api/src/auth/user.ts, apps/api/src/routes/me.ts, apps/pwa/src/api/client.ts.
New exported symbols: upsertUser (auth/user.ts), COLOR_PALETTE (auth/user.ts), meRouter (routes/me.ts), setupAuth/oidc middleware mount (auth/middleware.ts).
New route paths: GET /api/me, GET /callback (OIDC callback handled by processOAuthCallback).
New env vars: OIDC_AUTH_SECRET, OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_REDIRECT_URI, OIDC_AUTH_EXTERNAL_URL.
</artifacts_produced>
Fill `tests/auth/user.test.ts` GREEN using the test-DB fixture (tests/helpers/db.ts): assert (a) first insert assigns palette[0]; (b) second user assigns palette[1]; (c) re-upsert of user 1 returns the identical row + color and does not create a duplicate; (d) lookup is by iss+sub.
Create `src/routes/me.ts` exporting `meRouter` (Hono): GET / calls `getAuth(c)` to read `iss`, `sub`, `email`, then `upsertUser(auth.iss, auth.sub, auth.email)` and returns `{ user: { id, displayName, color } }`. Mount `app.route('/api/me', meRouter)`.
Session persistence (AUTH-02) is handled by @hono/oidc-auth refresh-token rotation (backend-held refresh token, no iframe — D-12). Note in a code comment that OIDC_AUTH_REFRESH_INTERVAL / OIDC_AUTH_EXPIRES govern this; defaults are acceptable for v1.
PWA: create `apps/pwa/src/api/client.ts` with a typed `fetchMe()` (GET /api/me, credentials: 'include'). Update `App.tsx`: React Query `useQuery(['me'], fetchMe)`; on 401/redirect the browser follows Authelia (full-page). Render the member's displayName and a color swatch using `user.color`. Keep the /health indicator from Plan 01.
Also record the Authelia client registration YAML (from RESEARCH Pattern 1) in the SUMMARY so the operator can paste it into Authelia's configuration.yml — this is the only human-side config (no code change in this repo).
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| Browser → Pangolin → Hono /api/* | Untrusted client; only authenticated requests cross (OIDC session cookie) |
| Authelia → /callback | OIDC authorization-code exchange; PKCE + state validate the callback |
| Hono → Authelia token endpoint | Backend confidential client; client_secret + refresh token never reach the browser |
STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|---|---|---|---|---|
| T-02-01 | Spoofing | OIDC redirect_uri | mitigate | Authelia validates exact match; OIDC_REDIRECT_URI env must equal the registered URI; OIDC_AUTH_EXTERNAL_URL set so Pangolin Host header cannot forge the redirect (Pitfall 1) |
| T-02-02 | Spoofing | CSRF on /callback | mitigate | @hono/oidc-auth uses PKCE (state + code_verifier); require_pkce true, S256 in Authelia client |
| T-02-03 | Tampering | OIDC session JWT cookie | mitigate | Cookie signed with OIDC_AUTH_SECRET (32+ char), httpOnly + Secure + SameSite; verified every request |
| T-02-04 | Information Disclosure | Refresh token / client_secret | mitigate | Backend-only (D-12); never serialized to frontend; not logged; OIDC_CLIENT_SECRET is the plain secret in env, never committed |
| T-02-05 | Elevation of Privilege | /api/* without auth | mitigate | oidcAuthMiddleware mounted on /api/*; no guest access (ASVS V4) |
| T-02-06 | Spoofing | Identity confusion via mutable email | mitigate | Identity keyed on oidc_iss + oidc_sub, never email (D-10) |
</threat_model>
- `pnpm exec tsc --noEmit` clean - user.test.ts green (from Task 1) - index.ts mounts oidcAuthMiddleware on /api/*, /callback before it, /health public - /api/me returns identity + color - .env.example complete (OIDC_* + OIDC_AUTH_EXTERNAL_URL) - Manual (Plan 04 deploy): unauthenticated /api/me → 302 to Authelia; after login lands on shell with name + color<success_criteria>
- AUTH-01: unauthenticated /api/* redirects to Authelia; login lands authenticated (verified live in Plan 04)
- AUTH-02: session persists via backend refresh-token rotation (no iframe)
- AUTH-03: stable identity (iss+sub) + stable distinct per-member color, asserted by unit tests
- /api/me returns the member; PWA shell shows name + color </success_criteria>