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