Add authentication

This commit is contained in:
2026-04-27 19:28:57 +02:00
parent 6684b7179d
commit e0af5f4053
92 changed files with 8140 additions and 208 deletions

View File

@@ -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*