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

13 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, user_setup, must_haves
phase plan type wave depends_on files_modified autonomous requirements user_setup must_haves
02-authentication-foundation 01 execute 1
gradle/libs.versions.toml
shared/build.gradle.kts
shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/Constants.kt
shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/dto/User.kt
shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/dto/MeResponse.kt
shared/src/commonTest/kotlin/dev/ulfrx/recipe/shared/dto/MeResponseSerializationTest.kt
composeApp/build.gradle.kts
server/build.gradle.kts
docs/authentik-setup.md
true
AUTH-01
AUTH-02
AUTH-03
AUTH-04
AUTH-05
AUTH-06
service why env_vars dashboard_config
authentik OIDC provider for mobile login and server JWT validation
name source
OIDC_ISSUER Authentik provider issuer URL
name source
OIDC_AUDIENCE Authentik OAuth2 provider client ID
name source
OIDC_JWKS_URL Optional JWKS URI from Authentik OpenID configuration
task location
Create public OAuth2/OIDC provider with PKCE S256, redirect URI recipe://callback, scopes openid profile email offline_access, RS256 signing, single-string audience equal to client_id Authentik Admin -> Applications -> Providers
truths artifacts key_links
All Phase 2 plans compile against one shared OIDC config and one /api/v1/me DTO contract
Authentik provider setup documents public client + PKCE S256, scopes openid profile email offline_access, RS256, single-string audience, JWKS, and end-session
Android secure token storage is explicit: auth code must not use no-arg Settings() for tokens
path provides contains
shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/Constants.kt OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_REDIRECT_URI, API_BASE_URL per D-11 OIDC_REDIRECT_URI
path provides contains
shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/dto/MeResponse.kt Serializable /api/v1/me response per D-27 @Serializable
path provides contains
docs/authentik-setup.md Provider scope mapping and manual UAT checklist per D-10 offline_access
from to via pattern
shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/Constants.kt docs/authentik-setup.md same issuer/client/redirect values recipe://callback
from to via pattern
gradle/libs.versions.toml composeApp/build.gradle.kts and server/build.gradle.kts catalog aliases only; no version literals in module build files ktor-serverAuthJwt|appauth|androidx-security-crypto
Create the shared contract and dependency foundation for Authentication Foundation.

Purpose: every downstream plan needs the same DTOs, dependency aliases, and Authentik provider contract before implementation starts. Output: shared DTO/config files, build dependency wiring, and docs/authentik-setup.md.

<execution_context> @/Users/rwilk/.codex/get-shit-done/workflows/execute-plan.md @/Users/rwilk/.codex/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/REQUIREMENTS.md @.planning/phases/02-authentication-foundation/02-CONTEXT.md @.planning/phases/02-authentication-foundation/02-RESEARCH.md @.planning/phases/02-authentication-foundation/02-VALIDATION.md @.planning/phases/02-authentication-foundation/02-PATTERNS.md @AGENTS.md Task 1: Add shared DTO/config contract and serialization test - shared/build.gradle.kts - shared/src/commonMain/kotlin/dev/ulfrx/recipe/Constants.kt - shared/src/commonTest/kotlin/dev/ulfrx/recipe/SharedCommonTest.kt - .planning/phases/02-authentication-foundation/02-CONTEXT.md (D-11, D-27, D-28) shared/build.gradle.kts, shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/Constants.kt, shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/dto/User.kt, shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/dto/MeResponse.kt, shared/src/commonTest/kotlin/dev/ulfrx/recipe/shared/dto/MeResponseSerializationTest.kt - `Constants.OIDC_REDIRECT_URI` equals exactly `recipe://callback` per D-09. - `Constants.OIDC_ISSUER` ends with `/`; use placeholder `https://auth.example.invalid/application/o/recipe/` until real homelab value is substituted. - `Constants.OIDC_CLIENT_ID` equals `recipe-app`. - `MeResponse` serializes fields `id`, `sub`, `email`, `displayName`, and maps to `User`. - `shared/commonMain` imports only allowed dependencies: Kotlin stdlib and kotlinx.serialization. Apply `alias(libs.plugins.kotlinSerialization)` to `shared/build.gradle.kts`; add `api(libs.kotlinx.serializationJson)` in `commonMain.dependencies`.
Add `dev.ulfrx.recipe.shared.Constants` as a public object with `OIDC_ISSUER`, `OIDC_CLIENT_ID`, `OIDC_REDIRECT_URI`, and `API_BASE_URL`. Add `dev.ulfrx.recipe.shared.dto.User` and `MeResponse` as public `@Serializable` data classes using `String` for the server UUID, with `MeResponse.toUser()`.

