docs(02-01): add Authentik provider setup and Phase 2 source audit
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".
This commit is contained in:
242
docs/authentik-setup.md
Normal file
242
docs/authentik-setup.md
Normal file
@@ -0,0 +1,242 @@
|
||||
# Authentik Provider Setup — Recipe (Phase 2)
|
||||
|
||||
> Reproducible Authentik configuration for the Recipe app. Anyone with admin access
|
||||
> to the homelab Authentik should be able to recreate the OAuth2/OIDC provider in
|
||||
> under five minutes by following this document end to end (D-10).
|
||||
>
|
||||
> Phase: `02-authentication-foundation`. Locked decisions referenced here live in
|
||||
> `.planning/phases/02-authentication-foundation/02-CONTEXT.md` (D-05 .. D-10,
|
||||
> D-19, D-21 .. D-23) and the version-controlled requirements in
|
||||
> `.planning/REQUIREMENTS.md` (AUTH-01, AUTH-02, AUTH-03, AUTH-04, AUTH-05, AUTH-06).
|
||||
|
||||
## Provider
|
||||
|
||||
Configure a **single OAuth2/OIDC provider** in Authentik with the following pinned settings.
|
||||
Authentik admin path: **Admin → Applications → Providers → Create → OAuth2/OpenID Provider**.
|
||||
|
||||
| Setting | Value | Why this exact value |
|
||||
|---------|-------|----------------------|
|
||||
| Provider type | **OAuth2/OIDC, Public client** | Mobile apps cannot ship a secret per RFC 8252 (D-05). No `client_secret` is set on the provider, sent on the wire, or stored anywhere in this repo. |
|
||||
| Authorization flow | **Authorization code with PKCE S256** | PKCE S256 is the only safe pattern for native + custom-scheme redirect URIs (D-05, D-09). `plain` is forbidden. |
|
||||
| Client ID | **`recipe-app`** | Mirrors `dev.ulfrx.recipe.shared.Constants.OIDC_CLIENT_ID`. The same value is the JWT `aud` claim per D-07. |
|
||||
| Client secret | **(leave empty)** | Public client — D-05. Any non-empty value is a bug. |
|
||||
| Redirect URIs | **`recipe://callback`** (exactly, no trailing slash, no spaces) | Custom URL scheme — see [`## Redirect URI`](#redirect-uri). Byte-for-byte match with `Constants.OIDC_REDIRECT_URI` (D-09). |
|
||||
| Signing algorithm | **RS256** | Authentik default; matches `JwkProviderBuilder` expectations on the server (D-08, D-21, D-22). |
|
||||
| Signing key | RS256 asymmetric key from Authentik (auto-managed) | Public key reaches the server through the JWKS endpoint, never copied or pinned in code (D-22). |
|
||||
| Audience | **single-string** value `aud = recipe-app` (NOT array) | Authentik can emit `aud` as either an array or a single string per provider config; pin to single string and let `JWTAuth.withAudience("recipe-app")` validate against it (D-07, PITFALLS.md #7). A negative test in Plan 02-02 asserts wrong-`aud` → 401. |
|
||||
| Issuer URL | `https://auth.<your-homelab>.tld/application/o/recipe/` (trailing slash required) | Trailing slash is byte-sensitive in Authentik's OpenID metadata responses (PITFALLS.md #8). The placeholder host `auth.example.invalid` in `Constants.kt` is replaced at deploy time via env-var override on the server (`OIDC_ISSUER`) — do not commit your real homelab URL here. |
|
||||
| JWKS URI | Pulled from `<issuer>/.well-known/openid-configuration` `jwks_uri` (typically `<issuer>jwks/`) | Cached and rate-limited on the server with `JwkProviderBuilder(issuer).cached(10, 15, MINUTES).rateLimited(10, 1, MINUTES)` (D-22). |
|
||||
| End-session endpoint | Pulled from `<issuer>/.well-known/openid-configuration` `end_session_endpoint` | Required for RP-initiated logout (D-19, D-20). See [`## Logout`](#logout). |
|
||||
| Token validity | Access token ~5 min, refresh token long-lived (default Authentik) | Short access lifetime exercises the `performActionWithFreshTokens` + Ktor bearer 401 fallback paths (D-16, D-17). |
|
||||
|
||||
After saving, bind the provider to a new **Application** (Admin → Applications → Applications → Create) called `Recipe`. The application is what Authentik users see in their My Apps / consent screens.
|
||||
|
||||
## Scopes
|
||||
|
||||
The app requests **exactly** these four scopes from Authentik:
|
||||
|
||||
```
|
||||
openid profile email offline_access
|
||||
```
|
||||
|
||||
| Scope | Purpose | Where it lands |
|
||||
|-------|---------|----------------|
|
||||
| `openid` | Marks the request as OIDC; Authentik issues an ID token | Mandatory; without it Authentik issues OAuth2-only tokens that are unusable here (D-06) |
|
||||
| `profile` | Populates the `name` / `preferred_username` claim | Maps to `users.display_name` via JIT provisioning (D-25) |
|
||||
| `email` | Populates the `email` claim | Maps to `users.email` via JIT provisioning (D-25) |
|
||||
| `offline_access` | Asks Authentik to issue a **refresh** token | AUTH-04 (session persists across launches via refresh) is impossible without it. Authentik issues no refresh token unless `offline_access` is BOTH requested by the client AND mapped/allowed in the provider's scope mapping (D-06, PITFALLS.md Phase 2 Pitfall 2). |
|
||||
|
||||
**Authentik provider configuration must explicitly map the `offline_access` scope** so the
|
||||
provider returns a refresh token. Newer Authentik versions add it by default; older ones
|
||||
require explicit creation under **Customization → Property Mappings → OAuth2 / OpenID Scope
|
||||
Mapping**.
|
||||
|
||||
## Redirect URI
|
||||
|
||||
The app uses a **custom URL scheme**:
|
||||
|
||||
```
|
||||
recipe://callback
|
||||
```
|
||||
|
||||
This single URI must be registered three times — byte-for-byte identical, no trailing
|
||||
slash, no query parameters. Drift between any of these three places produces silent OAuth
|
||||
redirect failures (D-09).
|
||||
|
||||
| Where | Mechanism | Phase 2 plan that lands it |
|
||||
|-------|-----------|----------------------------|
|
||||
| Authentik provider | Redirect URIs textbox (one line) | This document (Plan 02-01) |
|
||||
| iOS | `iosApp/iosApp/Info.plist` `CFBundleURLTypes` → `CFBundleURLSchemes` array containing exactly `recipe` | Plan 02-05 (iOS AppAuth actual) |
|
||||
| Android | `composeApp/src/androidMain/AndroidManifest.xml` `<intent-filter>` on AppAuth's `RedirectUriReceiverActivity` with `android:scheme="recipe"` and `android:host="callback"` | Plan 02-04 (Android AppAuth actual). Plan 02-01 already supplies the `appAuthRedirectScheme=recipe` manifest placeholder so the AppAuth dependency merges cleanly without yet wiring the receiver. |
|
||||
|
||||
PKCE S256 + AppAuth's state/nonce handling makes the well-known custom-scheme
|
||||
interception attack non-exploitable in practice. **Universal Links / App Links are
|
||||
explicitly excluded** from v1 — see [`## Source Audit`](#source-audit).
|
||||
|
||||
## Server Env Vars
|
||||
|
||||
The Ktor server reads OIDC configuration from `application.conf` with env-var overrides
|
||||
(D-12, mirrors Phase 1 D-16's `DATABASE_URL` pattern). Set these on the homelab deploy
|
||||
target before booting the server:
|
||||
|
||||
| Variable | Required | Example value | Notes |
|
||||
|----------|----------|---------------|-------|
|
||||
| `OIDC_ISSUER` | yes | `https://auth.example.invalid/application/o/recipe/` | Trailing slash required (PITFALLS.md #8). Must equal Authentik's issuer URL byte-for-byte. |
|
||||
| `OIDC_AUDIENCE` | yes | `recipe-app` | Equal to the provider's Client ID; validated as a **single string** (D-07). |
|
||||
| `OIDC_JWKS_URL` | optional | `https://auth.example.invalid/application/o/recipe/jwks/` | Optional — derived from `<OIDC_ISSUER>/.well-known/openid-configuration` if unset. Set explicitly only when Authentik puts JWKS at a non-standard path. |
|
||||
|
||||
Plan 02-02 wires these into `application.conf` and the `JwkProviderBuilder`. They are NOT
|
||||
read by the client — the client OIDC config is hardcoded in
|
||||
`shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/Constants.kt` per D-11 (single-
|
||||
environment v1 acceptance from PITFALLS.md tech-debt table).
|
||||
|
||||
## Logout
|
||||
|
||||
Logout is **RP-initiated end-session** (D-19, D-20). Tapping "Wyloguj się" performs both
|
||||
of these atomically, in this order:
|
||||
|
||||
1. Call Authentik's `end_session_endpoint` (advertised by `<issuer>/.well-known/openid-configuration`) with the user's `id_token_hint`. AppAuth's `EndSessionRequest` API drives this on both mobile platforms (D-20).
|
||||
2. Delete the persisted `AuthState` blob from secure storage (Keychain on iOS, EncryptedSharedPreferences on Android per D-13).
|
||||
|
||||
If step 1 fails (network unreachable, Authentik down), step 2 still runs so the user
|
||||
isn't trapped in a half-logged-out state — correct semantics for a shared household
|
||||
device. Local-only logout would fail AUTH-05 because the next "Zaloguj się" tap would
|
||||
silently SSO instead of forcing fresh credentials.
|
||||
|
||||
To support this, the Authentik provider's **end-session endpoint must be reachable** from
|
||||
the device — confirm via `curl -I "<issuer>/.well-known/openid-configuration"` and
|
||||
checking that the JSON response contains `end_session_endpoint`.
|
||||
|
||||
## Manual UAT
|
||||
|
||||
These checks must pass on a real iOS device or simulator before Phase 2 is signed off
|
||||
(per `02-VALIDATION.md`'s Manual-Only Verifications). They cover the surface unit tests
|
||||
cannot reach: real Authentik, real browser handoff, real Keychain.
|
||||
|
||||
### UAT-01 — Fresh iOS login (AUTH-01)
|
||||
|
||||
1. Wipe app data: delete the app from the simulator/device.
|
||||
2. Reinstall via `./gradlew :composeApp:iosDeployIPhone…` or Xcode.
|
||||
3. Tap **"Zaloguj się przez Authentik"**.
|
||||
4. Confirm the system browser (`ASWebAuthenticationSession`) opens at Authentik's hosted login page.
|
||||
5. Authenticate. Confirm Authentik consent screen lists `openid profile email offline_access`.
|
||||
6. Confirm the app returns to foreground via `recipe://callback` and renders `Witaj, {displayName}!` with the partner's actual display name.
|
||||
7. **Failure modes to verify visually:** cancelling the system browser shows "Logowanie anulowane. Spróbuj ponownie." inline.
|
||||
|
||||
### UAT-02 — Reopen with refresh (AUTH-04)
|
||||
|
||||
1. Sign in via UAT-01.
|
||||
2. In Authentik, set the provider's access-token lifetime to ~60 seconds (or wait the default).
|
||||
3. Backgroud the app for ~2 minutes; relaunch.
|
||||
4. Confirm the app returns directly to `Witaj, {displayName}!` with no login prompt — the AppAuth `performActionWithFreshTokens` path silently exchanged the refresh token (D-16, D-17).
|
||||
|
||||
### UAT-03 — Logout returns to login (AUTH-05)
|
||||
|
||||
1. Sign in via UAT-01.
|
||||
2. Tap **"Wyloguj się"**.
|
||||
3. Confirm the app returns to the LoginScreen.
|
||||
4. Tap "Zaloguj się przez Authentik" again. Confirm Authentik **prompts for credentials** (no silent SSO) — proves the end-session call succeeded.
|
||||
5. **Network-failure variant:** disconnect from network, tap "Wyloguj się", confirm app still returns to LoginScreen and the local AuthState is gone (relaunching does not auto-sign-in).
|
||||
|
||||
### UAT-04 — `/api/v1/me` validation (AUTH-03)
|
||||
|
||||
This is the only UAT step performed via terminal, not the app. It validates the server
|
||||
side of the boundary independently of mobile UI bugs.
|
||||
|
||||
1. Run the server locally or hit the homelab. Capture a valid access token (e.g., copy from `AuthState` JSON via the iOS debugger immediately after UAT-01, or use `curl` directly against Authentik's token endpoint with the same client_id).
|
||||
2. Confirm:
|
||||
```bash
|
||||
curl -i -H "Authorization: Bearer $TOKEN" https://api.<homelab>/api/v1/me
|
||||
# expect: HTTP/1.1 200 OK; body matches MeResponse {"id":..., "sub":..., "email":..., "displayName":...}
|
||||
```
|
||||
3. Confirm:
|
||||
```bash
|
||||
curl -i https://api.<homelab>/api/v1/me
|
||||
# expect: HTTP/1.1 401 Unauthorized
|
||||
```
|
||||
4. Confirm wrong-audience rejection by minting a JWT with `aud != recipe-app` (use the test JWKS Plan 02-02 ships):
|
||||
```bash
|
||||
curl -i -H "Authorization: Bearer $WRONG_AUD_TOKEN" https://api.<homelab>/api/v1/me
|
||||
# expect: HTTP/1.1 401 Unauthorized
|
||||
```
|
||||
5. Confirm logs do **not** contain the token body. The custom `CallLogging` filter must redact the `Authorization` header (D-23).
|
||||
|
||||
## Source Audit
|
||||
|
||||
This document is the Phase 2 anchor for "every locked source is honored". The table below
|
||||
asserts that every Phase 2 input — goal, requirement, research finding, decision, UI
|
||||
spec, validation gate, and pattern map — is either covered here or in a downstream Phase
|
||||
2 plan. Markers: ✅ covered in this document, ⤳ covered in a downstream plan (with
|
||||
plan number), ✂ explicitly deferred (see end of section).
|
||||
|
||||
| Source | Item | Coverage |
|
||||
|--------|------|----------|
|
||||
| GOAL | Phase 2 goal: end-to-end OIDC+PKCE login with server JWT validation and JIT users | ✅ Provider + Scopes + Redirect URI + Server Env Vars + Manual UAT |
|
||||
| REQ | **AUTH-01** sign in via Authentik OIDC + PKCE | ✅ Provider; ⤳ 02-04 (Android AppAuth) + 02-05 (iOS AppAuth) |
|
||||
| REQ | **AUTH-02** secure token storage | ⤳ 02-03 (common contract) + 02-04 (Android EncryptedSharedPreferences) + 02-05 (iOS Keychain) |
|
||||
| REQ | **AUTH-03** server JWT validation via JWKS | ✅ Provider (RS256, single-string aud, JWKS); ⤳ 02-02 (Ktor JWT install + tests) |
|
||||
| REQ | **AUTH-04** session persists across launches via refresh | ✅ Scopes (`offline_access`); ⤳ 02-03 (AuthSession refresh wiring) + Manual UAT-02 |
|
||||
| REQ | **AUTH-05** logout returns to login | ✅ Logout section; ⤳ 02-04/02-05 (AppAuth end-session per platform) + Manual UAT-03 |
|
||||
| REQ | **AUTH-06** JIT user provisioning by `sub` | ⤳ 02-02 (`V1__users.sql` + upsert by sub + `/api/v1/me`) |
|
||||
| RESEARCH | Standard stack: AppAuth, Ktor JWT, multiplatform-settings, Exposed DSL, Flyway | ✅ Server Env Vars (Ktor); ⤳ 02-01 catalog wiring (this plan, task 2) + per-platform plans |
|
||||
| RESEARCH | Open Questions resolved: Android secure storage = EncryptedSharedPreferences behind `SecureAuthStateStore` seam | ⤳ 02-03 (seam) + 02-04 (Android impl) |
|
||||
| RESEARCH | Open Question resolved: Exposed `newSuspendedTransaction` import verified at impl time | ⤳ 02-02 |
|
||||
| RESEARCH | Open Question resolved: Ktor stays at 3.4.1 (no patch bump) | ✅ Task 2 catalog keeps `ktor = "3.4.1"` |
|
||||
| CONTEXT | **D-01** AppAuth on both mobile platforms via expect/actual `OidcClient` | ⤳ 02-03 (expect) + 02-04 (Android actual) + 02-05 (iOS actual) |
|
||||
| CONTEXT | **D-02** JVM `actual` is `DEV_AUTH_TOKEN` env-var stub | ⤳ 02-03 |
|
||||
| CONTEXT | **D-03** Wasm `actual` is `NotImplementedError("Wasm OIDC: v2")` | ⤳ 02-03 |
|
||||
| CONTEXT | **D-04** `OidcClient.login()` / `.refresh()` are `suspend` | ⤳ 02-03 |
|
||||
| CONTEXT | **D-05** Public + PKCE S256 | ✅ Provider |
|
||||
| CONTEXT | **D-06** scopes `openid profile email offline_access` | ✅ Scopes |
|
||||
| CONTEXT | **D-07** single-string `aud` = `client_id` | ✅ Provider |
|
||||
| CONTEXT | **D-08** RS256 signing | ✅ Provider |
|
||||
| CONTEXT | **D-09** redirect URI `recipe://callback` | ✅ Redirect URI |
|
||||
| CONTEXT | **D-10** this document is a Phase 2 deliverable | ✅ this document |
|
||||
| CONTEXT | **D-11** client OIDC config in `shared/commonMain/Constants.kt` | ✅ Server Env Vars (relationship spelled out); ⤳ 02-01 task 1 (Constants.kt landed) |
|
||||
| CONTEXT | **D-12** server OIDC config via env vars | ✅ Server Env Vars |
|
||||
| CONTEXT | **D-13** persist full AppAuth `AuthState` JSON via `multiplatform-settings` | ⤳ 02-03 + 02-04 + 02-05 |
|
||||
| CONTEXT | **D-14** iOS Keychain `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly` | ⤳ 02-05 |
|
||||
| CONTEXT | **D-15** one AuthState blob per app install | ⤳ 02-03 |
|
||||
| CONTEXT | **D-16** proactive refresh via `performActionWithFreshTokens` | ⤳ 02-04 + 02-05 |
|
||||
| CONTEXT | **D-17** Ktor bearer `refreshTokens` 401 fallback | ⤳ 02-03 |
|
||||
| CONTEXT | **D-18** silent refresh-failure transition | ⤳ 02-03 |
|
||||
| CONTEXT | **D-19** RP-initiated end-session | ✅ Logout |
|
||||
| CONTEXT | **D-20** AppAuth `EndSessionRequest` drives logout | ✅ Logout; ⤳ 02-04 + 02-05 |
|
||||
| CONTEXT | **D-21** Ktor `jwt("authentik")` install with leeway 30s and `sub` validation | ⤳ 02-02 |
|
||||
| CONTEXT | **D-22** JWKS provider cache 10 / 15min, rate limit 10/min | ⤳ 02-02 |
|
||||
| CONTEXT | **D-23** never log Authorization header / token bodies | ✅ Manual UAT-04 step 5; ⤳ 02-02 (server filter) + 02-03 (client logger discipline) |
|
||||
| CONTEXT | **D-24** ship `V1__users.sql` migration | ⤳ 02-02 |
|
||||
| CONTEXT | **D-25** JIT upsert by `sub`, update email/display_name | ⤳ 02-02 |
|
||||
| CONTEXT | **D-26** Exposed DSL only, `newSuspendedTransaction` | ⤳ 02-02 |
|
||||
| CONTEXT | **D-27** `/api/v1/me` returns `MeResponse` | ✅ Manual UAT-04; ⤳ 02-01 task 1 (DTO) + 02-02 (route) |
|
||||
| CONTEXT | **D-28** `AuthState` sealed shape with `householdId: HouseholdId? = null` | ⤳ 02-03 |
|
||||
| CONTEXT | **D-29** `AuthSession` Koin singleton in `authModule` | ⤳ 02-03 + 02-06 |
|
||||
| CONTEXT | **D-30** auth gate in `App()` | ⤳ 02-06 (UI) |
|
||||
| CONTEXT | **D-31** minimal login screen | ⤳ 02-06 |
|
||||
| CONTEXT | **D-32** inline login error states | ⤳ 02-06 |
|
||||
| CONTEXT | **D-33** post-login placeholder `Witaj, {displayName}!` | ⤳ 02-06 |
|
||||
| CONTEXT | **D-34** Compose Resources strings from day 1 | ⤳ 02-06 |
|
||||
| UI-SPEC | Auth screen contract: SplashScreen / LoginScreen / PostLoginPlaceholderScreen | ⤳ 02-06 |
|
||||
| VALIDATION | Wave 0 tests: AuthJwtTest, MeRouteTest, AuthSessionTest, SecureAuthStateStoreTest | ⤳ 02-02 (server tests) + 02-03 (client tests) |
|
||||
| VALIDATION | Manual UAT checklist in `docs/authentik-setup.md` | ✅ Manual UAT |
|
||||
| PATTERNS | File map: shared DTO/Constants location, Koin authModule, Ktor JWT install, Exposed table, AppAuth platform actuals | ⤳ 02-01 task 1 (shared) + 02-02 (server) + 02-03 (client common) + 02-04 (Android) + 02-05 (iOS) + 02-06 (UI) |
|
||||
|
||||
### Deferred (excluded from Phase 2)
|
||||
|
||||
These are explicitly out of scope for v1 per `.planning/phases/02-authentication-foundation/02-CONTEXT.md` § Deferred Ideas. Listed here so the audit makes the exclusions traceable.
|
||||
|
||||
- **Universal Links / App Links** — excluded; rely on `recipe://callback` custom scheme. Revisit only if app gains broader distribution beyond the household or if Apple/Google deprecate custom-scheme OIDC redirects.
|
||||
- **Real Desktop OIDC** — JVM target ships a `DEV_AUTH_TOKEN` env-var stub (D-02). Loopback-redirect implementation deferred until Desktop becomes a release surface.
|
||||
- **Wasm OIDC implementation** — `wasmJs` actual throws `NotImplementedError`. Browser-redirect flow deferred until Wasm becomes a release surface.
|
||||
- **Apple Sign-in as a first-class button** — Authentik can federate Apple upstream if ever desired.
|
||||
- **Authentik provisioning automation (Terraform/Ansible)** — this document is the manual reproduction playbook; automation deferred post-v1.
|
||||
- **JWT validation tests against a real Authentik instance** — Phase 2 ships unit/integration tests with hand-crafted JWTs. Real-Authentik integration tests deferred to Phase 11 (deployment).
|
||||
- **BuildConfig-style Gradle injection of OIDC config** — `Constants.kt` is the v1 single-environment acceptance per PITFALLS.md tech-debt table.
|
||||
- **Per-user persisted `AuthState`** — one user per install is the v1 model.
|
||||
- **Modal/toast for refresh-failure UX** — silent transition ships in v1 (D-18).
|
||||
- **Background token refresh** — v1 has no background work.
|
||||
- **"Wyloguj się i zapomnij sesję" two-tier logout** — single RP-initiated logout only.
|
||||
|
||||
---
|
||||
|
||||
*Phase: `02-authentication-foundation` · Plan: `02-01` · Last updated: 2026-04-28*
|
||||
Reference in New Issue
Block a user