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:
http://keycloak.localhost/admin→ Loginadmin/admin→ Realm-Dropdown oben links auf
procurex stellen.
- Clients → Create client.
- General settings: Client type
OpenID Connect, Client IDprocurex-loadtest, Name
PROCUREX Lasttest-Client (k6, PRX-36) → Next.
- 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.
- 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:
http://keycloak.localhost/admin→ Loginadmin/admin→ Realm procurex.- **Clients →
procurex-frontend→ Client scopes →procurex-frontend-dedicated→ Mappers →
Add mapper → By configuration → User Attribute**.
- Name/Token-Claim-Name
cost_center, User Attributecost_center, "Add to ID token" und
"Add to access token" beide AN → Save.
- 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).
- Users →
approver1→ Attributes → Add attribute: Keycost_center, ValueCC-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.