docs: add infrastructure guide for JWT in Docker and refactor Keycloak config

Added a detailed guide (`jwt-in-docker.md`) to address JWT validation challenges in Docker environments (Split Horizon issue). Refactored Keycloak realm configuration (`meldestelle-realm.json`) with updated roles, clients, and improved infrastructure clarity. Updated `.env` variables for streamlined token validation. Adjusted Docker Compose services (`dc-backend.yaml`) to use revised Keycloak environment variables.
This commit is contained in:
Stefan Mogeritsch 2026-01-22 17:23:24 +01:00
parent a9b788aca9
commit c692a2395c
8 changed files with 214 additions and 43 deletions

8
.env
View File

@ -44,6 +44,12 @@ KC_HOSTNAME=localhost
KC_PORT=8180:8080
KC_DEBUG_PORT=9000:9000
# --- KEYCLOAK TOKEN VALIDATION ---
# Public Issuer URI (must match the token issuer from browser/postman)
KC_ISSUER_URI=http://localhost:8180/realms/meldestelle
# Internal JWK Set URI (for service-to-service communication within Docker)
KC_JWK_SET_URI=http://keycloak:8080/realms/meldestelle/protocol/openid-connect/certs
# --- PGADMIN ---
PGADMIN_IMAGE=dpage/pgadmin4:8
PGADMIN_EMAIL=meldestelle@mo-code.at
@ -83,8 +89,6 @@ GATEWAY_SERVER_PORT=8081
GATEWAY_SPRING_PROFILES_ACTIVE=docker
GATEWAY_DEBUG=true
GATEWAY_SERVICE_NAME=api-gateway
SSEC_ISSUER_URI=http://keycloak:8080/realms/meldestelle
SSEC_JWK_SET_URI=http://keycloak:8080/realms/meldestelle/protocol/openid-connect/certs
GATEWAY_CONSUL_HOSTNAME=api-gateway
GATEWAY_CONSUL_PREFER_IP=true

View File

@ -36,7 +36,10 @@
"oauth2DeviceCodeLifespan": 600,
"oauth2DevicePollingInterval": 5,
"internationalizationEnabled": true,
"supportedLocales": ["de", "en"],
"supportedLocales": [
"de",
"en"
],
"defaultLocale": "de",
"roles": {
"realm": [
@ -52,6 +55,12 @@
"composite": false,
"clientRole": false
},
{
"name": "MELD_USER",
"description": "Verified user role (Technical placeholder for REITER)",
"composite": false,
"clientRole": false
},
{
"name": "MONITORING",
"description": "Monitoring role for system health checks",
@ -195,6 +204,26 @@
"attributes": {
"pkce.code.challenge.method": "S256"
}
},
{
"clientId": "postman-client",
"name": "Postman Test Client",
"description": "Confidential client for backend testing via Postman",
"enabled": true,
"clientAuthenticatorType": "client-secret",
"secret": "postman-secret-123",
"redirectUris": [
"https://oauth.pstmn.io/v1/callback"
],
"webOrigins": [],
"protocol": "openid-connect",
"bearerOnly": false,
"publicClient": false,
"standardFlowEnabled": false,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": false,
"fullScopeAllowed": true
}
],
"users": [
@ -212,15 +241,26 @@
"temporary": true
}
],
"realmRoles": ["ADMIN", "USER"],
"realmRoles": [
"ADMIN",
"USER",
"MELD_USER"
],
"clientRoles": {
"api-gateway": ["ADMIN"]
"api-gateway": [
"ADMIN"
]
}
}
],
"groups": [],
"defaultRoles": ["USER", "GUEST"],
"requiredCredentials": ["password"],
"defaultRoles": [
"USER",
"GUEST"
],
"requiredCredentials": [
"password"
],
"passwordPolicy": "length(8)",
"otpPolicyType": "totp",
"otpPolicyAlgorithm": "HmacSHA1",
@ -228,12 +268,20 @@
"otpPolicyDigits": 6,
"otpPolicyLookAheadWindow": 1,
"otpPolicyPeriod": 30,
"otpSupportedApplications": ["FreeOTP", "Google Authenticator"],
"otpSupportedApplications": [
"FreeOTP",
"Google Authenticator"
],
"webAuthnPolicyRpEntityName": "meldestelle",
"webAuthnPolicySignatureAlgorithms": ["ES256", "RS256"],
"webAuthnPolicySignatureAlgorithms": [
"ES256",
"RS256"
],
"smtpServer": {},
"eventsEnabled": true,
"eventsListeners": ["jboss-logging"],
"eventsListeners": [
"jboss-logging"
],
"enabledEventTypes": [
"LOGIN",
"LOGIN_ERROR",
@ -254,7 +302,9 @@
"providerId": "rsa-generated",
"subComponents": {},
"config": {
"priority": ["100"]
"priority": [
"100"
]
}
},
{
@ -262,8 +312,12 @@
"providerId": "hmac-generated",
"subComponents": {},
"config": {
"priority": ["100"],
"algorithm": ["HS256"]
"priority": [
"100"
],
"algorithm": [
"HS256"
]
}
},
{
@ -271,7 +325,9 @@
"providerId": "aes-generated",
"subComponents": {},
"config": {
"priority": ["100"]
"priority": [
"100"
]
}
}
]

