---
phase: 02-authentication-foundation
plan: 04
type: execute
wave: 3
depends_on: [02-01, 02-03]
files_modified:
  - 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
autonomous: true
requirements: [AUTH-01, AUTH-02, AUTH-04, AUTH-05]
must_haves:
  truths:
    - "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"
  artifacts:
    - path: "composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.android.kt"
      provides: "Android AppAuth actual per D-01, D-04, D-16, D-19, D-20"
      contains: "AuthorizationService"
    - 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/androidMain/AndroidManifest.xml"
      provides: "recipe://callback registration for AppAuth redirect receiver"
      contains: "RedirectUriReceiverActivity"
  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: "composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.android.kt"
      to: "composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.android.kt"
      via: "AuthState JSON returned by AppAuth is what AuthSession persists through the store"
      pattern: "jsonSerializeString|jsonDeserialize"
---

<objective>
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.
</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
@.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
</context>

<tasks>

<task type="auto">
  <name>Task 1: Implement Android AppAuth OidcClient actual</name>
  <read_first>
    - 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)
  </read_first>
  <files>composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.android.kt</files>
  <action>
    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.
  </action>
  <verify>
    <automated>./gradlew :composeApp:compileDebugKotlinAndroid</automated>
  </verify>
  <acceptance_criteria>
    - `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
  </acceptance_criteria>
  <done>Android AppAuth login, refresh, and logout compile behind the common OidcClient contract.</done>
</task>

<task type="auto">
  <name>Task 2: Implement Android secure AuthState store and callback manifest</name>
  <read_first>
    - 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)
  </read_first>
  <files>composeApp/src/androidMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.android.kt, composeApp/src/androidMain/AndroidManifest.xml</files>
  <action>
    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`).
  </action>
  <verify>
    <automated>./gradlew :composeApp:compileDebugKotlinAndroid</automated>
  </verify>
  <acceptance_criteria>
    - `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
  </acceptance_criteria>
  <done>Android token storage is explicit and the custom URL callback is registered for AppAuth.</done>
</task>

</tasks>

<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>

<verification>
Run `./gradlew :composeApp:compileDebugKotlinAndroid`.
</verification>

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

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