Tasks completed: 2/2
- Define common OIDC and secure store contracts
- Add JVM and Wasm actuals
SUMMARY: .planning/phases/02-authentication-foundation/02-03-SUMMARY.md
- Add OIDC result and expect client seam with pinned native AppAuth semantics
- Add secure AuthState JSON store contract and JVM dev actuals for test compilation
Adds the per-plan SUMMARY for 02-01: shared MeResponse/User DTOs +
Constants, full Phase 2 dependency catalog wired into composeApp/
server without bumping Ktor 3.4.1, and docs/authentik-setup.md
reproducible-provider playbook with multi-source audit.
3 tasks (Task 1 ran TDD: RED + GREEN), 4 commits, 6 deviations
auto-fixed (5 × Rule 3 blocking, 1 × Rule 1 bug), 0 scope creep.
All plan-level verifications PASS.
Per parallel-execution rules, this commit does not modify STATE.md
or ROADMAP.md — the orchestrator owns those updates after the wave
completes.
Task 02-01-03. Creates docs/authentik-setup.md as the load-bearing
Phase 2 deliverable (D-10): a reproducible playbook for the
homelab Authentik provider plus the multi-source audit that ties
every Phase 2 input to a covering plan.
Sections (in mandated order):
- Provider — Public + PKCE S256, recipe-app client_id, RS256, single-
string aud, JWKS URI, end-session endpoint, Issuer trailing slash.
- Scopes — exactly `openid profile email offline_access`; explains
why offline_access must be both requested AND mapped on the
provider for refresh tokens (PITFALLS.md Phase 2 Pitfall 2).
- Redirect URI — recipe://callback, registered byte-for-byte in
Authentik + iOS Info.plist + Android <intent-filter>.
- Server Env Vars — OIDC_ISSUER / OIDC_AUDIENCE / OIDC_JWKS_URL with
override semantics matching Phase 1's DATABASE_URL pattern.
- Logout — RP-initiated end-session sequence (D-19, D-20).
- Manual UAT — UAT-01 fresh login, UAT-02 reopen with refresh,
UAT-03 logout returns to login, UAT-04 curl/HTTP verification of
GET /api/v1/me 200/401 cases including wrong-aud and never-log-
Authorization assertion.
- Source Audit — exhaustive table mapping GOAL Phase 2, REQ
AUTH-01..AUTH-06, RESEARCH constraints, CONTEXT D-01..D-34,
UI-SPEC, VALIDATION Wave 0, and PATTERNS file map to either this
doc (✅) or a downstream Phase 2 plan (⤳). All deferred ideas
listed as ✂ excluded: Universal Links/App Links, real Desktop
OIDC, Wasm OIDC, Apple Sign-in, Authentik provisioning automation,
per-user AuthState, modal refresh-failure UX, background refresh,
two-tier logout, BuildConfig OIDC injection, real-Authentik
integration tests.
Verification:
- grep -E 'openid profile email offline_access|PKCE S256|single-string
|recipe://callback|/api/v1/me|Source Audit' docs/authentik-setup.md:
hits all six tokens.
- All Task 3 grep acceptance criteria PASS, including
AUTH-01.*AUTH-02.*AUTH-03.*AUTH-04.*AUTH-05.*AUTH-06 on a single
audit-table line and "Universal Links / App Links.*excluded".
Task 02-01-02. Adds Phase 2 deps to the version catalog and routes
them into composeApp + server build files. Ktor stays pinned at
3.4.1 per the resolved Open Question — patch bump deferred unless a
concrete incompatibility appears.
Catalog (gradle/libs.versions.toml):
- Versions: appauth, appauth-ios, androidx-security-crypto, exposed,
hikari, multiplatformSettings, testcontainers, plus the
kotlinCocoapods plugin alias.
- Libraries: ktor server auth/auth-jwt/call-logging/status-pages,
ktor client core/auth/content-negotiation/logging/okhttp/darwin/cio,
ktor-serializationKotlinxJsonMpp (the multiplatform variant; the
-jvm one stays for server), AppAuth, AndroidX Security Crypto,
multiplatform-settings + coroutines, Exposed core/jdbc/java-time,
HikariCP, Testcontainers postgresql + junit-jupiter.
composeApp/build.gradle.kts:
- Apply kotlinSerialization (alias) and kotlin.native.cocoapods (by
id — the plugin is shipped inside the Kotlin Gradle plugin already
on the classpath via recipe.kotlin.multiplatform; alias-applying
it would request a fresh version and fail).
- Cocoapods block: ComposeApp baseName + isStatic, ../iosApp/Podfile,
iOS deployment target 15.0, AppAuth pod pulled from
libs.versions.appauth.ios.get() — no literal pin in the build
file (verify-no-version-literals.sh stays green).
- Common deps: Ktor client family, MPP serialization, multiplatform
settings; Android: AppAuth-Android + Security Crypto + OkHttp
engine; iOS: Darwin engine; JVM: CIO engine.
server/build.gradle.kts: Adds Ktor server auth/JWT/CallLogging/
StatusPages, Exposed DSL trio, Hikari, kotlinx.serialization-json,
plus testImplementation testcontainers postgresql + junit-jupiter.
Deviations:
- Rule 3 (blocking): manifestPlaceholders["appAuthRedirectScheme"]
= "recipe" added to Android defaultConfig because AppAuth-Android's
bundled manifest declares a ${appAuthRedirectScheme} placeholder
that breaks AGP merge before Plan 02-04 lands the full <intent-filter>.
- Rule 3 (blocking): top-level group/version on composeApp (required
by the cocoapods podspec generator) pushes the Compose Resources
Res-class package off recipe.composeapp.generated.resources, breaking
Phase 1 App.kt imports. Lock the package via compose.resources {
packageOfResClass = "recipe.composeapp.generated.resources" }.
- Rule 3 (housekeeping): *.podspec is generated by the cocoapods
plugin on every build; ignored.
Verification:
- ./gradlew :composeApp:dependencies --configuration debugCompileClasspath
:server:dependencies --configuration runtimeClasspath: PASS
(the plan-stated androidMainCompileClasspath name doesn't exist
under this AGP/Gradle combo; debugCompileClasspath is the
functional equivalent and resolves all new deps).
- ./gradlew :composeApp:compileDebugKotlinAndroid :server:compileKotlin: PASS
- ./gradlew :composeApp:compileKotlinIosSimulatorArm64: PASS (cinterop
pulls AppAuth pod cleanly).
- ./tools/verify-no-version-literals.sh: PASS
- ./tools/verify-shared-pure.sh: PASS
- All Task 2 grep acceptance criteria satisfied.
GREEN phase of TDD task 02-01-01. Adds the load-bearing Phase 2
contract that downstream plans compile against:
- Constants.kt: OIDC_ISSUER (trailing slash, placeholder homelab host),
OIDC_CLIENT_ID = recipe-app, OIDC_REDIRECT_URI = recipe://callback,
API_BASE_URL, plus moved SERVER_PORT for one shared config object.
- dto/User.kt: domain identity (id/sub/email/displayName), id is String
to keep shared free of UUID library deps (D-19 / INFRA-06).
- dto/MeResponse.kt: @Serializable wire DTO for GET /api/v1/me with a
one-to-one toUser() mapper. Stable for Phase 3 to add householdId
via ignoreUnknownKeys.
- Removes the now-redundant shared/.gitkeep placeholder.
Verification:
- ./gradlew :shared:jvmTest :shared:compileCommonMainKotlinMetadata: PASS
- ./tools/verify-shared-pure.sh: PASS
- All grep acceptance criteria for Task 1 satisfied
RED phase of TDD task 02-01-01. Locks the wire-format contract for
GET /api/v1/me before the DTO exists:
- camelCase JSON keys (id, sub, email, displayName) per D-27
- ignoreUnknownKeys forward compat for Phase 3 householdId per D-28
- MeResponse.toUser() one-to-one mapping
Wires kotlinx.serialization into shared/build.gradle.kts (api scope so
both client and server inherit the @Serializable runtime) and adds the
kotlinx-serializationJson catalog alias. The shared module remains
pure: only kotlin stdlib + kotlinx.serialization-json are pulled into
commonMain (D-19 / INFRA-06 still holds).
Test currently fails: MeResponse and User unresolved; GREEN follows.
UI-SPEC verified — all 6 dimensions PASS. No flags. Frontmatter
status flipped from draft to approved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Locks scaffold-level visual contract for Phase 2: spacing scale,
typography roles, Material 3 color seed, copywriting keys, and
auth-gate routing — without committing to Liquid-Glass / Haze
(Phase 10) or final font + Polish polish (Phase 11).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>