View File

@ -28,8 +28,8 @@ services:
DEBUG: "${GATEWAY_DEBUG:-true}"
# --- KEYCLOAK ---
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI: "${SSEC_ISSUER_URI:-http://keycloak:8080/realms/meldestelle}"
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI: "${SSEC_JWK_SET_URI:-http://keycloak:8080/realms/meldestelle/protocol/openid-connect/certs}"
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI: "${KC_ISSUER_URI}"
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI: "${KC_JWK_SET_URI}"
# --- CONSUL ---
SPRING_CLOUD_CONSUL_HOST: "${CONSUL_HOST:-consul}"
@ -98,6 +98,10 @@ services:
DEBUG: "${PING_DEBUG:-true}"
SERVER_PORT: "${PING_SERVER_PORT:-8082}"
# --- KEYCLOAK ---
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI: "${KC_ISSUER_URI}"
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI: "${KC_JWK_SET_URI}"
# --- CONSUL ---
SPRING_CLOUD_CONSUL_HOST: "${CONSUL_HOST:-consul}"
SPRING_CLOUD_CONSUL_PORT: "${CONSUL_HTTP_PORT:-8500}"

View File

@ -25,12 +25,12 @@ Damit wir testen können, brauchen wir einen User und einen Client in Keycloak,
* **Realm:** Wähle oben links `meldestelle` aus (wurde beim Start importiert).
**Check:**
* **User:** Gibt es einen User? (z.B. `testuser` / `password` mit Rolle `MELD_USER`)?
* *Falls nicht:* Lege schnell einen User an, setze Credentials (temporary: off) und weise ihm unter "Role Mapping" die Rolle `MELD_USER` zu.
* **Client:** Gibt es einen Client? (z.B. `meldestelle-frontend` oder `postman`)?
* *Falls nicht:* Lege einen Client `postman` an.
* Access Type: `public` (oder `confidential` wenn du Client Secret nutzen willst, public reicht für Postman oft).
* Valid Redirect URIs: `*` (für Tests ok) oder `https://oauth.pstmn.io/v1/callback`.
* **User:** Der Standard-Admin User `admin` hat bereits die notwendige Rolle `MELD_USER`.
* **Client:** Es gibt einen dedizierten Test-Client `postman-client`.
* Client ID: `postman-client`
* Client Secret: `postman-secret-123`
* Access Type: `confidential`
* Direct Access Grants: `Enabled`
---
@ -42,9 +42,10 @@ Erstelle eine neue Collection "Meldestelle Ping Test".
Setze folgende Variablen in Postman (Environment "Local Docker"):
* `gateway_url`: `http://localhost:8081`
* `auth_url`: `http://localhost:8180/realms/meldestelle/protocol/openid-connect/token`
* `client_id`: `meldestelle-frontend` (oder wie dein Client heißt)
* `username`: `testuser` (dein User)
* `password`: `password` (dein Passwort)
* `client_id`: `postman-client`
* `client_secret`: `postman-secret-123`
* `username`: `admin`
* `password`: `Change_Me_In_Production!`
---
@ -108,6 +109,7 @@ Wir testen nun die verschiedenen Endpunkte und Sicherheitsstufen.
* Grant Type: `Password Credentials`
* Access Token URL: `{{auth_url}}`
* Client ID: `{{client_id}}`
* Client Secret: `{{client_secret}}`
* Username: `{{username}}`
* Password: `{{password}}`
* Klick auf "Get New Access Token".

View File

@ -1,9 +0,0 @@
---
type: Redirect
status: ARCHIVED
---
# MOVED
This documentation has been superseded.
Please refer to: [Ping Service Reference](PingService_Reference.md)

