Add authentication
This commit is contained in:
169
.planning/phases/02-authentication-foundation/02-05-PLAN.md
Normal file
169
.planning/phases/02-authentication-foundation/02-05-PLAN.md
Normal file
@@ -0,0 +1,169 @@
|
||||
---
|
||||
phase: 02-authentication-foundation
|
||||
plan: 05
|
||||
type: execute
|
||||
wave: 3
|
||||
depends_on: [02-01, 02-03]
|
||||
files_modified:
|
||||
- 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
|
||||
autonomous: true
|
||||
requirements: [AUTH-01, AUTH-02, AUTH-04, AUTH-05]
|
||||
must_haves:
|
||||
truths:
|
||||
- "iOS login uses AppAuth authorization-code flow with PKCE through system browser and recipe://callback"
|
||||
- "iOS requested scopes are exactly openid profile email offline_access"
|
||||
- "iOS persists full AppAuth AuthState JSON in Keychain with kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly"
|
||||
- "SwiftUI callback wiring forwards recipe://callback to the current AppAuth flow"
|
||||
- "iOS logout uses AppAuth end-session when metadata exposes an endpoint"
|
||||
artifacts:
|
||||
- path: "composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.ios.kt"
|
||||
provides: "iOS AppAuth actual per D-01, D-04, D-16, D-19, D-20"
|
||||
contains: "OIDAuthorizationService"
|
||||
- path: "composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.ios.kt"
|
||||
provides: "iOS Keychain storage per D-14"
|
||||
contains: "kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly"
|
||||
- path: "iosApp/iosApp/Info.plist"
|
||||
provides: "recipe URL scheme registration"
|
||||
contains: "CFBundleURLSchemes"
|
||||
- path: "iosApp/iosApp/iOSApp.swift"
|
||||
provides: "SwiftUI openURL callback forwarding to AppAuth"
|
||||
contains: "onOpenURL"
|
||||
- path: "iosApp/Podfile"
|
||||
provides: "AppAuth CocoaPod integration if required by chosen KMP CocoaPods setup"
|
||||
contains: "AppAuth"
|
||||
key_links:
|
||||
- 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: "iosApp/iosApp/Info.plist"
|
||||
to: "composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.ios.kt"
|
||||
via: "registered URL scheme matches redirect URI consumed by AppAuth"
|
||||
pattern: "CFBundleURLSchemes|recipe"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Implement the iOS OIDC and secure storage actuals.
|
||||
|
||||
Purpose: satisfy iOS-primary AUTH-01/AUTH-02/AUTH-04/AUTH-05 behind the common contracts from Plan 02-03 without mixing Android work into the same execution plan.
|
||||
Output: iOS AppAuth OidcClient actual, iOS Keychain AuthState store, URL scheme registration, Swift callback wiring, and Podfile integration.
|
||||
</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
|
||||
@iosApp/iosApp/Info.plist
|
||||
@iosApp/iosApp/iOSApp.swift
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Implement iOS AppAuth OidcClient actual and CocoaPods bridge</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
|
||||
- iosApp/Podfile
|
||||
- .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/iosMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.ios.kt, iosApp/Podfile</files>
|
||||
<action>
|
||||
Implement iOS `actual class OidcClient` via AppAuth-iOS interop using `OIDAuthorizationService`, `OIDAuthState`, `OIDAuthorizationRequest`, token refresh/fresh-token helpers, and `OIDEndSessionRequest`. Use `suspendCancellableCoroutine` so cancellation cancels the current AppAuth request.
|
||||
|
||||
Request scopes exactly `openid`, `profile`, `email`, and `offline_access`. Serialize and deserialize the full `OIDAuthState` JSON blob per D-13. Refresh must use AppAuth fresh-token behavior and return updated AuthState JSON for AuthSession persistence. Logout must attempt RP-initiated end-session with `id_token_hint` when available; if end-session is unavailable or fails, surface no local-token-clearing responsibility here because AuthSession clears local state after calling logout.
|
||||
|
||||
Ensure AppAuth CocoaPod integration is present through the existing Gradle CocoaPods setup from Plan 02-01 and/or `iosApp/Podfile` as required by the repo's KMP CocoaPods wiring. Do not introduce an additional OIDC library.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>./gradlew :composeApp:compileKotlinIosSimulatorArm64</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -q 'OIDAuthorizationService' composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.ios.kt`
|
||||
- `grep -q 'offline_access' composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.ios.kt`
|
||||
- `grep -q 'suspendCancellableCoroutine' composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.ios.kt`
|
||||
- `grep -q 'OIDEndSessionRequest' composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.ios.kt`
|
||||
- `grep -q 'AppAuth' iosApp/Podfile composeApp/build.gradle.kts`
|
||||
- `./gradlew :composeApp:compileKotlinIosSimulatorArm64` exits 0
|
||||
</acceptance_criteria>
|
||||
<done>iOS AppAuth login, refresh, and logout compile behind the common OidcClient contract.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Implement iOS Keychain store and callback wiring</name>
|
||||
<read_first>
|
||||
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.kt
|
||||
- iosApp/iosApp/Info.plist
|
||||
- iosApp/iosApp/iOSApp.swift
|
||||
- .planning/phases/02-authentication-foundation/02-CONTEXT.md (D-09, D-13, D-14, D-15)
|
||||
</read_first>
|
||||
<files>composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.ios.kt, iosApp/iosApp/Info.plist, iosApp/iosApp/iOSApp.swift</files>
|
||||
<action>
|
||||
Implement iOS `actual class SecureAuthStateStore` with Keychain read/write/delete for one opaque AuthState JSON string per app install. Use `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly` exactly per D-14; do not store AuthState in UserDefaults or plaintext files.
|
||||
|
||||
Add `CFBundleURLTypes` to `iosApp/iosApp/Info.plist` registering scheme `recipe`, matching redirect URI `recipe://callback`.
|
||||
|
||||
Add SwiftUI `.onOpenURL` or an app delegate bridge in `iOSApp.swift` that forwards incoming `recipe://callback` URLs to the current AppAuth external user-agent session held by the KMP iOS OidcClient bridge. Keep existing Koin initialization intact.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>./gradlew :composeApp:compileKotlinIosSimulatorArm64</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -q 'kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly' composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.ios.kt`
|
||||
- `! grep -R 'NSUserDefaults\\|UserDefaults' composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/auth`
|
||||
- `grep -q 'CFBundleURLSchemes' iosApp/iosApp/Info.plist`
|
||||
- `grep -q 'recipe' iosApp/iosApp/Info.plist`
|
||||
- `grep -q 'onOpenURL\\|application(.*open' iosApp/iosApp/iOSApp.swift`
|
||||
- `./gradlew :composeApp:compileKotlinIosSimulatorArm64` exits 0
|
||||
</acceptance_criteria>
|
||||
<done>iOS token storage is explicit and the custom URL callback is wired back into AppAuth.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| system browser -> iOS app | Authorization code returns through custom URL scheme |
|
||||
| iOS app -> Keychain | AuthState JSON containing refresh token is persisted |
|
||||
| Swift shell -> KMP auth bridge | openURL callback crosses from SwiftUI into KMP/AppAuth flow state |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-02-05-01 | Spoofing/Elevation | custom URL callback | mitigate | AppAuth handles state/nonce and PKCE S256; Info.plist byte-matches `recipe://callback` |
|
||||
| T-02-05-02 | Information Disclosure | iOS token store | mitigate | Keychain item uses `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`; grep forbids UserDefaults in auth |
|
||||
| T-02-05-03 | Information Disclosure | AppAuth diagnostics | mitigate | iOS actual must not log AuthState JSON, access tokens, refresh tokens, ID tokens, or Authorization headers |
|
||||
| T-02-05-04 | Spoofing | Swift callback bridge | mitigate | `onOpenURL` forwards only registered callback URLs to the active AppAuth session |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
Run `./gradlew :composeApp:compileKotlinIosSimulatorArm64`.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
iOS AppAuth login/refresh/logout, iOS Keychain AuthState persistence, URL scheme registration, and callback forwarding compile independently below the file-count threshold.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/02-authentication-foundation/02-05-SUMMARY.md`.
|
||||
</output>
|
||||
Reference in New Issue
Block a user