---
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"
---

<objective>
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`.
</objective>

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

<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
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Add shared DTO/config contract and serialization test</name>
  <read_first>
    - 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)
  </read_first>
  <files>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</files>
  <behavior>
    - `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.
  </behavior>
  <action>
    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`.
  </action>
  <verify>
    <automated>./gradlew :shared:jvmTest :shared:compileCommonMainKotlinMetadata</automated>
  </verify>
  <acceptance_criteria>
    - `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
  </acceptance_criteria>
  <done>Shared config and DTO contract exists and is tested without violating shared module purity.</done>
</task>

<task type="auto">
  <name>Task 2: Add Phase 2 dependency aliases without Ktor patch bump</name>
  <read_first>
    - gradle/libs.versions.toml
    - composeApp/build.gradle.kts
    - server/build.gradle.kts
    - .planning/phases/02-authentication-foundation/02-RESEARCH.md (Standard Stack, Open Questions)
  </read_first>
  <files>gradle/libs.versions.toml, composeApp/build.gradle.kts, server/build.gradle.kts</files>
  <action>
    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.
  </action>
  <verify>
    <automated>./gradlew :composeApp:dependencies --configuration androidMainCompileClasspath :server:dependencies --configuration runtimeClasspath</automated>
  </verify>
  <acceptance_criteria>
    - `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
  </acceptance_criteria>
  <done>Phase 2 dependencies are cataloged and wired while preserving the pinned Ktor version.</done>
</task>

<task type="auto">
  <name>Task 3: Document Authentik provider setup and source audit</name>
  <read_first>
    - .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
  </read_first>
  <files>docs/authentik-setup.md</files>
  <action>
    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.
  </action>
  <verify>
    <automated>grep -E 'openid profile email offline_access|PKCE S256|single-string|recipe://callback|/api/v1/me|Source Audit' docs/authentik-setup.md</automated>
  </verify>
  <acceptance_criteria>
    - `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`
  </acceptance_criteria>
  <done>Authentik setup and multi-source audit are reproducible and trace every locked requirement/decision.</done>
</task>

</tasks>

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

<verification>
Run `./gradlew :shared:jvmTest :shared:compileCommonMainKotlinMetadata`, `./tools/verify-shared-pure.sh`, and `./tools/verify-no-version-literals.sh`.
</verification>

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

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