diff --git a/.planning/phases/02-authentication-foundation/02-05-SUMMARY.md b/.planning/phases/02-authentication-foundation/02-05-SUMMARY.md new file mode 100644 index 0000000..4c9402d --- /dev/null +++ b/.planning/phases/02-authentication-foundation/02-05-SUMMARY.md @@ -0,0 +1,153 @@ +--- +phase: 02-authentication-foundation +plan: 05 +subsystem: auth +tags: [oidc, appauth, ios, keychain, swiftui, callback] + +requires: + - phase: 02-authentication-foundation + provides: 02-01 CocoaPods/AppAuth dependency wiring and OIDC constants + - phase: 02-authentication-foundation + provides: 02-03 common OidcClient and SecureAuthStateStore contracts +provides: + - iOS AppAuth OidcClient actual with login, refresh, logout, and callback bridge + - iOS Keychain-backed SecureAuthStateStore using kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly + - recipe URL scheme registration in Info.plist + - SwiftUI onOpenURL callback forwarding into the current AppAuth flow + - iosApp Podfile with AppAuth pod integration +affects: [02-06-auth-session-ui, 02-07-auth-integration-verification] + +tech-stack: + added: + - AppAuth CocoaPod reference in iosApp/Podfile + patterns: + - "iOS AppAuth bridge: Kotlin singleton holds currentAuthorizationFlow; SwiftUI forwards recipe://callback URLs by absolute string." + - "iOS AuthState persistence: full OIDAuthState NSSecureCoding archive wrapped in an opaque JSON string and stored through KeychainSettings." + +key-files: + created: + - composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.ios.kt + - composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.ios.kt + - iosApp/Podfile + modified: + - iosApp/iosApp/Info.plist + - iosApp/iosApp/iOSApp.swift + +key-decisions: + - "AppAuth-iOS AuthState persistence uses NSSecureCoding wrapped in JSON because AppAuth-iOS 2.0.0 does not expose the Android-style serialize()/jsonDeserialize API." + - "SecureAuthStateStore was implemented in the first task commit because the Task 1 compile gate cannot pass while the common expect class lacks an iOS actual." + - "SwiftUI forwards only recipe://callback URLs to the KMP bridge; other URLs are ignored before AppAuth sees them." + +patterns-established: + - "Never log token-bearing values in iOS auth actuals; token variables are only returned through OidcResult or stored in Keychain." + - "Mobile callback state remains inside AppAuth's current external user-agent session and is consumed once." + +requirements-completed: [AUTH-01, AUTH-02, AUTH-04, AUTH-05] + +duration: 27m +completed: 2026-04-28 +--- + +# Phase 02 Plan 05: iOS AppAuth Actuals Summary + +**iOS AppAuth login, fresh-token refresh, RP-initiated logout, Keychain AuthState persistence, and recipe://callback forwarding behind the Phase 02 common auth contracts.** + +## Performance + +- **Duration:** 27 min +- **Started:** 2026-04-28T13:52:54Z +- **Completed:** 2026-04-28T14:19:03Z +- **Tasks:** 2 +- **Files modified:** 5 + +## Accomplishments + +- Added the iOS `OidcClient` actual using AppAuth discovery, authorization-code flow with PKCE, exact `openid profile email offline_access` scopes, fresh-token refresh, and end-session logout. +- Added the iOS secure store actual using Keychain-backed settings with `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`. +- Registered the `recipe` URL scheme and wired SwiftUI `.onOpenURL` to forward only `recipe://callback` URLs to the active AppAuth external user-agent session. +- Added `iosApp/Podfile` with `AppAuth` so the iOS shell has explicit pod integration alongside the existing KMP CocoaPods block. + +## Task Commits + +1. **Task 1: Implement iOS AppAuth OidcClient actual and CocoaPods bridge** - `ac9fc61` (feat) +2. **Task 2: Implement iOS Keychain store and callback wiring** - `88dc8d7` (feat) + +## Files Created/Modified + +- `composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.ios.kt` - AppAuth-iOS login, refresh, logout, AuthState archive/restore, and callback bridge. +- `composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.ios.kt` - Keychain-backed opaque AuthState store with the required accessibility class. +- `iosApp/Podfile` - iOS target Podfile declaring the `AppAuth` pod. +- `iosApp/iosApp/Info.plist` - `CFBundleURLTypes` registration for the `recipe` custom URL scheme. +- `iosApp/iosApp/iOSApp.swift` - SwiftUI `.onOpenURL` forwarding for `recipe://callback`. + +## Decisions Made + +See frontmatter `key-decisions`. + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 3 - Blocking] Implemented `SecureAuthStateStore.ios.kt` during Task 1** + +- **Found during:** Task 1 verification +- **Issue:** `./gradlew :composeApp:compileKotlinIosSimulatorArm64` cannot pass after adding only `OidcClient.ios.kt` because the common `SecureAuthStateStore` expect class also requires an iOS actual. +- **Fix:** Added the Keychain-backed iOS secure store in the Task 1 commit, then Task 2 added the URL scheme and Swift callback wiring. +- **Files modified:** `composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.ios.kt` +- **Verification:** `./gradlew :composeApp:compileKotlinIosSimulatorArm64` +- **Committed in:** `ac9fc61` + +**2. [Rule 3 - Blocking] Wrapped AppAuth-iOS secure archive in JSON** + +- **Found during:** Task 1 implementation +- **Issue:** AppAuth-iOS 2.0.0 exposes `OIDAuthState` as `NSSecureCoding`; it does not expose the Android-style `serialize()` / JSON-deserialize API assumed by the plan. +- **Fix:** Persisted a JSON wrapper containing the full `NSKeyedArchiver` secure archive of `OIDAuthState`, preserving the common opaque `authStateJson` contract while using AppAuth-iOS' supported persistence mechanism. +- **Files modified:** `composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.ios.kt` +- **Verification:** `./gradlew :composeApp:compileKotlinIosSimulatorArm64` +- **Committed in:** `ac9fc61` + +--- + +**Total deviations:** 2 auto-fixed (2 x Rule 3). +**Impact on plan:** No auth behavior was reduced. Both fixes were required for the iOS target to compile against the actual AppAuth-iOS API and the existing common expect contracts. + +## Known Stubs + +None. + +## Threat Flags + +None beyond the plan's threat model. This plan intentionally touches the browser callback, Keychain storage, and Swift-to-KMP callback trust boundaries already listed in `02-05-PLAN.md`. + +## Issues Encountered + +- `iosApp/Podfile` did not exist even though the plan listed it in `read_first`; it was created in Task 1. +- A parallel `git add` attempt briefly hit Git's index lock. Staging was retried sequentially; no repository state was lost. +- `./gradlew :composeApp:linkDebugFrameworkIosSimulatorArm64` passed as an extra confidence check and confirmed `IosAppAuthBridge.shared.resumeExternalUserAgentFlow(urlString:)` is exported to Swift. + +## User Setup Required + +None for this plan. Real login still requires the Authentik provider configuration documented in `docs/authentik-setup.md`. + +## Verification + +- Task 1 acceptance greps - PASS +- Task 2 acceptance greps - PASS +- `./gradlew :composeApp:compileKotlinIosSimulatorArm64` - PASS +- Extra: `./gradlew :composeApp:linkDebugFrameworkIosSimulatorArm64` - PASS +- Token/logging scan - PASS; no `Logger`, `println`, or token/AuthState logging calls were added. + +## Next Phase Readiness + +Plan 02-06 can consume the common `OidcClient` and `SecureAuthStateStore` on iOS. Plan 02-07 should still run real iOS/Authenik UAT for browser handoff, refresh across relaunch, and end-session behavior. + +## Self-Check: PASSED + +- Created/modified files exist: all five plan-owned source/config files plus this summary were found. +- Commits exist: `ac9fc61` and `88dc8d7` were found in git history. +- Acceptance criteria: all Task 1 and Task 2 grep checks passed. +- Plan-level verification: `./gradlew :composeApp:compileKotlinIosSimulatorArm64` passed. + +--- +*Phase: 02-authentication-foundation* +*Completed: 2026-04-28*