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>
- Replace testRoot template assertion with 'health endpoint returns 200 with status ok'
- Compose only configureRouting() in testApplication — NOT Application.module()
- This keeps the test independent of Database.migrate / running Postgres (D-11 test invariant)
- Install ContentNegotiation { json() } inside application { } — production module() does it,
but the test composes routing directly and must install the plugin itself
- All imports explicit (D-11 allWarningsAsErrors); no wildcards
- Body checked via substring for "status" + "ok" — robust to JSON field ordering
Note: ./gradlew :server:test runtime verification deferred to Plan 07 (integration build)
since build-logic/recipe.jvm.server plugin is being authored in parallel Plan 02 worktree.
- Create MainApplication : Application() running configureLogging() then initKoin { androidContext(this@MainApplication) } in onCreate
- Register android:name=".MainApplication" on <application> element (MainActivity entry preserved)
- Establishes the canonical init order for Android process boot
- Single postgres service pinned to postgres:16
- Credentials recipe/recipe/recipe match application.conf HOCON defaults
- Named volume recipe-pgdata for persistence across restarts
- Healthcheck via pg_isready enables docker compose up --wait usage
- No version key (modern compose v2); Authentik stays on homelab (D-17)
Three executable bash scripts under tools/ that Wave 0 and every
subsequent Phase 1 plan's <automated> block rely on:
- verify-no-version-literals.sh (INFRA-01 SC#2 / D-09): no literal
library/plugin version strings in any *.gradle.kts. Excludes
build-logic/build.gradle.kts (needs asDependency() literals) and
top-level project-version assignments ("^version = \"x.y.z\"")
which are artifact metadata, not library pins.
- verify-shared-pure.sh (INFRA-06 / D-19): shared/commonMain must
not import Ktor/Compose/SQLDelight. Returns OK if the directory
does not exist yet (pre-scaffold tolerance for Plan 07).
- verify-ios-flags.sh (INFRA-03 / D-18): both K/N iOS binary flags
present in gradle.properties.
All three use bash (#!/usr/bin/env bash + set -euo pipefail) and
are marked chmod +x. Scripts exit 0 against the current repo state.