diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index db90a47..3a83155 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -73,7 +73,16 @@ Plans:
3. I tap "Wyloguj się"; the app returns to the login screen and the stored tokens are gone from Keychain/EncryptedSharedPreferences.
4. Calling `GET /api/v1/me` with a valid token returns my user record; the same call with a missing, expired, or wrong-audience token returns 401.
5. My user row exists in the server DB after my first successful login, keyed by the OIDC `sub` claim (no manual user creation needed).
-**Plans:** TBD
+**Plans:** 7 plans
+
+Plans:
+- [ ] 02-01-PLAN.md — Shared auth contracts, dependency aliases, Authentik setup docs, and source audit
+- [ ] 02-02-PLAN.md — Server JWT validation, JWKS hardening, JIT users, and `/api/v1/me`
+- [ ] 02-03-PLAN.md — Common OIDC/store contracts, JVM/Wasm actuals, and store contract test
+- [ ] 02-04-PLAN.md — Android AppAuth actual, Android secure AuthState store, and manifest callback
+- [ ] 02-05-PLAN.md — iOS AppAuth actual, iOS Keychain store, URL scheme, Swift callback, and Podfile
+- [ ] 02-06-PLAN.md — AuthSession state machine, bearer HTTP client, refresh/logout behavior, and Koin wiring
+- [ ] 02-07-PLAN.md — Compose auth gate UI, Polish resource strings, and iOS Authentik UAT
**UI hint:** yes
**Research flag:** yes
@@ -88,14 +97,7 @@ Plans:
3. Once both users are in the same household, any household-scoped API call returns identical data regardless of which member made it.
4. A crafted API request that puts a different `household_id` in the body is ignored — the server always derives `household_id` from the authenticated principal, not the payload.
5. The server starts up and Flyway automatically applies `V1__init.sql` (or equivalent) in the correct order; restarting the server twice in a row is idempotent.
-**Plans:** 5 plans
-
-Plans:
-- [ ] 02-01-PLAN.md — Shared auth contracts, dependency aliases, Authentik setup docs, and source audit
-- [ ] 02-02-PLAN.md — Server JWT validation, JWKS hardening, JIT users, and `/api/v1/me`
-- [ ] 02-03-PLAN.md — AppAuth platform actuals, callback registration, and secure token storage
-- [ ] 02-04-PLAN.md — AuthSession state machine, bearer HTTP client, refresh/logout behavior, and Koin wiring
-- [ ] 02-05-PLAN.md — Compose auth gate UI, Polish resource strings, and iOS Authentik UAT
+**Plans:** TBD
**UI hint:** yes
**Research flag:** no
@@ -220,7 +222,7 @@ Plans:
| Phase | Plans Complete | Status | Completed |
|-------|----------------|--------|-----------|
| 1. Project Infrastructure & Module Wiring | 7/7 | Complete | 2026-04-24 |
-| 2. Authentication Foundation | 0/5 | Planned | - |
+| 2. Authentication Foundation | 0/7 | Planned | - |
| 3. Households, Membership & Server Data Foundation | 0/0 | Not started | - |
| 4. Sync Engine Skeleton | 0/0 | Not started | - |
| 5. Recipe Catalog (Read Path) | 0/0 | Not started | - |
diff --git a/.planning/phases/02-authentication-foundation/02-03-PLAN.md b/.planning/phases/02-authentication-foundation/02-03-PLAN.md
index fad34c4..4d1e836 100644
--- a/.planning/phases/02-authentication-foundation/02-03-PLAN.md
+++ b/.planning/phases/02-authentication-foundation/02-03-PLAN.md
@@ -8,14 +8,6 @@ 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/androidMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.android.kt
- - composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.android.kt
- - composeApp/src/androidMain/AndroidManifest.xml
- - composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.ios.kt
- - composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.ios.kt
- - iosApp/iosApp/Info.plist
- - iosApp/iosApp/iOSApp.swift
- - iosApp/Podfile
- 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
@@ -25,47 +17,44 @@ autonomous: true
requirements: [AUTH-01, AUTH-02, AUTH-04, AUTH-05]
must_haves:
truths:
- - "iOS and Android login use AppAuth authorization-code flow with PKCE through system browser and recipe://callback"
- - "Requested scopes are exactly openid profile email offline_access"
- - "AuthState JSON is stored through explicit iOS Keychain and Android EncryptedSharedPreferences-backed stores"
- - "Every configured KMP target has a SecureAuthStateStore actual, including JVM and Wasm stubs"
- - "JVM target has DEV_AUTH_TOKEN dev stub; Wasm target throws NotImplementedError(\"Wasm OIDC: v2\")"
- - "Logout platform clients support RP-initiated end-session and local store clearing"
+ - "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/androidMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.android.kt"
- provides: "Android explicit secure token storage per AUTH-02"
- contains: "EncryptedSharedPreferences"
- - path: "composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.ios.kt"
- provides: "iOS Keychain storage with AfterFirstUnlockThisDeviceOnly per D-14"
- contains: "kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly"
- - path: "composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.jvm.kt"
- provides: "JVM dev-only in-memory AuthState store actual so desktop tests compile"
- contains: "actual class SecureAuthStateStore"
- - path: "composeApp/src/webMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.wasmJs.kt"
- provides: "Wasm non-persistent AuthState store actual so wasm target compiles while OIDC remains v2"
- contains: "actual class SecureAuthStateStore"
- - path: "iosApp/iosApp/Info.plist"
- provides: "recipe URL scheme registration"
- contains: "CFBundleURLSchemes"
+ - 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/androidMain/AndroidManifest.xml"
- to: "composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.android.kt"
- via: "AppAuth redirect receiver for recipe://callback"
- pattern: "RedirectUriReceiverActivity|recipe"
- - from: "iosApp/iosApp/iOSApp.swift"
- to: "composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.ios.kt"
- via: "openURL forwards callback to current AppAuth external user-agent session"
- pattern: "onOpenURL|currentAuthorizationFlow"
+ - 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"
---
-Implement the platform OIDC and secure storage boundary for mobile auth.
+Define the common OIDC and AuthState storage contracts, plus JVM/Wasm actuals that keep secondary targets compiling.
-Purpose: satisfy AUTH-01/AUTH-02 platform requirements before `AuthSession` composes them into app state.
-Output: expect/actual OIDC client, explicit secure auth state store, URL callback registration, and platform stubs.
+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.
@@ -82,26 +71,35 @@ Output: expect/actual OIDC client, explicit secure auth state store, URL callbac
@.planning/phases/02-authentication-foundation/02-PATTERNS.md
@.planning/phases/02-authentication-foundation/02-01-SUMMARY.md
@AGENTS.md
-@composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainActivity.kt
-@iosApp/iosApp/iOSApp.swift
+@composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt
+@composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt
-
- Task 1: Define OidcClient and secure store common contracts
+
+ 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)
+ - .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` sealed type with `Success(authStateJson: String, accessToken: String, idToken: String?, expiresAtEpochMillis: Long)`, `Cancelled`, `NetworkError`, and `AuthError`.
+ 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 contract must state that native actuals use AppAuth and request scopes exactly `openid profile email offline_access`.
+ 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()`. Add contract tests using a fake in-memory implementation to lock read/write/clear semantics; platform implementations compile in Task 2.
+ 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
@@ -110,48 +108,15 @@ Output: expect/actual OIDC client, explicit secure auth state store, URL callbac
- `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 platform seams exist with testable store semantics and exact scope contract.
+ Common auth seams exist with exact scope/logout/storage semantics and testable store behavior.
- Task 2: Implement Android and iOS AppAuth actuals with explicit secure storage
-
- - composeApp/build.gradle.kts
- - composeApp/src/androidMain/AndroidManifest.xml
- - iosApp/iosApp/Info.plist
- - iosApp/iosApp/iOSApp.swift
- - .planning/phases/02-authentication-foundation/02-CONTEXT.md (D-01, D-04, D-09, D-13, D-14, D-19, D-20)
-
- composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.android.kt, composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.android.kt, composeApp/src/androidMain/AndroidManifest.xml, composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.ios.kt, composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.ios.kt, iosApp/iosApp/Info.plist, iosApp/iosApp/iOSApp.swift, iosApp/Podfile
-
- Android: implement AppAuth-Android using `AuthorizationServiceConfiguration.fetchFromIssuer`, `AuthorizationRequest.Builder`, `ResponseTypeValues.CODE`, `setScopes("openid", "profile", "email", "offline_access")`, AppAuth PKCE defaults, `suspendCancellableCoroutine`, `AuthState.jsonSerializeString()`, `AuthState.jsonDeserialize(...)`, `performActionWithFreshTokens`, and `EndSessionRequest` when metadata exposes end-session. Register `net.openid.appauth.RedirectUriReceiverActivity` for scheme `recipe` host `callback`.
-
- Android secure storage decision: use AndroidX Security Crypto `EncryptedSharedPreferences` behind `SecureAuthStateStore.android.kt` for AUTH-02 because the requirement explicitly calls out Android EncryptedSharedPreferences. Document in code comment that the dependency is deprecated upstream but isolated behind `SecureAuthStateStore`; do not use no-arg `Settings()` or ordinary `SharedPreferences` for auth tokens.
-
- iOS: implement AppAuth-iOS via CocoaPods/interop using `OIDAuthorizationService`, `OIDAuthState`, `OIDAuthorizationRequest`, `OIDTokenRequest` refresh/fresh-token helpers, `OIDEndSessionRequest`, and `suspendCancellableCoroutine`. Register `CFBundleURLTypes` for `recipe`. Add SwiftUI `.onOpenURL` or app delegate bridge in `iOSApp.swift` to resume the current AppAuth flow.
-
- iOS secure storage: implement Keychain read/write/delete with `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`. Persist the full AppAuth AuthState JSON blob per D-13.
-
-
- ./gradlew :composeApp:compileDebugKotlinAndroid :composeApp:compileKotlinIosSimulatorArm64
-
-
- - `grep -q 'EncryptedSharedPreferences' composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.android.kt`
- - `! grep -R 'Settings()' composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/auth`
- - `grep -q 'kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly' composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.ios.kt`
- - `grep -q 'offline_access' composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.android.kt`
- - `grep -q 'offline_access' composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.ios.kt`
- - `grep -q 'recipe' composeApp/src/androidMain/AndroidManifest.xml`
- - `grep -q 'CFBundleURLSchemes' iosApp/iosApp/Info.plist`
- - `./gradlew :composeApp:compileDebugKotlinAndroid :composeApp:compileKotlinIosSimulatorArm64` exits 0
-
- Mobile targets compile with AppAuth login/refresh/logout and explicit secure AuthState persistence.
-
-
-
- Task 3: Add JVM and Wasm target actuals
+ 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
@@ -159,13 +124,11 @@ Output: expect/actual OIDC client, explicit secure auth state store, URL callbac
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 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 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()`. This store is dev-only and process-local; do not use it for mobile targets.
+ 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 actual throws exactly `NotImplementedError("Wasm OIDC: v2")` for login/refresh/logout per D-03.
-
- Wasm `SecureAuthStateStore` actual must also exist so `:composeApp:compileKotlinWasmJs` compiles. Implement `actual class SecureAuthStateStore` with the same private nullable in-memory `authStateJson` property and `read()`, `write(authStateJson: String)`, `clear()` methods. Because Wasm OIDC itself throws `NotImplementedError("Wasm OIDC: v2")`, this store is non-persistent and only satisfies the KMP actual contract.
+ 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
@@ -177,7 +140,7 @@ Output: expect/actual OIDC client, explicit secure auth state store, URL callbac
- `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 scope into real Desktop/Wasm OIDC.
+ Secondary targets compile without expanding Phase 2 into real Desktop or Wasm OIDC.
@@ -187,27 +150,26 @@ Output: expect/actual OIDC client, explicit secure auth state store, URL callbac
| Boundary | Description |
|----------|-------------|
-| system browser -> app | Authorization code returns through custom URL scheme |
-| app process -> OS secure storage | AuthState JSON containing refresh token is persisted |
-| app -> Authentik | Refresh and end-session requests exchange tokens with IdP |
+| 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 | custom URL callback | mitigate | AppAuth handles state/nonce and PKCE S256; redirect URI byte-matched to `recipe://callback` |
-| T-02-03-02 | Information Disclosure | Android token store | mitigate | Use EncryptedSharedPreferences behind `SecureAuthStateStore`; grep forbids no-arg `Settings()` in auth |
-| T-02-03-03 | Information Disclosure | iOS token store | mitigate | Keychain item uses `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly` |
-| T-02-03-04 | Information Disclosure | AppAuth diagnostics | mitigate | Do not log AuthState JSON, access tokens, refresh tokens, id tokens, or Authorization headers |
-| T-02-03-05 | Spoofing | JVM dev stub | accept | Desktop is dev tool only; stub requires explicit `DEV_AUTH_TOKEN` and is not a release surface |
+| 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 |
-Run `./gradlew :composeApp:jvmTest :composeApp:compileDebugKotlinAndroid :composeApp:compileKotlinIosSimulatorArm64 :composeApp:compileKotlinWasmJs`.
+Run `./gradlew :composeApp:jvmTest :composeApp:compileKotlinWasmJs`.
-AUTH-01/AUTH-02 platform primitives exist: native AppAuth login/refresh/logout compiles, secure stores are explicit, and secondary target stubs match decisions.
+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.