diff --git a/.planning/phases/02-authentication-foundation/02-01-PLAN.md b/.planning/phases/02-authentication-foundation/02-01-PLAN.md index 214f328..51acfb6 100644 --- a/.planning/phases/02-authentication-foundation/02-01-PLAN.md +++ b/.planning/phases/02-authentication-foundation/02-01-PLAN.md @@ -127,24 +127,29 @@ Output: shared DTO/config files, build dependency wiring, and `docs/authentik-se gradle/libs.versions.toml, composeApp/build.gradle.kts, server/build.gradle.kts In `gradle/libs.versions.toml`, keep existing `ktor = "3.4.1"` unchanged. Add version keys and aliases for: - `appauth = "0.11.1"`, `androidx-security-crypto = "1.1.0"`, `multiplatformSettings = "1.3.0"`, `exposed = "0.55.0"`, `hikari = "6.2.1"`, plus `kotlinCocoapods` plugin. + `appauth = "0.11.1"`, `appauth-ios = "2.0.0"`, `androidx-security-crypto = "1.1.0"`, `multiplatformSettings = "1.3.0"`, `exposed = "0.55.0"`, `hikari = "6.2.1"`, `testcontainers = "1.21.4"`, plus `kotlinCocoapods` plugin. - Add libraries: `appauth`, `androidx-security-crypto`, `multiplatform-settings`, `multiplatform-settings-coroutines`, Ktor client core/auth/content-negotiation/logging/okhttp/darwin/cio, `ktor-serializationKotlinxJsonMpp` using `io.ktor:ktor-serialization-kotlinx-json`, Ktor server auth/auth-jwt/call-logging/status-pages, Exposed core/jdbc/java-time, Hikari, `kotlinx-serializationJson`. + Add libraries: `appauth`, `androidx-security-crypto`, `multiplatform-settings`, `multiplatform-settings-coroutines`, Ktor client core/auth/content-negotiation/logging/okhttp/darwin/cio, `ktor-serializationKotlinxJsonMpp` using `io.ktor:ktor-serialization-kotlinx-json`, Ktor server auth/auth-jwt/call-logging/status-pages, Exposed core/jdbc/java-time, Hikari, `kotlinx-serializationJson`, and Testcontainers `org.testcontainers:postgresql` + `org.testcontainers:junit-jupiter`. - In `composeApp/build.gradle.kts`, apply `alias(libs.plugins.kotlinSerialization)` and `alias(libs.plugins.kotlinCocoapods)`. Add common deps for settings, Ktor client, serialization, and platform deps: Android AppAuth + AndroidX Security Crypto + OkHttp, iOS Darwin, JVM CIO. Configure CocoaPods with `podfile = project.file("../iosApp/Podfile")`, framework `baseName = "ComposeApp"`, `isStatic = true`, and `pod("AppAuth") { version = "2.0.0" }`. + In `composeApp/build.gradle.kts`, apply `alias(libs.plugins.kotlinSerialization)` and `alias(libs.plugins.kotlinCocoapods)`. Add common deps for settings, Ktor client, serialization, and platform deps: Android AppAuth + AndroidX Security Crypto + OkHttp, iOS Darwin, JVM CIO. Configure CocoaPods with `podfile = project.file("../iosApp/Podfile")`, framework `baseName = "ComposeApp"`, `isStatic = true`, and `pod("AppAuth") { version = libs.versions.appauth.ios.get() }`. Do not put a literal `version = "2.0.0"` in any `*.gradle.kts`; the CocoaPods version must come from the version catalog so `./tools/verify-no-version-literals.sh` can pass. - In `server/build.gradle.kts`, add server auth/JWT/call logging/status pages, Exposed, Hikari, and serialization deps from catalog. Do not add inline versions in build files. + In `server/build.gradle.kts`, add server auth/JWT/call logging/status pages, Exposed, Hikari, serialization deps, and test deps `testImplementation(libs.testcontainers.postgresql)` plus `testImplementation(libs.testcontainers.junit.jupiter)` from catalog. Do not add inline versions in build files. ./gradlew :composeApp:dependencies --configuration androidMainCompileClasspath :server:dependencies --configuration runtimeClasspath - `grep -q 'ktor = "3.4.1"' gradle/libs.versions.toml` + - `grep -q 'appauth-ios = "2.0.0"' gradle/libs.versions.toml` - `grep -q 'androidx-security-crypto' gradle/libs.versions.toml` + - `grep -q 'testcontainers = "1.21.4"' gradle/libs.versions.toml` - `grep -q 'pod("AppAuth")' composeApp/build.gradle.kts` + - `grep -q 'libs.versions.appauth.ios.get()' composeApp/build.gradle.kts` + - `! grep -q 'version = "2.0.0"' composeApp/build.gradle.kts` - `grep -q 'libs.androidx.security.crypto' composeApp/build.gradle.kts` - `grep -q 'libs.ktor.serverAuthJwt' server/build.gradle.kts` - `grep -q 'libs.exposed.jdbc' server/build.gradle.kts` + - `grep -q 'libs.testcontainers.postgresql' server/build.gradle.kts` - `./tools/verify-no-version-literals.sh` exits 0 Phase 2 dependencies are cataloged and wired while preserving the pinned Ktor version. diff --git a/.planning/phases/02-authentication-foundation/02-02-PLAN.md b/.planning/phases/02-authentication-foundation/02-02-PLAN.md index de7bb00..95f7b3d 100644 --- a/.planning/phases/02-authentication-foundation/02-02-PLAN.md +++ b/.planning/phases/02-authentication-foundation/02-02-PLAN.md @@ -127,7 +127,7 @@ Output: Flyway users migration, auth plugin, resolver, route, and negative JWT/J Create `AuthConfig.fromApplicationConfig(config)` and `configureAuthentication(authConfig: AuthConfig = AuthConfig.fromApplicationConfig(environment.config))`. `AuthPlugin.kt` must install `jwt("authentik")` using `JwkProviderBuilder(jwksUrl or issuer).cached(10, 15, TimeUnit.MINUTES).rateLimited(10, 1, TimeUnit.MINUTES)`, `.withIssuer(issuer)`, `.withAudience(audience)`, `.acceptLeeway(30)`, and a validate block rejecting null/blank `sub`. - Install `CallLogging` in `Application.module()` and redact `Authorization`. Never log token bodies or raw Authorization headers. + Install `CallLogging` in `Application.module()` using Ktor 3.4.1 APIs only. Configure `format { call -> "${call.request.httpMethod.value} ${call.request.path()} -> ${call.response.status()?.value ?: "-"}" }` so request/response method, path, and status are logged but headers are never included. Do not use `redactHeader(...)`; that API is not available on Ktor server `CallLoggingConfig` in the pinned Ktor 3.4.1 dependency. Never log token bodies or raw Authorization headers. Before implementing Task 3 DB code, verify the pinned Exposed suspend transaction API/import against the dependency used by this repo. Run: `./gradlew :server:dependencyInsight --dependency exposed-jdbc --configuration runtimeClasspath` @@ -142,7 +142,10 @@ Output: Flyway users migration, auth plugin, resolver, route, and negative JWT/J - `grep -q 'withAudience' server/src/main/kotlin/dev/ulfrx/recipe/auth/AuthPlugin.kt` - `grep -q 'acceptLeeway(30' server/src/main/kotlin/dev/ulfrx/recipe/auth/AuthPlugin.kt` - `grep -q 'rateLimited(10, 1, TimeUnit.MINUTES)' server/src/main/kotlin/dev/ulfrx/recipe/auth/AuthPlugin.kt` - - `grep -q 'redactHeader(HttpHeaders.Authorization)' server/src/main/kotlin/dev/ulfrx/recipe/Application.kt` + - `grep -q 'install(CallLogging)' server/src/main/kotlin/dev/ulfrx/recipe/Application.kt` + - `grep -q 'format { call ->' server/src/main/kotlin/dev/ulfrx/recipe/Application.kt` + - `! grep -q 'redactHeader' server/src/main/kotlin/dev/ulfrx/recipe/Application.kt` + - `! grep -q 'HttpHeaders.Authorization' server/src/main/kotlin/dev/ulfrx/recipe/Application.kt` - `./gradlew :server:test --tests "*AuthJwtTest*"` exits 0 Server rejects invalid JWTs, accepts valid Authentik-style JWTs, and redacts Authorization headers. @@ -166,7 +169,7 @@ Output: Flyway users migration, auth plugin, resolver, route, and negative JWT/J Add `meRoute(principalResolver)` under `authenticate("authentik") { get("/api/v1/me") { ... } }`. Wire route from `configureRouting`. - Create `MeRouteTest` for valid token creates row, second token same `sub` updates email/display name without duplicating, and valid response body contains `id`, `sub`, `email`, `displayName`. + Create `MeRouteTest` with an explicit runnable PostgreSQL test database using Testcontainers, not ambient local Postgres. Use `org.testcontainers.containers.PostgreSQLContainer` with image `postgres:16`, start it in the test fixture, set the server/database config to the container JDBC URL, username, and password before installing routes, and stop it after tests. Run Flyway against the container before assertions. Tests must cover: valid token creates row, second token same `sub` updates email/display name without duplicating, and valid response body contains `id`, `sub`, `email`, `displayName`. ./gradlew :server:test --tests "*MeRouteTest*" --tests "*AuthJwtTest*" @@ -178,6 +181,9 @@ Output: Flyway users migration, auth plugin, resolver, route, and negative JWT/J - `! grep -R 'transaction {' server/src/main/kotlin/dev/ulfrx/recipe/auth` - `grep -q 'ON CONFLICT (sub) DO UPDATE' server/src/main/kotlin/dev/ulfrx/recipe/auth/PrincipalResolver.kt` - `grep -q 'get("/api/v1/me")' server/src/main/kotlin/dev/ulfrx/recipe/auth/MeRoute.kt` + - `grep -q 'PostgreSQLContainer' server/src/test/kotlin/dev/ulfrx/recipe/auth/MeRouteTest.kt` + - `grep -q 'postgres:16' server/src/test/kotlin/dev/ulfrx/recipe/auth/MeRouteTest.kt` + - `grep -q 'Flyway' server/src/test/kotlin/dev/ulfrx/recipe/auth/MeRouteTest.kt` - `./gradlew :server:test --tests "*MeRouteTest*" --tests "*AuthJwtTest*"` exits 0 `/api/v1/me` validates tokens, JIT-provisions users atomically, and returns the shared DTO.