Add authentication

This commit is contained in:
2026-04-27 19:28:57 +02:00
parent 6684b7179d
commit e0af5f4053
92 changed files with 8140 additions and 208 deletions

View File

@@ -0,0 +1,220 @@
---
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>