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

12 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
02-authentication-foundation 03 execute 2
02-01
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.kt
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/OidcResult.kt
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.kt
composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.jvm.kt
composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.jvm.kt
composeApp/src/webMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.wasmJs.kt
composeApp/src/webMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.wasmJs.kt
composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStoreContractTest.kt
true
AUTH-01
AUTH-02
AUTH-04
AUTH-05
truths artifacts key_links
Common auth code compiles against one expect OidcClient seam with login, refresh, and logout
Requested native OIDC scopes are documented in the common contract as exactly openid profile email offline_access
Every configured non-mobile target has actuals so JVM and Wasm builds compile
JVM target uses explicit DEV_AUTH_TOKEN dev behavior and does not hardcode a usable bearer token
Wasm target preserves the v2 boundary with NotImplementedError("Wasm OIDC: v2")
SecureAuthStateStore read/write/clear semantics are locked by a common contract test
path provides contains
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.kt expect OIDC seam with suspend login/refresh/logout per D-01..D-04 expect class OidcClient
path provides contains
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/OidcResult.kt common OIDC result model consumed by AuthSession and LoginViewModel sealed
path provides contains
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.kt expect secure AuthState JSON store per D-13..D-15 expect class SecureAuthStateStore
path provides contains
composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.jvm.kt JVM dev-only token stub per D-02 DEV_AUTH_TOKEN
path provides contains
composeApp/src/webMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.wasmJs.kt Wasm v2 stub per D-03 NotImplementedError("Wasm OIDC: v2")
from to via pattern
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.kt composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.jvm.kt actual class implements common suspend login/refresh/logout contract actual class OidcClient
from to via pattern
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.kt composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStoreContractTest.kt contract test validates read/write/clear behavior without platform secure storage read.*write.*clear
Define the common OIDC and AuthState storage contracts, plus JVM/Wasm actuals that keep secondary targets compiling.

Purpose: downstream mobile plans implement platform AppAuth behind a stable seam, while AuthSession can be built without platform-specific APIs. Output: common auth contracts, JVM dev actuals, Wasm v2 stubs, and a common store contract test.

<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/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 @.planning/phases/02-authentication-foundation/02-01-SUMMARY.md @AGENTS.md @composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt @composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt Task 1: Define common OIDC and secure store contracts - shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/Constants.kt - .planning/phases/02-authentication-foundation/02-CONTEXT.md (D-01..D-06, D-13..D-20) - .planning/phases/02-authentication-foundation/02-RESEARCH.md (Pitfall 1 and secure storage recommendation) composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.kt, composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/OidcResult.kt, composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.kt, composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStoreContractTest.kt - `OidcResult.Success` carries `authStateJson`, `accessToken`, nullable `idToken`, and `expiresAtEpochMillis`. - `OidcClient` exposes suspend `login()`, `refresh(authStateJson)`, and `logout(authStateJson)`. - Common contract text states native actuals use AppAuth and request exactly `openid profile email offline_access` per D-01/D-06. - `SecureAuthStateStore` exposes `read()`, `write(authStateJson)`, and `clear()`. - Contract test proves write overwrites previous value, read returns latest value, and clear removes it. Create `OidcResult` as a sealed interface or sealed class with `Success(authStateJson: String, accessToken: String, idToken: String?, expiresAtEpochMillis: Long)`, `Cancelled`, `NetworkError`, and `AuthError(message: String, cause: Throwable? = null)`.
Create `expect class OidcClient` with `suspend fun login(): OidcResult`, `suspend fun refresh(authStateJson: String): OidcResult`, and `suspend fun logout(authStateJson: String): Unit`. The common KDoc must pin D-01, D-04, D-06, D-16, D-19, and D-20: native implementations use AppAuth, bridge callbacks with `suspendCancellableCoroutine`, request exactly `openid profile email offline_access`, refresh through AppAuth fresh-token APIs, and logout through RP-initiated end-session before local clear.

Create `expect class SecureAuthStateStore` with `fun read(): String?`, `fun write(authStateJson: String)`, and `fun clear()`. The KDoc must state it persists the full AppAuth AuthState JSON blob per D-13 and must not use no-arg insecure settings for tokens.

