Files
recipe/.planning/phases/02-authentication-foundation/02-05-SUMMARY.md
2026-04-29 21:07:49 +02:00

7.4 KiB

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, duration, completed
phase plan subsystem tags requires provides affects tech-stack key-files key-decisions patterns-established requirements-completed duration completed
02-authentication-foundation 05 auth
oidc
appauth
ios
keychain
swiftui
callback
phase provides
02-authentication-foundation 02-01 CocoaPods/AppAuth dependency wiring and OIDC constants
phase provides
02-authentication-foundation 02-03 common OidcClient and SecureAuthStateStore contracts
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
02-06-auth-session-ui
02-07-auth-integration-verification
added patterns
AppAuth CocoaPod reference in iosApp/Podfile
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.
created modified
composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.ios.kt
composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.ios.kt
iosApp/Podfile
iosApp/iosApp/Info.plist
iosApp/iosApp/iOSApp.swift
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.
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.
AUTH-01
AUTH-02
AUTH-04
AUTH-05
27m 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