---
phase: 02-authentication-foundation
plan: 06
subsystem: auth
tags: [kmp, auth-session, ktor-client, bearer-auth, koin, oidc]

requires:
  - phase: 02-authentication-foundation
    provides: 02-01 shared Constants, User, MeResponse, Ktor client dependencies
  - phase: 02-authentication-foundation
    provides: 02-03 common OidcClient and SecureAuthStateStore contracts
  - phase: 02-authentication-foundation
    provides: 02-04 Android AppAuth and secure store actuals
  - phase: 02-authentication-foundation
    provides: 02-05 iOS AppAuth and Keychain actuals
provides:
  - AuthState Loading / Unauthenticated / Authenticated(user, householdId?) model
  - AuthSession StateFlow owner for restore, login, logout, proactive refresh, and Ktor reactive refresh
  - MeClient for GET /api/v1/me mapped to User
  - AuthHttpClient Ktor bearer client with token-redacting logging
  - authModule Koin singleton wiring included from appModule
affects: [02-07-auth-integration-verification, phase-03-households]

tech-stack:
  added: []
  patterns:
    - "AuthSession depends on small common gateways so state-machine tests use fakes while production constructors delegate to platform expect classes."
    - "Authenticated state is built from server MeResponse only; Phase 2 householdId remains null."
    - "Ktor bearer loadTokens/refreshTokens delegates to AuthSession, with Authorization header sanitization and message redaction."

key-files:
  created:
    - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/AuthState.kt
    - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/AuthSession.kt
    - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/AuthHttpClient.kt
    - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/MeClient.kt
    - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/AuthModule.kt
    - composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/auth/AuthSessionTest.kt
  modified:
    - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt

key-decisions:
  - "Use lightweight common gateway interfaces for AuthSession tests instead of changing OidcClient/SecureAuthStateStore expect/actual contracts."
  - "MeClient accepts an optional access token for AuthSession's explicit /me calls; other authenticated clients use AuthHttpClient bearer auth."
  - "Koin provides AuthSession and AuthHttpClient as singletons from authModule; Koin startup remains platform bootstrap-owned."

patterns-established:
  - "AuthSession.restore/login refreshes through OidcClient before /api/v1/me and persists the updated opaque AuthState JSON."
  - "Refresh failures, including invalid_grant/AuthError, silently clear the store and emit Unauthenticated."
  - "logout() attempts end-session first, then always clears the secure store and emits Unauthenticated."

requirements-completed: [AUTH-01, AUTH-02, AUTH-04, AUTH-05]

duration: 34m
completed: 2026-04-28
---

# Phase 02 Plan 06: Common Auth Runtime Summary

**AuthSession state machine, token-safe Ktor bearer client, /api/v1/me client, and Koin singleton wiring for persisted OIDC sessions.**

## Performance

- **Duration:** 34 min
- **Started:** 2026-04-28T14:22:01Z
- **Completed:** 2026-04-28T14:56:05Z
- **Tasks:** 3
- **Files modified:** 7

## Accomplishments

- Added common AuthSession behavior for Loading -> restored Authenticated/Unauthenticated, login, logout, proactive refresh, and Ktor reactive refresh support.
- Added AuthState with Phase 3-ready `householdId: HouseholdId? = null`, with tests asserting Phase 2 authenticated sessions keep it null.
- Added MeClient for `GET /api/v1/me`, mapping server MeResponse to User so authenticated state is built from the server, not token claims.
- Added AuthHttpClient with Ktor bearer `loadTokens`, `refreshTokens`, `sendWithoutRequest`, ContentNegotiation JSON, and token-redacting logging.
- Wired authModule into appModule as Koin singletons without changing Koin startup ownership.

## Task Commits

1. **Task 1: Write AuthSession state-machine tests** - `06e5eaf` (test)
2. **Task 2: Implement AuthState, AuthSession, MeClient, and bearer HTTP client** - `0a24be9` (feat)
3. **Task 3: Wire authModule into Koin** - `938f324` (feat)

## Files Created/Modified

- `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/AuthState.kt` - Loading/Unauthenticated/Authenticated auth model with nullable household id.
- `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/AuthSession.kt` - StateFlow auth owner with restore/login/logout/token refresh behavior and testable gateway seams.
- `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/AuthHttpClient.kt` - Ktor client factory with bearer auth refresh and token redaction.
- `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/MeClient.kt` - `/api/v1/me` client mapped to shared User.
- `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/AuthModule.kt` - Koin singleton definitions for auth runtime collaborators.
- `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt` - Includes authModule from the app bootstrap module.
- `composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/auth/AuthSessionTest.kt` - State-machine tests for restore, login, invalid_grant/AuthError, logout, and cancellation.