View File

@ -1,9 +0,0 @@
---
type: Redirect
status: ARCHIVED
---
# MOVED
This documentation has been superseded.
Please refer to: [Ping Service Reference](PingService_Reference.md)

View File

@ -0,0 +1,79 @@
---
type: Guide
status: ACTIVE
owner: DevOps Engineer
tags: [jwt, oidc, keycloak, docker, networking, security]
---
# Leitfaden: JWT-Validierung in der Docker-Umgebung
Dieser Leitfaden erklärt eine kritische Herausforderung und deren Lösung bei der Arbeit mit Keycloak (OIDC) und Microservices in einer Docker-Umgebung.
## Das Problem: Das "Split Horizon"-Dilemma
In unserer lokalen Docker-Umgebung existieren zwei "Sichtweisen" (Horizons) auf Keycloak:
1. **Die externe Sicht (Browser/Postman):** Ein Client außerhalb von Docker (z.B. Postman) greift auf Keycloak über `http://localhost:8180` zu. Wenn dieser Client ein Token anfordert, stellt Keycloak dieses Token mit dem Issuer (`iss`) Claim `http://localhost:8180/realms/meldestelle` aus.
2. **Die interne Sicht (Microservices):** Ein Service innerhalb des Docker-Netzwerks (z.B. `ping-service`) kann `localhost` nicht verwenden, um Keycloak zu erreichen. Er muss den Docker-internen Hostnamen `http://keycloak:8080` verwenden.
Wenn der Service nun das von außen kommende Token validieren will, passiert Folgendes:
* Das Token sagt: "Ich wurde von `http://localhost:8180/realms/meldestelle` ausgestellt."
* Die Standardkonfiguration des Services sagt: "Ich vertraue aber nur Token von `http://keycloak:8080/realms/meldestelle`."
* **Ergebnis:** Die Validierung schlägt mit `iss claim is not valid` fehl.
## Die Lösung: Getrennte Konfiguration von Issuer und JWK-Pfad
Die Lösung besteht darin, Spring Security so zu konfigurieren, dass es zwischen der **Validierung des Issuers** und dem **technischen Abruf der Schlüssel** unterscheidet.
* `spring.security.oauth2.resourceserver.jwt.issuer-uri`: **Muss exakt mit dem `iss`-Claim im Token übereinstimmen.** Hier verwenden wir die öffentliche URL.
* `spring.security.oauth2.resourceserver.jwt.jwk-set-uri`: Der technische Endpunkt, unter dem der Service die Public Keys (JWKs) zur Signaturprüfung abruft. Hier verwenden wir die interne Docker-URL.
### Implementierung
Wir steuern dies zentral über unsere `.env`-Datei, um eine "Single Source of Truth" zu haben:
**`.env`**
```dotenv
# Public Issuer URI (must match the token issuer from browser/postman)
KC_ISSUER_URI=http://localhost:8180/realms/meldestelle
# Internal JWK Set URI (for service-to-service communication within Docker)
KC_JWK_SET_URI=http://keycloak:8080/realms/meldestelle/protocol/openid-connect/certs
```
Diese Variablen werden dann in der `dc-backend.yaml` an die Services durchgereicht:
**`dc-backend.yaml`**
```yaml
services:
ping-service:
environment:
# ...
# --- KEYCLOAK ---
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI: "${KC_ISSUER_URI}"
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI: "${KC_JWK_SET_URI}"
```
Diese Konfiguration stellt sicher, dass die Validierung erfolgreich ist, während die Kommunikation innerhalb des Docker-Netzwerks korrekt funktioniert.
---
## Empfehlungen für die Entwickler
### Für den Senior Backend Developer
1. **Konfigurations-Muster übernehmen:** Wenn du neue Microservices erstellst, kopiere exakt den `environment`-Block für Keycloak aus dem `ping-service` in der `dc-backend.yaml`. Das stellt sicher, dass die JWT-Validierung von Anfang an korrekt funktioniert.
2. **Rollen-Synchronisation:** Bevor du einen Endpunkt mit `@PreAuthorize("hasRole('DEINE_ROLLE')")` sicherst, stelle sicher, dass die Rolle `DEINE_ROLLE` auch in der `config/docker/keycloak/meldestelle-realm.json` definiert ist.
3. **Debugging-Tipp:** Bei einem `401 Unauthorized` auf einem geschützten Endpunkt, prüfe immer zuerst die Logs des Services. Die Fehlermeldungen von Spring Security sind sehr aussagekräftig (z.B. `iss claim not valid`, `Connection refused`, `An error occurred while attempting to decode the Jwt`).
### Für den KMP Frontend Expert
1. **Stabiler Auth-Flow:** Die Authentifizierung ist jetzt stabil. Du kannst dich auf die Implementierung des Login-Prozesses konzentrieren.
2. **Client-Konfiguration:** Der `web-app` Client in Keycloak ist für dich vorbereitet. Er ist ein **public client** und nutzt den sicheren **Authorization Code Flow mit PKCE**. Du musst eine OIDC-Client-Bibliothek verwenden, die diesen Flow unterstützt (z.B. `keycloak-js` oder eine moderne Alternative wie `oidc-client-ts`).
3. **Benötigte Konfiguration:** Dein Frontend benötigt folgende Informationen, die du am besten in einer Environment-Datei ablegst:
* Keycloak URL: `http://localhost:8180`
* Realm: `meldestelle`
* Client ID: `web-app`
4. **Keine Secrets im Frontend:** Der `web-app` Client hat absichtlich **kein** Secret. Versuche niemals, ein Secret im Frontend-Code zu speichern oder zu verwenden. PKCE sorgt hier für die nötige Sicherheit.
5. **Token-Handling:** Nach dem Login erhältst du ein **Access Token**. Dieses muss bei jeder API-Anfrage an das Backend (`http://localhost:8081/api/...`) im `Authorization: Bearer <token>` HTTP-Header mitgesendet werden.

