diff --git a/.env b/.env index 3e94e8fd..d85f2e6f 100644 --- a/.env +++ b/.env @@ -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 diff --git a/config/docker/keycloak/meldestelle-realm.json b/config/docker/keycloak/meldestelle-realm.json index 06a37131..1d8ad6ce 100644 --- a/config/docker/keycloak/meldestelle-realm.json +++ b/config/docker/keycloak/meldestelle-realm.json @@ -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" + ] } } ] diff --git a/dc-backend.yaml b/dc-backend.yaml index b95f2ebf..6f1eb445 100644 --- a/dc-backend.yaml +++ b/dc-backend.yaml @@ -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}" diff --git a/docs/05_Backend/Guides/Testing_with_Postman.md b/docs/05_Backend/Guides/Testing_with_Postman.md index 3bd5cca7..8cee2178 100644 --- a/docs/05_Backend/Guides/Testing_with_Postman.md +++ b/docs/05_Backend/Guides/Testing_with_Postman.md @@ -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". diff --git a/docs/05_Backend/Services/PingService.md b/docs/05_Backend/Services/PingService.md deleted file mode 100644 index 8bd82fc1..00000000 --- a/docs/05_Backend/Services/PingService.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -type: Redirect -status: ARCHIVED ---- - -# MOVED - -This documentation has been superseded. -Please refer to: [Ping Service Reference](PingService_Reference.md) diff --git a/docs/05_Backend/Services/ping-service.md b/docs/05_Backend/Services/ping-service.md deleted file mode 100644 index 8bd82fc1..00000000 --- a/docs/05_Backend/Services/ping-service.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -type: Redirect -status: ARCHIVED ---- - -# MOVED - -This documentation has been superseded. -Please refer to: [Ping Service Reference](PingService_Reference.md) diff --git a/docs/07_Infrastructure/guides/jwt-in-docker.md b/docs/07_Infrastructure/guides/jwt-in-docker.md new file mode 100644 index 00000000..223d9997 --- /dev/null +++ b/docs/07_Infrastructure/guides/jwt-in-docker.md @@ -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 ` HTTP-Header mitgesendet werden. diff --git a/docs/99_Journal/2026-01-22_Session_Log.md b/docs/99_Journal/2026-01-22_Session_Log.md new file mode 100644 index 00000000..3cb97d17 --- /dev/null +++ b/docs/99_Journal/2026-01-22_Session_Log.md @@ -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).