--- phase: 02-authentication-foundation plan: 01 type: execute wave: 1 depends_on: [] files_modified: - 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 autonomous: true requirements: [AUTH-01, AUTH-02, AUTH-03, AUTH-04, AUTH-05, AUTH-06] user_setup: - service: authentik why: "OIDC provider for mobile login and server JWT validation" env_vars: - name: OIDC_ISSUER source: "Authentik provider issuer URL" - name: OIDC_AUDIENCE source: "Authentik OAuth2 provider client ID" - name: OIDC_JWKS_URL source: "Optional JWKS URI from Authentik OpenID configuration" dashboard_config: - task: "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" location: "Authentik Admin -> Applications -> Providers" must_haves: truths: - "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" artifacts: - path: "shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/Constants.kt" provides: "OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_REDIRECT_URI, API_BASE_URL per D-11" contains: "OIDC_REDIRECT_URI" - path: "shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/dto/MeResponse.kt" provides: "Serializable /api/v1/me response per D-27" contains: "@Serializable" - path: "docs/authentik-setup.md" provides: "Provider scope mapping and manual UAT checklist per D-10" contains: "offline_access" key_links: - from: "shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/Constants.kt" to: "docs/authentik-setup.md" via: "same issuer/client/redirect values" pattern: "recipe://callback" - from: "gradle/libs.versions.toml" to: "composeApp/build.gradle.kts and server/build.gradle.kts" via: "catalog aliases only; no version literals in module build files" pattern: "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`. @/Users/rwilk/.codex/get-shit-done/workflows/execute-plan.md @/Users/rwilk/.codex/get-shit-done/templates/summary.md @.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"`, `androidx-security-crypto = "1.1.0"`, `multiplatformSettings = "1.3.0"`, `exposed = "0.55.0"`, `hikari = "6.2.1"`, 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`. 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 `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. ./gradlew :composeApp:dependencies --configuration androidMainCompileClasspath :server:dependencies --configuration runtimeClasspath - `grep -q 'ktor = "3.4.1"' gradle/libs.versions.toml` - `grep -q 'androidx-security-crypto' gradle/libs.versions.toml` - `grep -q 'pod("AppAuth")' 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` - `./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. ## 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 | Run `./gradlew :shared:jvmTest :shared:compileCommonMainKotlinMetadata`, `./tools/verify-shared-pure.sh`, and `./tools/verify-no-version-literals.sh`. 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. After completion, create `.planning/phases/02-authentication-foundation/02-01-SUMMARY.md`.