Files
recipe/.planning/phases/02-authentication-foundation/02-02-SUMMARY.md
2026-04-29 20:54:13 +02:00

9.0 KiB

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, duration, completed
phase plan subsystem tags requires provides affects tech-stack key-files key-decisions patterns-established requirements-completed duration completed
02-authentication-foundation 02 auth
ktor
jwt
authentik
jwks
postgres
flyway
exposed
testcontainers
phase provides
02-01 shared auth DTOs, dependency aliases, and Authentik setup context
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
phase-03-households
server-auth
principal-resolution
api-v1
added patterns
ktor-server-auth-jwt
jwks-rsa
hikari
testcontainers-postgresql
Ktor jwt("authentik") provider
cached/rate-limited JWKS provider
newSuspendedTransaction for route DB work
Postgres ON CONFLICT upsert
created modified
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
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
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.
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.
AUTH-03
AUTH-06
13min 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