docs(02): capture phase context
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
# Phase 2: Authentication Foundation - Discussion Log
|
||||
|
||||
> **Audit trail only.** Do not use as input to planning, research, or execution agents.
|
||||
> Decisions are captured in `02-CONTEXT.md` — this log preserves the alternatives considered.
|
||||
|
||||
**Date:** 2026-04-27
|
||||
**Phase:** 02-authentication-foundation
|
||||
**Areas discussed:** iOS OIDC wrapper approach, Redirect URI + Authentik provider config, Token lifecycle (storage/refresh/logout), Phase 2/3 boundary + login UX shape
|
||||
|
||||
---
|
||||
|
||||
## iOS OIDC wrapper approach
|
||||
|
||||
**Original question:** "How should iOS speak OIDC + PKCE to Authentik?"
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| AppAuth on both platforms | AppAuth-Android + AppAuth-iOS via CocoaPod. Symmetric expect/actual seam. Battle-tested, library-managed refresh, PKCE built-in. | ✓ |
|
||||
| Hand-rolled ASWebAuthenticationSession wrapper | Thin Swift wrapper + Kotlin-side PKCE/token-exchange. Smallest deps; ~250 LOC owned. | |
|
||||
| Defer to researcher — evaluate community KMP libs | Have researcher survey 2026 KMP OIDC library landscape first. | |
|
||||
| Hand-rolled iOS, AppAuth Android | Asymmetric. Avoids CocoaPods on iOS. | |
|
||||
|
||||
**User's clarification:** Asked whether AppAuth would block Desktop and Wasm support; questioned whether to abandon Wasm.
|
||||
|
||||
**Reframed answer:** Native OIDC is platform-specific regardless of choice — AppAuth doesn't make Desktop/Wasm worse. Decision: AppAuth on mobile + dev-mode env-var stub (`DEV_AUTH_TOKEN`) for Desktop + `NotImplementedError` stub for Wasm. Don't abandon Wasm yet — cost of preserving (~5-30 LOC stubs per platform-touching phase) is much smaller than cost of resurrecting it later. Revisit only if stubbing tax compounds painfully.
|
||||
|
||||
**User's choice:** AppAuth on both platforms.
|
||||
|
||||
**Notes:** User's pushback on cross-target implications was correct and surfaced a load-bearing decision (Desktop/Wasm stubs). Recorded as D-01 through D-04 in CONTEXT.md.
|
||||
|
||||
---
|
||||
|
||||
## Redirect URI + Authentik provider config
|
||||
|
||||
### Sub-question 1: Redirect URI scheme
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Custom URL scheme `recipe://callback` | iOS Info.plist + Android intent-filter. ~10 lines. AppAuth + PKCE state/nonce makes interception non-exploitable. | ✓ |
|
||||
| Universal Links / App Links via HTTPS | Requires hosting apple-app-site-association + assetlinks.json on homelab. Cert SHA pinning. Cryptographically tied to domain. | |
|
||||
| Custom now, Universal Links later if needed | Same as option 1 for v1; documented in deferred. | |
|
||||
|
||||
**User's choice:** Custom URL scheme `recipe://callback`.
|
||||
|
||||
### Sub-question 2: Client OIDC config location
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Hardcoded in shared/commonMain/Constants.kt | Single source of truth. Per PITFALLS.md "acceptable v1 single-environment only" tech-debt note. | ✓ |
|
||||
| Gradle property → BuildConfig-style generated Kotlin | Build-time injection; supports dev/staging/prod variants. | |
|
||||
| Hybrid: hardcoded defaults, Gradle override available | Constants with Gradle-property overrides. | |
|
||||
|
||||
**User's choice:** Hardcoded in `shared/commonMain/Constants.kt`.
|
||||
|
||||
### Sub-question 3: OIDC scopes
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| openid profile email offline_access | Standard mobile-app OIDC scope set. JIT-provisioning gets sub + email + display_name + refresh token. | ✓ |
|
||||
| openid email offline_access (no profile) | Drops display_name; UI shows email everywhere. | |
|
||||
| openid offline_access (minimal) | Sub + refresh token only; no email or name. | |
|
||||
|
||||
**User's choice:** `openid profile email offline_access`.
|
||||
|
||||
**Notes:** No-fork recommendations also recorded: pin Authentik `aud` to single client_id string; ship `docs/authentik-setup.md` as a Phase 2 deliverable. RS256 signing alg confirmed (Authentik default, matches PITFALLS.md #7 expectation).
|
||||
|
||||
---
|
||||
|
||||
## Token lifecycle (storage, refresh, logout)
|
||||
|
||||
### Sub-question 1: Storage backend
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Full AppAuth AuthState blob, AfterFirstUnlockThisDeviceOnly | ~2KB JSON via multiplatform-settings. AppAuth's `.serialize()`/`.jsonDeserialize()`. iCloud-Keychain-excluded. | ✓ |
|
||||
| Just access + refresh + expiry, AfterFirstUnlockThisDeviceOnly | ~200 bytes explicit fields. We own the deserialization. | |
|
||||
| Full AppAuth AuthState blob, WhenUnlockedThisDeviceOnly | Stricter accessibility; blocks pre-unlock work (none in v1, but blocks future background sync). | |
|
||||
|
||||
**User's choice:** Full AppAuth AuthState blob, AfterFirstUnlockThisDeviceOnly.
|
||||
|
||||
### Sub-question 2: Refresh policy
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Proactive (AppAuth performActionWithFreshTokens) + reactive 401 fallback | Both layers. Library-provided single-flight on each. UX: refresh is invisible. | ✓ |
|
||||
| Reactive only (Ktor bearer plugin) | Simpler. One wasted round-trip per expiry boundary. | |
|
||||
| Proactive only (AppAuth, no Ktor bearer) | Skips Ktor's plugin. No clock-drift recovery. | |
|
||||
|
||||
**User's choice:** Proactive + reactive 401 fallback.
|
||||
|
||||
### Sub-question 3: Refresh-failure UX
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Silent — transition to Unauthenticated, return to login | No modal, no toast. Cleanest UX. | ✓ |
|
||||
| Surfaced — modal "Twoja sesja wygasła, zaloguj się ponownie" | Explicit dialog before returning to login. | |
|
||||
| Surfaced as toast on login screen | Silent transition + non-blocking snackbar. | |
|
||||
|
||||
**User's choice:** Silent.
|
||||
|
||||
### Sub-question 4: Logout semantics
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| RP-initiated end-session | Wipe local tokens AND call Authentik's end_session_endpoint with id_token_hint. Forces fresh credentials on next login. | ✓ |
|
||||
| Local-only token wipe | Authentik session persists; next login silently SSO's. | |
|
||||
| Both — local default + "forget session" as long-press / settings option | Two-tier UX. Overkill for v1. | |
|
||||
|
||||
**User's choice:** RP-initiated end-session.
|
||||
|
||||
**Notes:** User accepted all four recommendations without challenge. Decisions recorded as D-13 through D-20 in CONTEXT.md.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2/3 boundary + login UX shape
|
||||
|
||||
### Sub-question 1: Server schema split
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Phase 2 owns V1__users.sql; Phase 3 layers V2__households_memberships_invites.sql | Auth phase owns auth-principal table. JIT-provisioning writes a real row in Phase 2. ROADMAP Phase 3 description gets a one-line edit (drop `users`). | ✓ |
|
||||
| Phase 3 ships V1__init.sql with everything; Phase 2 returns JWT-derived user | Single migration in Phase 3. Phase 2 doesn't persist; SC#5 gets rewritten. | |
|
||||
| Phase 2 ships V1__users.sql with JIT-insert wired but table only used in Phase 3 | Schema lands but doesn't see traffic until Phase 3. Same complexity, no win. | |
|
||||
|
||||
**User's choice:** Phase 2 owns `V1__users.sql`; Phase 3 layers `V2`.
|
||||
|
||||
### Sub-question 2: AuthSession state shape
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Forward-compat: Authenticated(user, householdId: HouseholdId?) — null in Phase 2 | Carries Phase 3's needs from day 1. No sealed-class refactor at Phase 2/3 boundary. Mild forward-compat justified by adjacency. | ✓ |
|
||||
| Phase 2 minimal: Authenticated(user) only | Strict YAGNI. Phase 3 widens the sealed shape; refactor cost is small. | |
|
||||
|
||||
**User's choice:** Forward-compat with nullable `householdId`.
|
||||
|
||||
### Sub-question 3: Login screen shape
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Minimal: app name + "Zaloguj się przez Authentik" button | Centered, no marketing copy. Inline error states for cancelled/network/exchange failures. | ✓ |
|
||||
| Branded: app name + tagline + button + disclosure | Adds tagline + "Otworzy się w przeglądarce" disclosure. | |
|
||||
| Stub: just a button labeled "login" | Bare-minimum; Phase 11 polishes. | |
|
||||
|
||||
**User's choice:** Minimal app name + button.
|
||||
|
||||
### Sub-question 4: Post-login UI in Phase 2
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Placeholder "Witaj, {displayName}!" + Wyloguj button | Confirms login worked end-to-end. Lets you exercise logout. Phase 3 replaces wholesale. | ✓ |
|
||||
| Empty state "Brak gospodarstwa" + Wyloguj button | Forward-compat: this IS Phase 3's "no household yet" state. | |
|
||||
| Just route back to login screen with token persisted | No post-login UI; verify via /api/v1/me curl. | |
|
||||
|
||||
**User's choice:** Placeholder welcome screen.
|
||||
|
||||
**Notes:** All four sub-questions accepted recommendations. Decisions recorded as D-24 through D-33 in CONTEXT.md.
|
||||
|
||||
---
|
||||
|
||||
## Claude's Discretion
|
||||
|
||||
The following implementation details were left to Claude's judgment during planning/execution:
|
||||
- Exact Koin `authModule` definition style (`single<T> { ... }` vs `single { T(get(), ...) }`)
|
||||
- Ktor Client `Auth { bearer { ... } }` plugin configuration boilerplate
|
||||
- Whether `MeResponse` DTO and `User` domain model are unified or separate
|
||||
- UUID library choice for `User.id` (`kotlinx.uuid` vs `kotlin.uuid.Uuid` if Kotlin 2.3 is stable)
|
||||
- AppAuth-iOS CocoaPod integration via Gradle DSL (`cocoapods { pod("AppAuth") }`) vs hand-written Podfile
|
||||
- Splash placeholder visual during `Loading` state
|
||||
- `OIDC_ISSUER` trailing-slash convention (pin and document)
|
||||
- Logger tag/level for AppAuth events
|
||||
|
||||
## Deferred Ideas
|
||||
|
||||
The following ideas surfaced during discussion and were noted for future phases or v2:
|
||||
- Universal Links / App Links (deferred unless distribution broadens or custom schemes get deprecated)
|
||||
- BuildConfig-style Gradle config injection (defer until staging Authentik is a real need)
|
||||
- Real Desktop OIDC (deferred unless Desktop becomes a release surface)
|
||||
- Wasm OIDC implementation (deferred to v2; native AppAuth path won't reuse)
|
||||
- Two-tier logout ("forget session" long-press)
|
||||
- Background token refresh
|
||||
- Apple Sign-in first-class button (PROJECT.md says Authentik federates upstream)
|
||||
- Per-user persisted AuthState (multi-account is post-v1)
|
||||
- Modal/toast for refresh-failure UX (revisit if users complain about silent logout)
|
||||
- Authentik provisioning automation (Terraform/Ansible — post-v1)
|
||||
- Integration tests against real Authentik (deferred to Phase 11 deployment)
|
||||
Reference in New Issue
Block a user