Add `SecureAuthStateStoreContractTest` using a fake in-memory implementation in commonTest to lock the store behavior. Keep this test platform-free; Android and iOS secure implementations are created in Plans 02-04 and 02-05.
./gradlew :composeApp:jvmTest - `grep -q 'expect class OidcClient' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.kt` - `grep -q 'openid profile email offline_access' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.kt` - `grep -q 'expect class SecureAuthStateStore' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.kt` - `grep -q 'AuthState JSON' composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.kt` - `grep -q 'clear' composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStoreContractTest.kt` - `./gradlew :composeApp:jvmTest` exits 0 Common auth seams exist with exact scope/logout/storage semantics and testable store behavior. Task 2: Add JVM and Wasm actuals - .planning/phases/02-authentication-foundation/02-CONTEXT.md (D-02, D-03) - composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt - composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.jvm.kt, composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.jvm.kt, composeApp/src/webMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.wasmJs.kt, composeApp/src/webMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.wasmJs.kt JVM actual reads `DEV_AUTH_TOKEN` from the environment and returns `OidcResult.Success(authStateJson = "dev:$token", accessToken = token, idToken = null, expiresAtEpochMillis = Long.MAX_VALUE)` when present. If missing, return `OidcResult.AuthError("DEV_AUTH_TOKEN is not set")`; do not hardcode a usable bearer token.
JVM `SecureAuthStateStore` actual must compile for desktop dev/tests without pretending to be production secure storage. Implement `actual class SecureAuthStateStore` with a private nullable in-memory `authStateJson` property and exact methods `read()`, `write(authStateJson: String)`, and `clear()`.

Wasm `OidcClient` actual throws exactly `NotImplementedError("Wasm OIDC: v2")` for login, refresh, and logout per D-03. Wasm `SecureAuthStateStore` actual must also exist so `:composeApp:compileKotlinWasmJs` compiles; implement the same non-persistent in-memory store shape used by JVM.
./gradlew :composeApp:jvmTest :composeApp:compileKotlinWasmJs - `grep -q 'DEV_AUTH_TOKEN' composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.jvm.kt` - `grep -q 'actual class SecureAuthStateStore' composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.jvm.kt` - `grep -q 'NotImplementedError("Wasm OIDC: v2")' composeApp/src/webMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.wasmJs.kt` - `grep -q 'actual class SecureAuthStateStore' composeApp/src/webMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.wasmJs.kt` - `./gradlew :composeApp:jvmTest :composeApp:compileKotlinWasmJs` exits 0 Secondary targets compile without expanding Phase 2 into real Desktop or Wasm OIDC.

<threat_model>

Trust Boundaries

Boundary Description
common auth contract -> platform actuals Common AuthSession code delegates browser/token behavior to target-specific implementations
app process -> dev environment JVM dev stub reads bearer token from DEV_AUTH_TOKEN
app process -> non-persistent stubs JVM/Wasm stores satisfy contracts without claiming production secure storage

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-02-03-01 Spoofing/Elevation OidcClient contract mitigate Common KDoc pins AppAuth, PKCE-compatible native flow, exact scopes, state/nonce ownership, and RP-initiated logout semantics for platform plans
T-02-03-02 Information Disclosure SecureAuthStateStore contract mitigate Contract states full AuthState JSON must use explicit secure platform storage; Android/iOS plans implement the secure actuals
T-02-03-03 Spoofing JVM dev stub accept Desktop is dev tool only; stub requires explicit DEV_AUTH_TOKEN and never hardcodes a usable bearer token
T-02-03-04 Scope Creep Wasm OIDC accept Wasm actual throws NotImplementedError("Wasm OIDC: v2") per D-03 and does not implement browser OIDC in Phase 2
</threat_model>
Run `./gradlew :composeApp:jvmTest :composeApp:compileKotlinWasmJs`.

<success_criteria> Common OIDC/storage contracts exist below the file-count threshold, JVM/Wasm targets compile, and downstream Android/iOS/AuthSession plans can depend on stable auth seams. </success_criteria>

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