From 3122fdaf37f28dc609665e8471010098dc1db91a Mon Sep 17 00:00:00 2001 From: ulfrxdev Date: Tue, 28 Apr 2026 13:46:46 +0200 Subject: [PATCH] docs(02-02): complete server auth boundary plan - add execution summary with verification and deviations - update state, roadmap progress, and completed auth requirements --- .planning/REQUIREMENTS.md | 4 +- .planning/ROADMAP.md | 4 +- .planning/STATE.md | 29 +-- .../02-02-SUMMARY.md | 169 ++++++++++++++++++ 4 files changed, 188 insertions(+), 18 deletions(-) create mode 100644 .planning/phases/02-authentication-foundation/02-02-SUMMARY.md diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index d3c48c7..6eb5a0c 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -9,10 +9,10 @@ - [ ] **AUTH-01**: User can sign in via the self-hosted Authentik instance using OIDC (authorization code flow with PKCE) - [ ] **AUTH-02**: Client stores access + refresh tokens securely (iOS Keychain / Android EncryptedSharedPreferences) -- [ ] **AUTH-03**: Ktor server validates incoming access tokens via Authentik's JWKS endpoint (issuer, audience, expiry, signature, clock skew leeway) +- [x] **AUTH-03**: Ktor server validates incoming access tokens via Authentik's JWKS endpoint (issuer, audience, expiry, signature, clock skew leeway) - [ ] **AUTH-04**: User session persists across app launches without re-authentication (token refresh handled transparently) - [ ] **AUTH-05**: User can sign out, which revokes local tokens and returns to the login screen -- [ ] **AUTH-06**: Users are JIT-provisioned in the server database on first successful login (by OIDC `sub` claim) +- [x] **AUTH-06**: Users are JIT-provisioned in the server database on first successful login (by OIDC `sub` claim) ### Household sharing diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index de091b6..9a5b51f 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -77,7 +77,7 @@ Plans: Plans: - [x] 02-01-PLAN.md — Shared auth contracts, dependency aliases, Authentik setup docs, and source audit -- [ ] 02-02-PLAN.md — Server JWT validation, JWKS hardening, JIT users, and `/api/v1/me` +- [x] 02-02-PLAN.md — Server JWT validation, JWKS hardening, JIT users, and `/api/v1/me` - [ ] 02-03-PLAN.md — Common OIDC/store contracts, JVM/Wasm actuals, and store contract test - [ ] 02-04-PLAN.md — Android AppAuth actual, Android secure AuthState store, and manifest callback - [ ] 02-05-PLAN.md — iOS AppAuth actual, iOS Keychain store, URL scheme, Swift callback, and Podfile @@ -222,7 +222,7 @@ Plans: | Phase | Plans Complete | Status | Completed | |-------|----------------|--------|-----------| | 1. Project Infrastructure & Module Wiring | 7/7 | Complete | 2026-04-24 | -| 2. Authentication Foundation | 0/7 | Planned | - | +| 2. Authentication Foundation | 2/7 | Executing | - | | 3. Households, Membership & Server Data Foundation | 0/0 | Not started | - | | 4. Sync Engine Skeleton | 0/0 | Not started | - | | 5. Recipe Catalog (Read Path) | 0/0 | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index 6e84007..ca909a4 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -2,15 +2,15 @@ gsd_state_version: 1.0 milestone: v1.0 milestone_name: milestone -current_plan: 0 -status: planned -last_updated: "2026-04-28T08:30:48.000Z" +current_plan: 2 +status: executing +last_updated: "2026-04-28T11:44:38.794Z" progress: total_phases: 11 completed_phases: 1 total_plans: 14 - completed_plans: 7 - percent: 50 + completed_plans: 9 + percent: 64 --- # Project State: Recipe @@ -25,13 +25,13 @@ progress: ## Current Position -Phase: 02 — Authentication Foundation — PLANNED -Plan: 0 of 7 -**Current focus:** Phase 2 ready for execution -**Current plan:** 0 -**Status:** Phase 2 planning complete; ready to execute Phase 2 -**Phase progress:** 1 / 11 phases complete -**Progress bar:** `██░░░░░░░░░░░░░░░░░░` 9% +Phase: 02 (authentication-foundation) — EXECUTING +Plan: 2 of 7 +**Current focus:** Phase 02 — authentication-foundation +**Current plan:** 2 +**Status:** Ready to execute +**Phase progress:** 2 / 7 plans complete +**Progress bar:** `[██████░░░░] 64%` ## Performance Metrics @@ -41,7 +41,8 @@ Plan: 0 of 7 | v1 requirements | 72 | | Coverage | 100% | | Phases complete | 1 | -| Plans complete | 7 | +| Plans complete | 9 | +| Phase 02 P02 | 13min | 3 tasks | 14 files | ## Accumulated Context @@ -59,7 +60,7 @@ All locked tech-stack decisions are captured in `.planning/PROJECT.md § Key Dec ## Session Continuity -**Last session:** --stopped-at +**Last session:** 2026-04-28T11:44:38.789Z **Next action:** `/gsd-execute-phase 2` — Authentication Foundation. diff --git a/.planning/phases/02-authentication-foundation/02-02-SUMMARY.md b/.planning/phases/02-authentication-foundation/02-02-SUMMARY.md new file mode 100644 index 0000000..51e4d20 --- /dev/null +++ b/.planning/phases/02-authentication-foundation/02-02-SUMMARY.md @@ -0,0 +1,169 @@ +--- +phase: 02-authentication-foundation +plan: 02 +subsystem: auth +tags: [ktor, jwt, authentik, jwks, postgres, flyway, exposed, testcontainers] + +# Dependency graph +requires: + - phase: 02-01 + provides: shared auth DTOs, dependency aliases, and Authentik setup context +provides: + - Authentik-style JWT validation with issuer, audience, expiry, RS256 signature, JWKS caching, and non-empty sub enforcement + - Flyway users table migration keyed by OIDC sub + - Exposed DSL JIT user upsert and protected GET /api/v1/me route + - Server auth integration tests for JWT rejection and user provisioning +affects: [phase-03-households, server-auth, principal-resolution, api-v1] + +# Tech tracking +tech-stack: + added: [ktor-server-auth-jwt, jwks-rsa, hikari, testcontainers-postgresql] + patterns: [Ktor jwt("authentik") provider, cached/rate-limited JWKS provider, newSuspendedTransaction for route DB work, Postgres ON CONFLICT upsert] + +key-files: + created: + - server/src/main/resources/db/migration/V1__users.sql + - server/src/main/kotlin/dev/ulfrx/recipe/auth/AuthConfig.kt + - server/src/main/kotlin/dev/ulfrx/recipe/auth/AuthPlugin.kt + - server/src/main/kotlin/dev/ulfrx/recipe/auth/UsersTable.kt + - server/src/main/kotlin/dev/ulfrx/recipe/auth/PrincipalResolver.kt + - server/src/main/kotlin/dev/ulfrx/recipe/auth/MeRoute.kt + - server/src/test/kotlin/dev/ulfrx/recipe/auth/JwtTestSupport.kt + - server/src/test/kotlin/dev/ulfrx/recipe/auth/AuthJwtTest.kt + - server/src/test/kotlin/dev/ulfrx/recipe/auth/MeRouteTest.kt + modified: + - server/src/main/resources/application.conf + - server/src/main/kotlin/dev/ulfrx/recipe/Database.kt + - server/src/main/kotlin/dev/ulfrx/recipe/Application.kt + - server/src/test/kotlin/dev/ulfrx/recipe/ApplicationTest.kt + +key-decisions: + - "Pinned Exposed runtime is 0.55.0; the suspend transaction import used is org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction." + - "PrincipalResolver uses Postgres INSERT ... ON CONFLICT ... RETURNING via Exposed exec because the resolver must atomically upsert and return the generated user id." + - "CallLogging uses a custom method/path/status format and omits all headers because Ktor 3.4.1 server CallLogging has no redactHeader API." + +patterns-established: + - "Protected server routes sit inside authenticate(\"authentik\") and resolve JWTPrincipal through PrincipalResolver before returning user data." + - "Server-side user identity is derived only from JWT claims, never request bodies." + - "Server auth tests use in-process RSA/JWKS support for JWT verifier coverage and Testcontainers Postgres for JIT provisioning coverage." + +requirements-completed: [AUTH-03, AUTH-06] + +# Metrics +duration: 13min +completed: 2026-04-28 +--- + +# Phase 02 Plan 02: Server JWT Validation and JIT Users Summary + +**Ktor Authentik JWT validation with cached JWKS, atomic Postgres user provisioning by OIDC sub, and protected `/api/v1/me`.** + +## Performance + +- **Duration:** 13 min for final executor verification and summary; task commits already existed on this branch when this executor resumed. +- **Started:** 2026-04-28T11:18:15Z +- **Completed:** 2026-04-28T11:31:08Z +- **Tasks:** 3 completed +- **Files modified:** 13 code/config/test files plus this summary + +## Accomplishments + +- Added JWT validation coverage for missing, expired, wrong-issuer, wrong-audience, blank-sub, and valid RS256 tokens. +- Installed Ktor `jwt("authentik")` with issuer/audience checks, 30-second max leeway, non-empty `sub`, cached JWKS, and rate limiting. +- Added `users` Flyway migration, Exposed table mapping, Hikari-backed Exposed connection, atomic JIT upsert by `sub`, and protected `/api/v1/me`. +- Added Testcontainers Postgres integration coverage proving first request creates a user row and later requests update mutable claims without duplication. + +## Task Commits + +Each task was committed atomically: + +1. **Task 1: Create JWT validation tests before auth implementation** - `614b57c` (`test`) +2. **Task 2: Implement AuthConfig, JWT plugin, logging redaction, and verify Exposed suspend API** - `36c1b2c` (`feat`) +3. **Task 3: Add users migration, Exposed resolver, /api/v1/me route, and JIT tests** - `8cf112a` (`feat`) + +No tracked file deletions were present in the task commits. + +## Files Created/Modified + +- `server/src/main/resources/application.conf` - Adds OIDC issuer/audience/JWKS/leeway config with env overrides. +- `server/src/main/resources/db/migration/V1__users.sql` - Creates the `users` table and `users_sub_idx`. +- `server/src/main/kotlin/dev/ulfrx/recipe/Database.kt` - Adds Hikari-backed Exposed connection after Flyway migration. +- `server/src/main/kotlin/dev/ulfrx/recipe/Application.kt` - Installs safe CallLogging, authentication, DB migration/connection, and auth route wiring. +- `server/src/main/kotlin/dev/ulfrx/recipe/auth/AuthConfig.kt` - Reads server OIDC config from HOCON. +- `server/src/main/kotlin/dev/ulfrx/recipe/auth/AuthPlugin.kt` - Installs the Authentik JWT verifier. +- `server/src/main/kotlin/dev/ulfrx/recipe/auth/UsersTable.kt` - Exposed DSL mapping for `users`. +- `server/src/main/kotlin/dev/ulfrx/recipe/auth/PrincipalResolver.kt` - Resolves `JWTPrincipal` to `MeResponse` through atomic upsert. +- `server/src/main/kotlin/dev/ulfrx/recipe/auth/MeRoute.kt` - Provides protected `GET /api/v1/me`. +- `server/src/test/kotlin/dev/ulfrx/recipe/auth/JwtTestSupport.kt` - Generates RSA keys, JWKS provider, and configurable RS256 JWTs for tests. +- `server/src/test/kotlin/dev/ulfrx/recipe/auth/AuthJwtTest.kt` - Covers JWT validation positive and negative cases. +- `server/src/test/kotlin/dev/ulfrx/recipe/auth/MeRouteTest.kt` - Covers JIT provisioning against Testcontainers Postgres. +- `server/src/test/kotlin/dev/ulfrx/recipe/ApplicationTest.kt` - Keeps `/health` test wiring compatible with authenticated route registration. + +## Decisions Made + +- Used `newSuspendedTransaction` from `org.jetbrains.exposed.sql.transactions.experimental` after confirming `org.jetbrains.exposed:exposed-jdbc:0.55.0`. +- Used raw SQL through Exposed `exec` for `INSERT ... ON CONFLICT ... RETURNING`, because the resolver needs the returned row and generated UUID in one atomic operation. +- Kept logging to method, path, and status only; no header logging or bearer-token redaction API is used. + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 1 - Bug] Kept `/health` test route registration compatible with authenticated routes** +- **Found during:** Task 3 +- **Issue:** Once `configureRouting()` registered `meRoute`, tests that installed routing without Authentication would fail route setup. +- **Fix:** Updated `ApplicationTest` to install the test JWT authentication plugin before calling `configureRouting()`. +- **Files modified:** `server/src/test/kotlin/dev/ulfrx/recipe/ApplicationTest.kt` +- **Verification:** `./gradlew :server:test` +- **Committed in:** `8cf112a` + +**2. [Rule 3 - Blocking] Used Exposed `StatementType.SELECT` for Postgres upsert returning rows** +- **Found during:** Task 3 +- **Issue:** `INSERT ... RETURNING` must be executed as a result-producing statement; otherwise Postgres reports that a result was returned when none was expected. +- **Fix:** Added `explicitStatementType = StatementType.SELECT` to the Exposed `exec` call. +- **Files modified:** `server/src/main/kotlin/dev/ulfrx/recipe/auth/PrincipalResolver.kt` +- **Verification:** `./gradlew :server:test --tests "*MeRouteTest*" --tests "*AuthJwtTest*"` +- **Committed in:** `8cf112a` + +--- + +**Total deviations:** 2 auto-fixed (1 bug, 1 blocking issue) +**Impact on plan:** Both fixes were required for the planned tests and route behavior. No extra feature scope was added. + +## Issues Encountered + +- Testcontainers Postgres made the first filtered test run take several minutes while the container image/runtime initialized. Subsequent server test runs completed from cache. + +## Authentication Gates + +None. + +## Known Stubs + +None. + +## User Setup Required + +None for this plan. Real Authentik provider setup remains covered by the Phase 2 setup documentation from plan `02-01`. + +## Verification + +- `./gradlew :server:dependencyInsight --dependency exposed-jdbc --configuration runtimeClasspath` - passed; Exposed JDBC version is `0.55.0`. +- `./gradlew :server:test --tests "*AuthJwtTest*" --tests "*MeRouteTest*"` - passed. +- `./gradlew :server:test` - passed. +- Task acceptance greps for OIDC config, JWT verifier settings, logging safety, migration shape, no DAO imports, no blocking `transaction {}` in auth code, `/api/v1/me`, Testcontainers, `postgres:16`, and Flyway all passed. + +## Next Phase Readiness + +Phase 3 can extend `PrincipalResolver` from user identity to household-scoped principal resolution. The server now has the stable `users.sub` anchor and `/api/v1/me` boundary that Phase 3 onboarding and household membership can build on. + +## Self-Check: PASSED + +- Created/modified key files exist. +- Task commits found: `614b57c`, `36c1b2c`, `8cf112a`. +- Required verification commands passed. +- No unplanned tracked file deletions were detected in task commits. + +--- +*Phase: 02-authentication-foundation* +*Completed: 2026-04-28*