View File

@ -0,0 +1,44 @@
---
type: Journal
status: COMPLETED
owner: Curator
date: 2026-01-22
participants:
- DevOps & Infrastructure Engineer
- Owner (Stefan)
---
# Session Log: 22. Jänner 2026
## Zielsetzung
Analyse der Infrastruktur, Behebung von Authentifizierungs-Problemen (JWT/Keycloak) im Docker-Netzwerk und Validierung der "Tracer Bullet" Architektur (Ping-Service).
## Durchgeführte Arbeiten
### 1. Infrastruktur & IAM (Keycloak)
* **Analyse:** Diskrepanz zwischen Dokumentation (`Testing_with_Postman.md`) und Konfiguration (`meldestelle-realm.json`) festgestellt.
* **Fix (Realm):**
* Neue Rolle `MELD_USER` als technischer Platzhalter für verifizierte Benutzer eingeführt.
* Neuen Confidential Client `postman-client` erstellt, um saubere Backend-Tests via Password-Grant zu ermöglichen, ohne den Frontend-Client unsicher zu machen.
* Dem User `admin` die Rolle `MELD_USER` zugewiesen.
* **Fix (Networking/JWT):**
* Das "Split Horizon"-Problem identifiziert (Token Issuer `localhost` vs. Docker-interner Keycloak-Host).
* `.env` Datei refactored: Trennung in `KC_ISSUER_URI` (Public) und `KC_JWK_SET_URI` (Internal).
* `dc-backend.yaml` aktualisiert: `api-gateway` und `ping-service` nutzen nun diese expliziten Variablen.
### 2. Dokumentation (Single Source of Truth)
* **Update:** `docs/05_Backend/Guides/Testing_with_Postman.md` an den neuen `postman-client` angepasst.
* **Neu:** `docs/07_Infrastructure/guides/jwt-in-docker.md` erstellt. Dieses Dokument erklärt das "Split Horizon"-Problem und dient als Referenz für Frontend- und Backend-Entwickler.
### 3. Testing
* Erfolgreicher Durchlauf aller Postman-Tests (Connectivity, Health, Security Block, Auth Login, Security Access).
* Bestätigung, dass der `ping-service` nun korrekt Token validiert, die von außen (Postman) kommen, aber intern (Docker) geprüft werden.
## Ergebnisse
* Die lokale Entwicklungsumgebung ist nun **vollständig funktionsfähig** und **auth-ready**.
* Die Infrastruktur-Konfiguration ist sauberer und expliziter (Trennung von Public/Internal URLs).
* Eine solide Basis für die Frontend-Login-Implementierung ist geschaffen.
## Nächste Schritte
* **Frontend:** Implementierung des Login-Flows (Authorization Code Flow mit PKCE) unter Nutzung des `web-app` Clients.
* **Backend:** Beginn der Modellierung der **Events Domain** (Veranstaltungen).