Add authentication
This commit is contained in:
177
.planning/phases/02-authentication-foundation/02-03-PLAN.md
Normal file
177
.planning/phases/02-authentication-foundation/02-03-PLAN.md
Normal file
@@ -0,0 +1,177 @@
|
||||
---
|
||||
phase: 02-authentication-foundation
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: [02-01]
|
||||
files_modified:
|
||||
- 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
|
||||
autonomous: true
|
||||
requirements: [AUTH-01, AUTH-02, AUTH-04, AUTH-05]
|
||||
must_haves:
|
||||
truths:
|
||||
- "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"
|
||||
artifacts:
|
||||
- path: "composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.kt"
|
||||
provides: "expect OIDC seam with suspend login/refresh/logout per D-01..D-04"
|
||||
contains: "expect class OidcClient"
|
||||
- path: "composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/OidcResult.kt"
|
||||
provides: "common OIDC result model consumed by AuthSession and LoginViewModel"
|
||||
contains: "sealed"
|
||||
- path: "composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.kt"
|
||||
provides: "expect secure AuthState JSON store per D-13..D-15"
|
||||
contains: "expect class SecureAuthStateStore"
|
||||
- path: "composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.jvm.kt"
|
||||
provides: "JVM dev-only token stub per D-02"
|
||||
contains: "DEV_AUTH_TOKEN"
|
||||
- path: "composeApp/src/webMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.wasmJs.kt"
|
||||
provides: "Wasm v2 stub per D-03"
|
||||
contains: "NotImplementedError(\"Wasm OIDC: v2\")"
|
||||
key_links:
|
||||
- from: "composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.kt"
|
||||
to: "composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.jvm.kt"
|
||||
via: "actual class implements common suspend login/refresh/logout contract"
|
||||
pattern: "actual class OidcClient"
|
||||
- from: "composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.kt"
|
||||
to: "composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStoreContractTest.kt"
|
||||
via: "contract test validates read/write/clear behavior without platform secure storage"
|
||||
pattern: "read.*write.*clear"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</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/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
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Define common OIDC and secure store contracts</name>
|
||||
<read_first>
|
||||
- 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)
|
||||
</read_first>
|
||||
<files>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</files>
|
||||
<behavior>
|
||||
- `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.
|
||||
</behavior>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>./gradlew :composeApp:jvmTest</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `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
|
||||
</acceptance_criteria>
|
||||
<done>Common auth seams exist with exact scope/logout/storage semantics and testable store behavior.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Add JVM and Wasm actuals</name>
|
||||
<read_first>
|
||||
- .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
|
||||
</read_first>
|
||||
<files>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</files>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>./gradlew :composeApp:jvmTest :composeApp:compileKotlinWasmJs</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `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
|
||||
</acceptance_criteria>
|
||||
<done>Secondary targets compile without expanding Phase 2 into real Desktop or Wasm OIDC.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<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>
|
||||
|
||||
<verification>
|
||||
Run `./gradlew :composeApp:jvmTest :composeApp:compileKotlinWasmJs`.
|
||||
</verification>
|
||||
|
||||
<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>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/02-authentication-foundation/02-03-SUMMARY.md`.
|
||||
</output>
|
||||
Reference in New Issue
Block a user