# Keycloak — Realm `procurex`

PRX-19 (Umsetzung): echtes OIDC-Login statt Mock-Rollenumschalter. Läuft im geteilten
`enterprise-infrastructure`-Keycloak (`enterprise-infrastructure --profile postgres --profile keycloak up -d keycloak`).

Dieses Dokument ist bewusst ausführlich, weil Keycloak im Team bisher wenig bekannt ist —
der erste Abschnitt erklärt die Grundkonzepte, danach folgt die PROCUREX-spezifische
Konfiguration.

## Was ist Keycloak überhaupt?

Keycloak ist ein **Identity- und Access-Management-Server** (IAM). Statt dass jede
Anwendung (Frontend, Backend) selbst Passwörter speichert und ein eigenes Login-Formular
baut, übernimmt Keycloak das zentral für alle angeschlossenen Anwendungen. Es implementiert
den offenen Standard **OpenID Connect** (OIDC), der wiederum auf **OAuth 2.0** aufbaut.

Vorteil gegenüber Eigenbau: Login, Passwort-Reset, Session-Verwaltung, Multi-Faktor-Auth
usw. sind einmal zentral gelöst statt in jeder Anwendung neu — und Anwendungen müssen
niemals selbst ein Passwort sehen oder speichern (siehe Grant-Types unten).

## Kernbegriffe

| Begriff | Bedeutung |
|---|---|
| **Realm** | Ein isolierter Mandant: eigene Nutzer, Rollen und Clients, komplett getrennt von anderen Realms auf demselben Server. PROCUREX nutzt den Realm `procurex`, getrennt vom Keycloak-eigenen `master`-Realm (nur für die Server-Administration selbst). |
| **Client** | Eine Anwendung, die sich gegen den Realm authentifiziert bzw. für die sich Nutzer einloggen. Jede Anwendung (Frontend, Backend, Test-Tool) bekommt ihren eigenen Client. |
| **Public Client** | Kann kein Geheimnis sicher speichern (Browser-SPA, Mobile-App, Skript) — hat kein Client-Secret. Sicherheit kommt stattdessen über PKCE (siehe unten). |
| **Confidential Client** | Läuft auf einem vertrauenswürdigen Server und kann ein Client-Secret geheim halten. |
| **Realm-Rolle** | Eine Rolle, die global im gesamten Realm gilt (z. B. `ROLE_BUYER`). PROCUREX nutzt ausschließlich Realm-Rollen, keine Client-Rollen — einfacher, weil dieselbe Rolle für Frontend und Backend gilt. |
| **Access Token** | Kurzlebiges JWT (bei uns 300 s / 5 min gültig), das ein Client bei jedem API-Aufruf im `Authorization: Bearer …`-Header mitschickt. Enthält u. a. die Rollen im Claim `realm_access.roles`. |
| **Refresh Token** | Längerlebiges Token, mit dem ein Client ein neues Access Token holen kann, ohne den Nutzer erneut einzuloggen. |
| **ID Token** | Nur beim Browser-Login relevant: enthält Infos über den eingeloggten Nutzer (Name, E-Mail) für die Anzeige im Frontend. |
| **JWT (JSON Web Token)** | Signiertes, Base64-kodiertes JSON. `procurex-app` validiert nur die Signatur gegen Keycloaks öffentlichen Schlüssel (JWK Set) — fragt bei jedem Request **nicht** aktiv bei Keycloak nach ("Resource Server"-Pattern, siehe unten). |

## Grant Types (Flows) — wie kommt eine Anwendung an ein Token?

Keycloak unterstützt mehrere Wege, ein Token zu bekommen. PROCUREX nutzt bewusst
unterschiedliche für unterschiedliche Zwecke:

| Flow | Ablauf | Wo genutzt |
|---|---|---|
| **Authorization Code + PKCE** | Der Nutzer wird zur Keycloak-Login-Seite umgeleitet und gibt sein Passwort **dort** ein (nie in der eigenen App). Nach Login leitet Keycloak mit einem einmaligen "Code" zurück, den die App serverseitig gegen ein Token eintauscht. PKCE (Proof Key for Code Exchange) verhindert, dass ein Angreifer einen abgefangenen Code selbst einlösen kann — wichtig, weil `procurex-frontend` ein *public* Client ohne Secret ist. | `procurex-frontend` (echter Nutzer-Login im Browser) |
| **Direct Access Grants** (Resource Owner Password Credentials, ROPC) | Die App schickt Benutzername **und Passwort direkt** an den Token-Endpoint, ganz ohne Redirect oder Keycloak-Login-Seite. Einfach, aber unsicher für echte Anwendungen — die App sieht das Klartext-Passwort. Deshalb bei `procurex-frontend` deaktiviert. Für automatisierte Skripte ohne Browser (z. B. Lasttests) ist das trotzdem die pragmatische Wahl. | `procurex-loadtest` (k6-Skripte, PRX-36) |
| **Client Credentials** | Der Client authentifiziert sich mit seinem eigenen Secret, es ist gar kein Nutzer beteiligt — für Service-zu-Service-Aufrufe. | aktuell bei PROCUREX nicht genutzt |