## Decisions Made

- Kept platform OidcClient and SecureAuthStateStore expect/actual contracts unchanged; AuthSession uses gateway interfaces internally so common tests can fake dependencies.
- Used explicit token passing only for AuthSession's `/me` call. Broader authenticated API access goes through AuthHttpClient and its bearer plugin.
- No auth UI ViewModels were registered because they do not exist yet in this plan's input set.

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 3 - Blocking] Added testable gateway seams for AuthSession dependencies**

- **Found during:** Task 1/2 (state-machine tests and implementation)
- **Issue:** The plan required fakes for OidcClient and SecureAuthStateStore, but the existing common contracts are concrete expect classes. Changing expect/actual signatures would have touched platform files outside this plan's write scope.
- **Fix:** Added small common interfaces (`OidcClientGateway`, `AuthStateStore`, `MeGateway`) and made AuthSession's production constructor delegate concrete platform classes through adapters.
- **Files modified:** `AuthSession.kt`, `MeClient.kt`, `AuthSessionTest.kt`
- **Verification:** `./gradlew :composeApp:jvmTest --tests "*AuthSessionTest*"` and full plan gate passed.
- **Committed in:** `0a24be9`

**2. [Rule 3 - Blocking] Added explicit Koin generic types**

- **Found during:** Task 3 verification
- **Issue:** Koin's `single { ... }` calls could not infer expect-class singleton types under the KMP compile targets.
- **Fix:** Changed definitions to `single<SecureAuthStateStore>`, `single<OidcClient>`, `single<MeClient>`, and `single<AuthSession>`, with typed `get<...>()` calls.
- **Files modified:** `AuthModule.kt`
- **Verification:** `./gradlew :composeApp:jvmTest :composeApp:compileDebugKotlinAndroid :composeApp:compileKotlinIosSimulatorArm64`
- **Committed in:** `938f324`

---

**Total deviations:** 2 auto-fixed (2 x Rule 3).
**Impact on plan:** No scope expansion beyond common auth runtime and DI wiring. Both fixes were required to satisfy the planned tests and cross-target compile gate while respecting the write scope.

## Issues Encountered

- The RED test commit was amended before GREEN to make JUnit test methods return void while still failing on missing production auth runtime. This preserved the TDD red gate without adding a separate formatting-only commit.
- Pre-existing untracked `.claude/` and `AGENTS.md` remain untouched.

## Known Stubs

None.

## Threat Flags

None beyond the plan's threat model. The new network client, bearer refresh, secure-store access, and AuthSession UI state surfaces were all covered by T-02-06-01 through T-02-06-05.

## User Setup Required

None for this plan. Real OIDC login still requires the Authentik provider setup documented in `docs/authentik-setup.md`.

## Verification

- `./gradlew :composeApp:jvmTest --tests "*AuthSessionTest*"` - PASS
- `./gradlew :composeApp:jvmTest :composeApp:compileDebugKotlinAndroid :composeApp:compileKotlinIosSimulatorArm64` - PASS
- Task acceptance greps for `invalid_grant|AuthError`, `householdId`, `StateFlow<AuthState>`, `refreshTokens`, `sendWithoutRequest`, no `Authorization.*$`, `val authModule`, and appModule `authModule` - PASS
- Token/logging scan - PASS; no bearer token values or AuthState JSON are logged.

## Next Phase Readiness

Plan 02-07 can run integration verification against the common AuthSession + platform AppAuth actuals. Phase 3 can extend `/api/v1/me` with household data and fill `AuthState.Authenticated.householdId` without changing the sealed auth state shape.

## Self-Check: PASSED

- Created/modified files exist: all seven plan-owned source/test files plus this summary were found.
- Commits exist: `06e5eaf`, `0a24be9`, and `938f324` were found in git history.
- Acceptance criteria: all task grep checks passed.
- Plan-level verification: `./gradlew :composeApp:jvmTest :composeApp:compileDebugKotlinAndroid :composeApp:compileKotlinIosSimulatorArm64` passed.

---
*Phase: 02-authentication-foundation*
*Completed: 2026-04-28*
