Auth-Stack Soll-Zustand¶
Source-of-Truth für den Auth-Stack der kora-Plattform. Drift- Verifikation prüft den Live-Zustand gegen dieses Dokument plus die Realm-JSON-Dateien (für deklarativ deklarierte Felder).
Aktualisierungs-Disziplin: Vor jeder Auth-Änderung wird zuerst dieses Dokument aktualisiert, danach Realm-JSON / Init-Skripte / Frontend-Konfig / NPMplus-Routing — und das
scripts/verify-auth-stack.shmuss am Ende grün sein. Verzweigte Änderungen ohne Doku-Update sind der Drift-Generator, den TODO-Platform-09 schließen soll.
1. Realms¶
| Realm | Zweck | Pfad-Konvention | Default-Realm-Role |
|---|---|---|---|
kora-platform |
Operator (AVS) + Vendor (luki-net) Auth-Surface | https://auth.kora.luki-net.org/realms/kora-platform/... |
default-roles-kora-platform (KC-Built-In, composite) |
kora-tenants |
Tenant-User Auth-Surface (Block 8 wird die Tenant-User-Verwaltung über die Operator-UI bauen) | https://auth.kora.luki-net.org/realms/kora-tenants/... |
default-roles-kora-tenants (KC-Built-In, composite) |
Beide Realms laufen in derselben Keycloak-Instanz (Container
kora-platform-keycloak, intern Port 8080, extern via NPMplus auf
auth.kora.luki-net.org). Keycloak-Version 24+ — moderne Pfad-
Konvention ohne /auth/-Präfix.
2. Clients¶
Built-In-Clients (account, account-console, admin-cli, broker,
realm-management, security-admin-console) werden von Keycloak
beim Realm-Anlegen automatisch erzeugt und sind nicht im Realm-JSON
deklariert. Sie sind nicht Teil des Drift-Verifikations-Scopes.
2.1 kora-platform Realm¶
| Client-ID | Type | PKCE | Default-Scopes (Soll) | Service-Account | Quelle (SoT) |
|---|---|---|---|---|---|
kora-api |
Confidential, bearer-only | — | kora-scope |
nein | Realm-JSON |
kora-api-vendor-breakglass |
Confidential | — | kora-scope |
nein | Realm-JSON |
kora-api-vendor-tunnel |
Confidential | — | kora-scope |
nein | Realm-JSON |
kora-platform-audit |
Confidential | — | kora-scope |
ja (view-events realm-management Rolle) |
create-audit-service-account.sh (Init-Skript, nicht im JSON — TODO-Platform-05) |
operator-ui |
Public | S256 |
kora-scope |
nein | Realm-JSON + create-operator-ui-client.sh (Init-Skript) |
2.1.1 operator-ui — Redirect-URIs + Web-Origins¶
Redirect-URIs (Set, Reihenfolge irrelevant):
https://platform.kora.luki-net.org/admin/operator/auth/callback
https://platform.kora.luki-net.org/admin/operator/*
http://localhost:5174/auth/callback
http://localhost:5174/*
http://localhost:8280/admin/operator/auth/callback
http://localhost:8280/admin/operator/*
Web-Origins (Set):
Post-Logout-Redirect-URIs (##-getrennt):
https://platform.kora.luki-net.org/admin/operator/*
https://platform.kora.luki-net.org/*
http://localhost:5174/*
http://localhost:8280/admin/operator/*
2.2 kora-tenants Realm¶
| Client-ID | Type | PKCE | Default-Scopes (Soll) | Quelle (SoT) |
|---|---|---|---|---|
kora-api |
Confidential, bearer-only | — | kora-scope |
Realm-JSON |
tenant-ui |
Public | S256 |
kora-scope |
Realm-JSON |
2.2.1 tenant-ui — Redirect-URIs + Web-Origins¶
Redirect-URIs:
https://platform.kora.luki-net.org/tenant/auth/callback
https://platform.kora.luki-net.org/tenant/*
http://localhost:5173/tenant/auth/callback
http://localhost:5173/tenant/*
http://localhost:8280/tenant/auth/callback
http://localhost:8280/tenant/*
Web-Origins:
3. Client-Scopes pro Realm¶
Beide Realms haben dieselbe Custom-Scope-Architektur — bewusst
minimal: nur eine eigene Scope (kora-scope) plus Keycloak-Built-In
offline_access. OIDC-Standard-Scopes (profile, email,
roles, address, phone, web-origins, acr,
microprofile-jwt) existieren nicht als Client-Scope-Objekte.
Diese Architektur-Entscheidung ist gewollt und Lessons-Learned aus
TODO-Platform-08:
kora-scopeenthält Mapper fürpreferred_username,email,realm_access.roles,aud=kora-api— alle Claims, die Frontend und Backend konsumieren- Frontend fragt nur
scope=openid kora-scopean.openidist der OIDC-Protokoll-Marker (id-Token-Issuance) und benötigt kein Client-Scope-Objekt; alle anderen Scope-Tokens müssen registrierte Client-Scope-Objekte sein, sonst lehnt Keycloak die Anfrage miterror=invalid_scopeab
3.1 kora-scope-Mapper¶
kora-platform (4 Mapper):
| Mapper | Type | Claim | Quelle | id-Token | access-Token | userinfo |
|---|---|---|---|---|---|---|
kora-username |
oidc-usermodel-property-mapper |
preferred_username |
user.attribute=username |
✅ | ✅ | ✅ |
kora-email |
oidc-usermodel-property-mapper |
email |
user.attribute=email |
✅ | ✅ | ✅ |
kora-realm-roles |
oidc-usermodel-realm-role-mapper |
realm_access.roles |
Realm-Rollen, multivalued | ❌ | ✅ | ✅ |
kora-audience-mapper |
oidc-audience-mapper |
aud |
gepinnt auf kora-api |
❌ | ✅ | ❌ |
kora-tenants (5 Mapper) — dieselben vier plus zusätzlich:
| Mapper | Type | Claim | Quelle | id-Token | access-Token | userinfo |
|---|---|---|---|---|---|---|
kora-group-path-mapper |
oidc-group-membership-mapper |
groups |
Group-Membership, full-path, multivalued | ✅ | ✅ | ✅ |
3.2 JWT-Claim-Inventar (Soll vs. Ist)¶
Aktualisiert: 2026-04-28 (Color-System-Implementation, Merge 7261426).
Soll-Zustand: Standard-OIDC-Claims plus die in §3.1 von kora-scope
emittierten Custom-Claims. Ist-Zustand: aus einem frisch gemintten
Token (bench-operator-admin via Password-Grant) inspiziert.
| Claim | Soll | Ist (kora-platform) |
Ist (kora-tenants) |
Status |
|---|---|---|---|---|
sub |
✓ Standard-OIDC-Subject-Identifier (UUID) | ❌ fehlt | ❌ fehlt | DRIFT — siehe TODO-Auth-NEU |
preferred_username |
✓ via kora-username-Mapper |
✓ | ✓ | OK |
email |
✓ via kora-email-Mapper |
✓ | ✓ | OK |
realm_access.roles |
✓ via kora-realm-roles-Mapper |
✓ | ✓ | OK |
groups |
✓ via kora-group-path-mapper (nur kora-tenants) |
n/a | ✓ | OK |
aud |
✓ via kora-audience-mapper, gepinnt auf kora-api |
✓ | ✓ | OK |
sid |
✓ Session-ID (KC-Built-In) | ✓ | ✓ | OK |
iss |
✓ Issuer (KC-Built-In) | ✓ | ✓ | OK |
exp / iat |
✓ Standard-Timing | ✓ | ✓ | OK |
azp |
✓ Authorized Party (Client-ID) | ✓ | ✓ | OK |
Bekannte Drift: fehlendes sub-Claim¶
Konsequenzen für DB-Design:
- Tabellen mit User-Bezug nutzen aktuell zwei Workaround-Patterns:
- NULL-Fallback wie
platform_audit_log.actor_keycloak_id UUIDNULLABLE — bei fehlendemsubwird NULL eingetragen - Composite-PK wie
user_preferences (realm, username)statt natürlicher UUID-PK (Color-System-Implementation, Migration 0008) - Diese Patterns sind als Workarounds dokumentiert, nicht als Soll-Architektur
- Sobald
TODO-Auth-NEUumgesetzt ist, sollten neue Tabellen direkt UUID-PK viasub-Claim nutzen - Bestehende Tabellen können in einer separaten Migration auf
UUID-PK umgestellt werden (Daten-Migration via Username-zu-
sub- Lookup pro Realm)
Verify-Mechanismus: scripts/verify-auth-stack.sh
Check 58 testet die Anwesenheit des sub-Claims in einem Live-
Token aus beiden Realms. Aktuell liefert er FAIL als Drift-
Frühwarnung. Sobald der Mapper-Fix in Live-Keycloak deployed ist,
wechselt der Check automatisch auf PASS — keine Skript-Änderung
nötig.
4. Realm-Roles¶
4.1 kora-platform Realm-Roles (deklariert in JSON)¶
| Rolle | Composite | Beschreibung |
|---|---|---|
operator-admin |
nein | AVS Operator, voll: Tenants anlegen/löschen, Pakete, Templates, System-Settings |
operator-editor |
nein | AVS Operator, eingeschränkt: Tenant-Betreuung, keine strukturellen Änderungen |
operator-viewer |
nein | AVS Operator, nur lesen: Monitoring und Support |
vendor-support |
nein | luki-net Support-Account, Read-Only auf Logs/Metriken |
vendor-breakglass |
nein | luki-net Notfall-Full-Admin, 2h-Session-Cap |
vendor-tunnel |
nein | luki-net Reverse-Tunnel-Account |
KC-Built-In zusätzlich: default-roles-kora-platform (composite),
offline_access, uma_authorization.
Backend-Required-Role-Set:
OPERATOR_ROLES = {operator-admin, operator-editor, operator-viewer}
(siehe src/kora_platform/api/dependencies/tenant_context.py:38).
4.2 kora-tenants Realm-Roles (deklariert in JSON)¶
| Rolle | Composite | Beschreibung |
|---|---|---|
tenant-admin |
nein | Tenant-Verwaltung, voll |
tenant-editor |
nein | Tenant-Inhalte pflegen, keine User-Verwaltung |
tenant-viewer |
nein | Tenant-Inhalte lesen |
KC-Built-In: default-roles-kora-tenants (composite).
5. Frontend-Auth-Konfiguration¶
5.1 operator-ui (frontend/operator-ui/src/composables/useAuth.ts)¶
| Feld | Wert | Quelle |
|---|---|---|
keycloakBaseUrl Default |
https://auth.kora.luki-net.org |
Default in useAuth.ts:35, env-überschreibbar via VITE_KEYCLOAK_BASE_URL |
| Realm | kora-platform |
env VITE_KEYCLOAK_REALM |
| Client-ID | operator-ui |
env VITE_KEYCLOAK_CLIENT_ID |
| Scope-Request | openid kora-scope (literal, hartcodiert) |
useAuth.ts:login() |
| Code-Challenge-Method | S256 (PKCE) |
useAuth.ts:login() |
| Router-Base | /admin/operator/ |
router/index.ts: createWebHistory("/admin/operator/") |
redirectUri (zum Keycloak) |
${window.location.origin}/admin/operator/auth/callback |
useAuth.ts:35, hartcodiertes /admin/operator/-Präfix |
post_login_redirect-Speicherung |
Pfad-relativ zur Router-Base (also /tenants statt /admin/operator/tenants) |
useAuth.ts:login() — TODO-Platform-09 Drift #5 Fix |
5.2 tenant-ui (frontend/tenant-ui/src/composables/useAuth.ts)¶
| Feld | Wert | Quelle |
|---|---|---|
keycloakBaseUrl Default |
https://auth.kora.luki-net.org |
Default in useAuth.ts, env-überschreibbar via VITE_KEYCLOAK_BASE_URL |
| Realm | kora-tenants |
env VITE_KEYCLOAK_REALM |
| Client-ID | tenant-ui |
env VITE_KEYCLOAK_CLIENT_ID |
| Scope-Request | openid kora-scope (literal, hartcodiert) |
useAuth.ts:login() |
| Code-Challenge-Method | S256 (PKCE) |
useAuth.ts:login() |
| Router-Base | /tenant/ |
router/index.ts: createWebHistory("/tenant/") |
redirectUri (zum Keycloak) |
${window.location.origin}/tenant/auth/callback |
useAuth.ts, hartcodiertes /tenant/-Präfix |
post_login_redirect-Speicherung |
Pfad-relativ zur Router-Base | useAuth.ts:login() — TODO-Platform-09 Drift #5 Fix (analog operator-ui) |
6. Init-Skripte (Live-State-Reconciler)¶
| Skript | Wirkung | Idempotent? | Quelle |
|---|---|---|---|
infra/keycloak/init-scripts/create-audit-service-account.sh |
Legt kora-platform-audit Service-Account-Client an, weist realm-management.view-events zu, regeneriert das Secret nicht bei Re-Run |
ja | TODO-B2-03 |
infra/keycloak/init-scripts/create-operator-ui-client.sh |
Legt operator-ui Public-Client an, wenn er nicht existiert. Liest aus dem Realm-JSON. Aktuell: create-or-skip (kein Reconcile-Pattern für nachträgliche JSON-Änderungen) |
ja | TODO-Platform-05 |
Source-of-Truth-Prinzip: Realm-JSON deklariert den Soll-Zustand; Init-Skripte garantieren den Ist-Zustand idempotent. Re-Import des Realm-JSON ist nach Erst-Anlage tabu (würde Confidential-Client-Secrets regenerieren).
7. NPMplus-Routing-Soll¶
| Subdomain | Stream-Target | Pfad-Behandlung |
|---|---|---|
auth.kora.luki-net.org |
kora-platform-keycloak:8080 |
Alle Pfade durch zur Keycloak |
platform.kora.luki-net.org |
kora-platform-api:8080 |
/api/v1/* → Backend; /admin/operator/* → Frontend-Bundle (FastAPI StaticFiles); /tenant/* → Frontend-Bundle (FastAPI StaticFiles); /auth/* NICHT bedient — alle Auth-Endpoints liegen ausschließlich auf der auth.-Subdomain |
docs.kora.luki-net.org |
kora-platform-mkdocs:8000 |
(nicht Auth-relevant) |
HTTP-Probes (Soll):
https://auth.kora.luki-net.org/realms/kora-platform/.well-known/openid-configuration→ 200https://auth.kora.luki-net.org/realms/kora-tenants/.well-known/openid-configuration→ 200https://platform.kora.luki-net.org/admin/operator/→ 200 (SPA-HTML)https://platform.kora.luki-net.org/tenant/→ 200 (SPA-HTML)https://platform.kora.luki-net.org/auth/realms/...→ 404 (Bug-Pfad, muss broken sein)
8. Test-Skripte (bewusst nicht-deklarative Reconciler)¶
| Skript | Wirkung |
|---|---|
scripts/gen-test-tokens.sh |
Legt idempotent: bench-operator-admin (operator-admin-Rolle, PW bench-operator-admin-pw-1234), admin-bench-tenant-{a,b} (Tenant-User in Bench-Tenant-Gruppen, PW bench-tenant-admin-pw-1234); aktiviert + setzt PW für vendor-breakglass (PW bench-vendor-bg-pw-1234); ruft Tokens via Password-Grant ab |
Test-User existieren bewusst nicht im Realm-JSON — sie sind reine
Bench-User für Smoke- und E2E-Tests. Realm-JSON users[] enthält
nur die deklarierten Vendor-Accounts (vendor-support,
vendor-breakglass, vendor-tunnel) als disabled Soll-Stubs;
gen-test-tokens.sh aktiviert vendor-breakglass zur Test-Zeit.
9. Drift-Disziplin¶
Der Stack hat sechs Schichten, die alle gegeneinander driften können. Source-of-Truth pro Schicht:
| Schicht | SoT |
|---|---|
| Realm-Konfig-Soll | Dieses Dokument + Realm-JSON |
| Realm-Konfig-Ist | Live-Keycloak |
| Client-Reconciler | Init-Skripte (create-*.sh) |
| Frontend-Konfig | useAuth.ts-Defaults + VITE_*-Env-Vars |
| NPMplus-Routing | NPMplus-Konfig (außerhalb Repo) + Probes in §7 |
| Test-User | gen-test-tokens.sh |
Pflicht: Bei jeder Auth-Änderung wird zuerst dieses Dokument
aktualisiert, dann die jeweilige Schicht, dann
scripts/verify-auth-stack.sh lokal ausgeführt — exit 0 ist die
Merge-Bedingung. Im Block 14 wird das Skript in CI integriert.
10. Letzte Aktualisierung¶
7909ce852590aed5e65684a8dbe9ed160bf7d227 (TODO-Platform-09-Merge), 2026-04-26 — Initial-Anlage
nach Konsolidierung aus TODO-Platform-05/-06/-07/-08 und Drift-#5/#6.