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

9.6 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 04 execute 3
02-01
02-03
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
true
AUTH-01
AUTH-02
AUTH-04
AUTH-05
truths artifacts key_links
Android login uses AppAuth authorization-code flow with PKCE through system browser and recipe://callback
Android requested scopes are exactly openid profile email offline_access
Android persists full AppAuth AuthState JSON through EncryptedSharedPreferences-backed SecureAuthStateStore
Android refresh uses AppAuth fresh-token behavior and persists updated AuthState JSON
Android logout uses AppAuth end-session when metadata exposes an endpoint
path provides contains
composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.android.kt Android AppAuth actual per D-01, D-04, D-16, D-19, D-20 AuthorizationService
path provides contains
composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.android.kt Android explicit secure token storage per AUTH-02 EncryptedSharedPreferences
path provides contains
composeApp/src/androidMain/AndroidManifest.xml recipe://callback registration for AppAuth redirect receiver RedirectUriReceiverActivity
from to via pattern
composeApp/src/androidMain/AndroidManifest.xml composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.android.kt AppAuth redirect receiver for recipe://callback RedirectUriReceiverActivity|recipe
from to via pattern
composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.android.kt composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.android.kt AuthState JSON returned by AppAuth is what AuthSession persists through the store jsonSerializeString|jsonDeserialize
Implement the Android OIDC and secure storage actuals.

Purpose: satisfy Android's side of AUTH-01/AUTH-02/AUTH-04/AUTH-05 behind the common contracts from Plan 02-03 without mixing iOS work into the same execution plan. Output: Android AppAuth OidcClient actual, Android secure AuthState store, and Android callback manifest registration.

<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 @.planning/phases/02-authentication-foundation/02-03-SUMMARY.md @AGENTS.md @composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/MainActivity.kt @composeApp/src/androidMain/AndroidManifest.xml Task 1: Implement Android AppAuth OidcClient actual - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.kt - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/OidcResult.kt - shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/Constants.kt - .planning/phases/02-authentication-foundation/02-CONTEXT.md (D-01, D-04, D-05, D-06, D-09, D-16, D-19, D-20) composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.android.kt Implement Android `actual class OidcClient` using AppAuth-Android. Use `AuthorizationServiceConfiguration.fetchFromIssuer`, `AuthorizationRequest.Builder`, `ResponseTypeValues.CODE`, `setScopes("openid", "profile", "email", "offline_access")`, AppAuth PKCE defaults, and `suspendCancellableCoroutine` so cancellation cancels the underlying AppAuth request.
Token exchange and refresh must serialize/deserialize the AppAuth `AuthState` JSON with `AuthState.jsonSerializeString()` and `AuthState.jsonDeserialize(...)`. Refresh must use `performActionWithFreshTokens` so updated AuthState is persisted by AuthSession. Logout must build and execute `EndSessionRequest` when the discovery metadata exposes an end-session endpoint; if unavailable, return without throwing so AuthSession can still clear local state per D-19.

Map user cancellation to `OidcResult.Cancelled`, network failures to `OidcResult.NetworkError`, and token/auth failures to `OidcResult.AuthError`. Never log AuthState JSON, access tokens, refresh tokens, ID tokens, or Authorization headers.
./gradlew :composeApp:compileDebugKotlinAndroid - `grep -q 'AuthorizationServiceConfiguration.fetchFromIssuer' composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.android.kt` - `grep -q 'setScopes("openid", "profile", "email", "offline_access")' composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.android.kt` - `grep -q 'suspendCancellableCoroutine' composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.android.kt` - `grep -q 'performActionWithFreshTokens' composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.android.kt` - `grep -q 'EndSessionRequest' composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.android.kt` - `./gradlew :composeApp:compileDebugKotlinAndroid` exits 0 Android AppAuth login, refresh, and logout compile behind the common OidcClient contract. Task 2: Implement Android secure AuthState store and callback manifest - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.kt - composeApp/src/androidMain/AndroidManifest.xml - .planning/phases/02-authentication-foundation/02-CONTEXT.md (D-09, D-13, D-15) - .planning/phases/02-authentication-foundation/02-RESEARCH.md (Android secure storage correction) composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.android.kt, composeApp/src/androidMain/AndroidManifest.xml Implement Android `actual class SecureAuthStateStore` using AndroidX Security Crypto `EncryptedSharedPreferences`. Store one opaque AuthState JSON string per app install under a private key. Add a short code comment noting AndroidX Security Crypto deprecation is contained behind this abstraction because AUTH-02 explicitly calls for Android EncryptedSharedPreferences in v1.
Do not use no-arg `Settings()`, ordinary `SharedPreferences`, or plaintext file storage for auth tokens.

Register AppAuth redirect handling in `composeApp/src/androidMain/AndroidManifest.xml` with `net.openid.appauth.RedirectUriReceiverActivity` and an intent filter for scheme `recipe` and host `callback`, matching D-09 exactly (`recipe://callback`).
./gradlew :composeApp:compileDebugKotlinAndroid - `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 -R 'getSharedPreferences' composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.android.kt` - `grep -q 'RedirectUriReceiverActivity' composeApp/src/androidMain/AndroidManifest.xml` - `grep -q 'android:scheme="recipe"' composeApp/src/androidMain/AndroidManifest.xml` - `grep -q 'android:host="callback"' composeApp/src/androidMain/AndroidManifest.xml` - `./gradlew :composeApp:compileDebugKotlinAndroid` exits 0 Android token storage is explicit and the custom URL callback is registered for AppAuth.

<threat_model>

Trust Boundaries

Boundary Description
system browser -> Android app Authorization code returns through custom URL scheme
Android app -> OS secure storage AuthState JSON containing refresh token is persisted
Android app -> Authentik Refresh and end-session requests exchange tokens with IdP

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-02-04-01 Spoofing/Elevation custom URL callback mitigate AppAuth handles state/nonce and PKCE S256; Android manifest byte-matches recipe://callback
T-02-04-02 Information Disclosure Android token store mitigate Use EncryptedSharedPreferences behind SecureAuthStateStore; grep forbids no-arg Settings() and plaintext SharedPreferences in auth
T-02-04-03 Information Disclosure AppAuth diagnostics mitigate Android actual must not log AuthState JSON, access tokens, refresh tokens, ID tokens, or Authorization headers
T-02-04-04 Denial of Service refresh path mitigate Use AppAuth performActionWithFreshTokens so expiry refresh is handled before authenticated calls
</threat_model>
Run `./gradlew :composeApp:compileDebugKotlinAndroid`.

<success_criteria> Android AppAuth login/refresh/logout and Android secure AuthState persistence compile independently below the file-count threshold. </success_criteria>

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