## Wie PROCUREX Tokens validiert (Resource-Server-Pattern)

`procurex-app` prüft eingehende Access Tokens **selbst**, ohne bei jedem Request Keycloak
zu fragen: Spring Security lädt einmalig Keycloaks öffentliche Schlüssel (JWK Set, siehe
`PROCUREX_OIDC_JWK_SET_URI`) und prüft damit lokal die JWT-Signatur und Gültigkeit. Die
Rollen kommen aus dem Claim `realm_access.roles` (siehe `SecurityConfig.java`). Das ist
schnell (kein Netzwerk-Roundtrip pro Request) und funktioniert auch, wenn Keycloak kurz
nicht erreichbar ist, solange das Token noch gültig ist.

## Clients

| Client | Typ | Flow(s) | Zweck |
|---|---|---|---|
| `procurex-frontend` | public, kein Secret | Authorization Code + PKCE | Angular-SPA-Login für echte Nutzer |
| `procurex-backend` | confidential, bearer-only | — | nur zur Dokumentation — Spring validiert JWTs direkt gegen den Realm-Issuer, braucht keinen eigenen Client-Aufruf |
| `procurex-loadtest` | public, kein Secret | Direct Access Grants | PRX-36: k6-Lasttests holen sich damit echte Tokens für buyer1/approver1/supplieradmin1, ohne Browser. Bewusst ein **eigener** Client statt Direct Access Grants auf `procurex-frontend` zu aktivieren — additiv, rührt die produktive Login-Konfiguration nicht an, kann jederzeit gefahrlos gelöscht werden. |

Redirect-URIs von `procurex-frontend`: `http://procurex.localhost/*`, `http://localhost:4200/*`
(lokales `ng serve`). `procurex-loadtest` braucht keine Redirect-URIs (kein Browser-Flow).

### `procurex-loadtest` manuell anlegen

Dieser Client ist **nicht** in `procurex-realm-export.json` enthalten (die Datei ist eine
einzeilige ~27000-Token-JSON, die sich mit den verfügbaren Editier-Werkzeugen in dieser
Umgebung nicht automatisiert anpassen ließ — und Keycloak-Admin-Änderungen sind für den
Assistenten ohnehin per Sicherheits-Classifier blockiert). Falls das Realm neu importiert
wird, muss dieser Client daher manuell nachgezogen werden:

1. `http://keycloak.localhost/admin` → Login `admin`/`admin` → Realm-Dropdown oben links auf
   **procurex** stellen.
2. **Clients → Create client**.
3. *General settings*: Client type `OpenID Connect`, Client ID `procurex-loadtest`, Name
   `PROCUREX Lasttest-Client (k6, PRX-36)` → **Next**.
4. *Capability config*: Client authentication **AUS** lassen (public Client, kein Secret).
   Bei "Authentication flow" nur **Direct access grants** aktivieren, alle anderen
   Checkboxen (Standard flow, Implicit flow, Service accounts roles) deaktivieren → **Next**.
5. *Login settings*: alles leer lassen (keine Redirect-URIs nötig) → **Save**.

## Rollen

`ROLE_BUYER`, `ROLE_APPROVER`, `ROLE_SUPPLIER_ADMIN`, `ROLE_RECEIVING`, `ROLE_FINANCE`,
`ROLE_ADMIN` — entsprechen den in Confluence Seite 10 bestätigten Rollen und den Rollenansichten
im Frontend (`core/models/role.model.ts`).

## Testnutzer (Lernprojekt — keine echten Zugangsdaten für Produktivsysteme)

Passwort für alle: `procurex123`

| Username | Rolle | Kostenstelle (`cost_center`, siehe unten) |
|---|---|---|
| `buyer1` | ROLE_BUYER | – (nur Genehmiger brauchen das Attribut) |
| `approver1` | ROLE_APPROVER | `CC-100` |
| `supplieradmin1` | ROLE_SUPPLIER_ADMIN | – |
| `receiving1` | ROLE_RECEIVING | – |
| `finance1` | ROLE_FINANCE | – |
| `admin1` | ROLE_ADMIN | – (ROLE_ADMIN ist immer ein globaler Override, siehe unten) |

## Organisationsgrenze über `cost_center` (FR-010, seit 2026-08-17)