Create `MeResponseSerializationTest` covering round trip, `displayName` wire name, and `ignoreUnknownKeys` compatibility with future `householdId`.
./gradlew :shared:jvmTest :shared:compileCommonMainKotlinMetadata - `grep -q 'OIDC_REDIRECT_URI: String = "recipe://callback"' shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/Constants.kt` - `grep -q '@Serializable' shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/dto/MeResponse.kt` - `grep -q 'public fun toUser()' shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/dto/MeResponse.kt` - `./tools/verify-shared-pure.sh` exits 0 - `./gradlew :shared:jvmTest :shared:compileCommonMainKotlinMetadata` exits 0 Shared config and DTO contract exists and is tested without violating shared module purity. Task 2: Add Phase 2 dependency aliases without Ktor patch bump - gradle/libs.versions.toml - composeApp/build.gradle.kts - server/build.gradle.kts - .planning/phases/02-authentication-foundation/02-RESEARCH.md (Standard Stack, Open Questions) 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"`, `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`, 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 = 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, 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. Task 3: Document Authentik provider setup and source audit - .planning/phases/02-authentication-foundation/02-CONTEXT.md (D-05 through D-10, D-19, D-21 through D-23) - .planning/phases/02-authentication-foundation/02-VALIDATION.md - .planning/ROADMAP.md Phase 2 success criteria docs/authentik-setup.md Create `docs/authentik-setup.md` with these exact sections: `## Provider`, `## Scopes`, `## Redirect URI`, `## Server Env Vars`, `## Logout`, `## Manual UAT`, `## Source Audit`.
Provider section must specify: OAuth2/OIDC public client, authorization code with PKCE S256, no client secret in the app, redirect URI `recipe://callback`, RS256 signing, single-string `aud` equal to `recipe-app`, JWKS URI from the provider's OpenID configuration, and end-session endpoint.

Scopes section must state the app requests exactly `openid profile email offline_access` and that Authentik must map/allow `offline_access` for refresh tokens. Manual UAT must cover fresh iOS login, reopen/refresh after access-token expiry, logout returning to login, and curl/HTTP verification of `/api/v1/me` returning 200 with valid token and 401 without/wrong-audience token.

Source Audit must mark all Phase 2 sources covered: GOAL Phase 2, REQ AUTH-01..AUTH-06, RESEARCH constraints, CONTEXT D-01..D-34, UI-SPEC auth screens, VALIDATION Wave 0 tests, PATTERNS file map. Deferred ideas must be listed as excluded: Universal Links/App Links, real Desktop OIDC, Wasm OIDC, Apple Sign-in, Authentik automation.
grep -E 'openid profile email offline_access|PKCE S256|single-string|recipe://callback|/api/v1/me|Source Audit' docs/authentik-setup.md - `grep -q 'openid profile email offline_access' docs/authentik-setup.md` - `grep -q 'offline_access.*refresh' docs/authentik-setup.md` - `grep -q 'single-string.*aud' docs/authentik-setup.md` - `grep -q 'AUTH-01.*AUTH-02.*AUTH-03.*AUTH-04.*AUTH-05.*AUTH-06' docs/authentik-setup.md` - `grep -q 'Universal Links / App Links.*excluded' docs/authentik-setup.md` Authentik setup and multi-source audit are reproducible and trace every locked requirement/decision.

<threat_model>

Trust Boundaries

Boundary Description
app -> Authentik Mobile app launches system browser and receives authorization callback through custom URL scheme
app -> OS secure storage Refresh tokens cross from process memory to persistent device storage
client -> server Bearer access tokens cross HTTP boundary

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-02-01-01 Spoofing/Elevation OIDC provider setup mitigate Document public client + PKCE S256 + AppAuth state handling + exact recipe://callback registration
T-02-01-02 Information Disclosure token storage dependencies mitigate Explicit AndroidX Security Crypto and iOS Keychain store plan; forbid no-arg Settings() for auth tokens
T-02-01-03 Elevation JWT audience config mitigate Document single-string aud equal to recipe-app; server tests in Plan 02 enforce wrong audience 401
T-02-01-04 Information Disclosure logs/docs mitigate Docs state never log Authorization or token bodies; server/client implementation plans include redaction
</threat_model>
Run `./gradlew :shared:jvmTest :shared:compileCommonMainKotlinMetadata`, `./tools/verify-shared-pure.sh`, and `./tools/verify-no-version-literals.sh`.

<success_criteria> Downstream server, client, and UI plans have stable imports/config, Authentik setup is documented, Ktor remains at 3.4.1, and Android token security is explicit. </success_criteria>

After completion, create `.planning/phases/02-authentication-foundation/02-01-SUMMARY.md`.