`ApprovalService` prüft seit dem FR-010-Abgleich gegen `PROCUREX-PLANUNGSENTSCHEIDUNGEN.md`
zusätzlich zur Rolle, ob die Kostenstelle des Genehmigers zur Kostenstelle der Bestellung passt
(siehe `procurex-approval/.../ApprovalService.java`, `requireCostCenterMatch`). `ROLE_ADMIN`
entscheidet weiterhin kostenstellenübergreifend.

**Bewusste Grenze:** Die Prüfung ist **nicht fail-closed**, wenn das Token gar keinen
`cost_center`-Claim trägt — nur ein tatsächlich vorhandener, abweichender Claim führt zu HTTP 403.
Grund: dieses Fail-open-Verhalten verhindert, dass die Regel bestehende E2E-/k6-/
Genehmigungsläufe blockiert, solange nicht jeder Genehmiger ein `cost_center`-Attribut besitzt.

**Rollout-Status (29.08.2026):** Auf der lokalen `enterprise-infrastructure`-Instanz durchgeführt —
`approver1` trägt das Attribut `cost_center=CC-100` (per Keycloak-Admin-REST-API gesetzt,
inklusive Aktivierung von „Unmanaged attributes“ im User-Profil, ohne die war das Attribut sonst
beim Speichern still verworfen worden). Der Protocol-Mapper greift für Tokens des Clients
`procurex-frontend`; der `procurex-loadtest`-Client hat den Mapper bewusst nicht (siehe Zweck des
Clients oben), k6-Lasttests sehen den Claim daher nicht und bleiben unverändert im Fail-open-Pfad.

### Rollout auf einer bereits laufenden Instanz nachziehen

`procurex-realm-export.json` enthält seit diesem Commit einen `cost_center`-Protocol-Mapper
(`oidc-usermodel-attribute-mapper`) auf dem Client `procurex-frontend`. Bei einem **frischen**
Realm-Import ist damit nichts weiter zu tun außer den Nutzern das Attribut zu setzen (siehe
unten). Bei einer **bereits laufenden** Instanz (Realm schon importiert, bevor dieser Mapper
hinzukam) muss nachgezogen werden:

1. `http://keycloak.localhost/admin` → Login `admin`/`admin` → Realm **procurex**.
2. **Clients → `procurex-frontend` → Client scopes → `procurex-frontend-dedicated` → Mappers →
   Add mapper → By configuration → User Attribute**.
3. Name/Token-Claim-Name `cost_center`, User Attribute `cost_center`, "Add to ID token" und
   "Add to access token" beide **AN** → **Save**.
4. Falls Keycloak beim Setzen des Attributs in Schritt 5 einen Fehler wie "nicht erlaubtes
   Attribut" zeigt: **Realm settings → User profile → Unmanaged attributes** auf **Enabled**
   setzen (neuere Keycloak-Versionen lehnen per Default nicht deklarierte Attribute ab).
5. **Users → `approver1` → Attributes → Add attribute**: Key `cost_center`, Value `CC-100` →
   **Save**.

Ohne diese manuellen Schritte bleibt die Regel wie oben beschrieben inaktiv (kein Fehlverhalten,
nur keine Durchsetzung) — konsistent mit `docs/quality-gates.md`, das denselben Ehrlichkeitsgrundsatz
für andere noch nicht scharf geschaltete Prüfungen anwendet.

## Backend-Anbindung

`procurex-app` validiert JWTs als OAuth2 Resource Server gegen
`http://keycloak.localhost/realms/procurex` (siehe `procurex-app/application.yml`,
`PROCUREX_OIDC_ISSUER_URI`). Rollen kommen als `realm_access.roles`-Claim im Access Token.

## Frontend-Anbindung

`procurex-frontend` nutzt Authorization Code + PKCE gegen denselben Realm/Client (siehe
`procurex-frontend/README.md`, Abschnitt „Login“).

## Lasttest-Anbindung (PRX-36)

`procurex-loadtest` nutzt Direct Access Grants gegen denselben Realm — siehe
`loadtests/README.md` für die k6-Skripte und Ergebnisse.

## Neu anlegen (falls Volume zurückgesetzt wurde)

Es gibt **kein** `--import-realm` beim Start (siehe `shared/enterprise-infrastructure/docker-compose.yml` — jedes
angeschlossene Projekt legt seinen Realm selbst an). Am schnellsten über die Admin-Konsole
(`http://keycloak.localhost`, admin/admin) → *Realm importieren* → `procurex-realm-export.json`
(enthält Clients + Rollen, **keine** Nutzer/Passwörter und **nicht** den Client
`procurex-loadtest` — siehe Abschnitt oben zum manuellen Nachziehen). Alternativ per
Admin-REST-API, siehe Kommandos in der Commit-Historie dieses Ordners.
