fixing Keycloak Auth
This commit is contained in:
parent
72036207b0
commit
3e3af214e6
190
.env
190
.env
|
|
@ -1 +1,189 @@
|
||||||
config/.env.dev
|
# =============================================================================
|
||||||
|
# Meldestelle - Development Environment Configuration
|
||||||
|
# =============================================================================
|
||||||
|
# Development-specific environment variables
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 1. APPLICATION CONFIGURATION
|
||||||
|
# =============================================================================
|
||||||
|
APP_NAME=Meldestelle
|
||||||
|
APP_VERSION=1.0.0
|
||||||
|
APP_DESCRIPTION='Pferdesport Meldestelle System'
|
||||||
|
APP_ENVIRONMENT=development
|
||||||
|
APP_HOST=0.0.0.0
|
||||||
|
|
||||||
|
# Development-specific settings
|
||||||
|
DEBUG_MODE=true
|
||||||
|
DEV_HOT_RELOAD=true
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 2. PORT MANAGEMENT
|
||||||
|
# =============================================================================
|
||||||
|
# Gateway Ports
|
||||||
|
GATEWAY_PORT=8081
|
||||||
|
GATEWAY_ADMIN_PORT=8080
|
||||||
|
|
||||||
|
# Service Ports
|
||||||
|
PING_SERVICE_PORT=8082
|
||||||
|
MEMBERS_SERVICE_PORT=8083
|
||||||
|
HORSES_SERVICE_PORT=8084
|
||||||
|
EVENTS_SERVICE_PORT=8085
|
||||||
|
MASTERDATA_SERVICE_PORT=8086
|
||||||
|
AUTH_SERVICE_PORT=8087
|
||||||
|
|
||||||
|
# Client Application Ports
|
||||||
|
WEB_APP_PORT=4000
|
||||||
|
DESKTOP_VNC_PORT=5901
|
||||||
|
DESKTOP_WEB_VNC_PORT=6080
|
||||||
|
|
||||||
|
# Infrastructure Ports
|
||||||
|
CONSUL_PORT=8500
|
||||||
|
REDIS_PORT=6379
|
||||||
|
KAFKA_PORT=9092
|
||||||
|
PROMETHEUS_PORT=9090
|
||||||
|
GRAFANA_PORT=3000
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 3. DATABASE CONFIGURATION
|
||||||
|
# =============================================================================
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_NAME=meldestelle
|
||||||
|
DB_USER=meldestelle
|
||||||
|
DB_PASSWORD=meldestelle
|
||||||
|
DB_MAX_POOL_SIZE=10
|
||||||
|
DB_MIN_POOL_SIZE=5
|
||||||
|
DB_AUTO_MIGRATE=true
|
||||||
|
|
||||||
|
POSTGRES_USER=meldestelle
|
||||||
|
POSTGRES_PASSWORD=meldestelle
|
||||||
|
POSTGRES_DB=meldestelle
|
||||||
|
POSTGRES_EXTERNAL_PORT=5432
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 4. REDIS CONFIGURATION
|
||||||
|
# =============================================================================
|
||||||
|
REDIS_EVENT_STORE_HOST=localhost
|
||||||
|
REDIS_EVENT_STORE_PORT=6379
|
||||||
|
REDIS_EVENT_STORE_PASSWORD=
|
||||||
|
REDIS_EVENT_STORE_DATABASE=0
|
||||||
|
REDIS_EVENT_STORE_CONNECTION_TIMEOUT=2000
|
||||||
|
REDIS_EVENT_STORE_READ_TIMEOUT=2000
|
||||||
|
REDIS_EVENT_STORE_USE_POOLING=true
|
||||||
|
REDIS_EVENT_STORE_MAX_POOL_SIZE=8
|
||||||
|
REDIS_EVENT_STORE_MIN_POOL_SIZE=2
|
||||||
|
|
||||||
|
REDIS_CACHE_HOST=localhost
|
||||||
|
REDIS_CACHE_PORT=6379
|
||||||
|
REDIS_CACHE_PASSWORD=
|
||||||
|
REDIS_CACHE_DATABASE=1
|
||||||
|
|
||||||
|
REDIS_EXTERNAL_PORT=6379
|
||||||
|
REDIS_PASSWORD=
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 5. SECURITY CONFIGURATION
|
||||||
|
# =============================================================================
|
||||||
|
JWT_SECRET=meldestelle-jwt-secret-key-for-development-change-in-production
|
||||||
|
JWT_ISSUER=meldestelle-api
|
||||||
|
JWT_AUDIENCE=meldestelle-clients
|
||||||
|
JWT_REALM=meldestelle
|
||||||
|
API_KEY=meldestelle-api-key-for-development
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 6. KEYCLOAK CONFIGURATION
|
||||||
|
# =============================================================================
|
||||||
|
KEYCLOAK_ADMIN=admin
|
||||||
|
KEYCLOAK_ADMIN_PASSWORD=admin
|
||||||
|
KC_DB=postgres
|
||||||
|
KC_DB_URL=jdbc:postgresql://postgres:5432/keycloak
|
||||||
|
KC_DB_USERNAME=meldestelle
|
||||||
|
KC_DB_PASSWORD=meldestelle
|
||||||
|
KC_HOSTNAME=auth.meldestelle.local
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 7. SERVICE DISCOVERY
|
||||||
|
# =============================================================================
|
||||||
|
CONSUL_HOST=consul
|
||||||
|
CONSUL_ENABLED=true
|
||||||
|
SERVICE_DISCOVERY_ENABLED=true
|
||||||
|
SERVICE_DISCOVERY_REGISTER_SERVICES=true
|
||||||
|
SERVICE_DISCOVERY_HEALTH_CHECK_PATH=/health
|
||||||
|
SERVICE_DISCOVERY_HEALTH_CHECK_INTERVAL=10
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 8. MESSAGING (Kafka)
|
||||||
|
# =============================================================================
|
||||||
|
ZOOKEEPER_CLIENT_PORT=2181
|
||||||
|
KAFKA_BROKER_ID=1
|
||||||
|
KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181
|
||||||
|
KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092
|
||||||
|
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
|
||||||
|
KAFKA_INTER_BROKER_LISTENER_NAME=PLAINTEXT
|
||||||
|
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 9. MONITORING
|
||||||
|
# =============================================================================
|
||||||
|
GF_SECURITY_ADMIN_USER=meldestelle
|
||||||
|
GF_SECURITY_ADMIN_PASSWORD=meldestelle
|
||||||
|
GF_USERS_ALLOW_SIGN_UP=false
|
||||||
|
|
||||||
|
METRICS_AUTH_USERNAME=admin
|
||||||
|
METRICS_AUTH_PASSWORD=metrics
|
||||||
|
|
||||||
|
GRAFANA_HOSTNAME=grafana.meldestelle.local
|
||||||
|
PROMETHEUS_HOSTNAME=prometheus.meldestelle.local
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 10. LOGGING CONFIGURATION
|
||||||
|
# =============================================================================
|
||||||
|
LOGGING_LEVEL=DEBUG
|
||||||
|
LOGGING_REQUESTS=true
|
||||||
|
LOGGING_RESPONSES=true
|
||||||
|
LOGGING_REQUEST_HEADERS=true
|
||||||
|
LOGGING_REQUEST_BODY=true
|
||||||
|
LOGGING_RESPONSE_HEADERS=true
|
||||||
|
LOGGING_RESPONSE_BODY=true
|
||||||
|
LOGGING_STRUCTURED=true
|
||||||
|
LOGGING_CORRELATION_ID=true
|
||||||
|
LOGGING_REQUEST_ID_HEADER=X-Request-ID
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 11. CORS AND RATE LIMITING
|
||||||
|
# =============================================================================
|
||||||
|
SERVER_CORS_ENABLED=true
|
||||||
|
SERVER_CORS_ALLOWED_ORIGINS=*
|
||||||
|
RATELIMIT_ENABLED=true
|
||||||
|
RATELIMIT_GLOBAL_LIMIT=100
|
||||||
|
RATELIMIT_GLOBAL_PERIOD_MINUTES=1
|
||||||
|
RATELIMIT_INCLUDE_HEADERS=true
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 12. DOCKER BUILD ARGUMENTS
|
||||||
|
# =============================================================================
|
||||||
|
# Centralized Docker build arguments for compose files
|
||||||
|
# These mirror the values from docker/build-args/ for standalone compose usage
|
||||||
|
DOCKER_GRADLE_VERSION=9.0.0
|
||||||
|
DOCKER_JAVA_VERSION=21
|
||||||
|
DOCKER_NODE_VERSION=20.11.0
|
||||||
|
DOCKER_NGINX_VERSION=1.25-alpine
|
||||||
|
DOCKER_APP_VERSION=1.0.0
|
||||||
|
BUILD_DATE=2025-09-13T23:32:00Z
|
||||||
|
|
||||||
|
# Monitoring & Infrastructure versions
|
||||||
|
DOCKER_PROMETHEUS_VERSION=v2.54.1
|
||||||
|
DOCKER_GRAFANA_VERSION=11.3.0
|
||||||
|
DOCKER_KEYCLOAK_VERSION=26.0.7
|
||||||
|
|
||||||
|
# Spring profiles for Docker builds
|
||||||
|
DOCKER_SPRING_PROFILES_DEFAULT=default
|
||||||
|
DOCKER_SPRING_PROFILES_DOCKER=docker
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 13. SPRING PROFILES AND GATEWAY
|
||||||
|
# =============================================================================
|
||||||
|
SPRING_PROFILES_ACTIVE=dev
|
||||||
|
GATEWAY_ADMIN_USER=admin
|
||||||
|
GATEWAY_ADMIN_PASSWORD=admin
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ Das Projekt wurde kürzlich auf eine modulare Architektur migriert, um die Wartb
|
||||||
|
|
||||||
- Java 21
|
- Java 21
|
||||||
- Kotlin 2.2.10
|
- Kotlin 2.2.10
|
||||||
- Gradle 8.11+ (automatischer Download über Gradle Wrapper)
|
- Gradle 9.0.0 (automatischer Download über Gradle Wrapper)
|
||||||
- Docker und Docker Compose (v2.0+)
|
- Docker und Docker Compose (v2.0+)
|
||||||
|
|
||||||
## Infrastruktur
|
## Infrastruktur
|
||||||
|
|
|
||||||
|
|
@ -54,13 +54,15 @@ services:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
# Authentifizierung
|
# Authentifizierung - Keycloak
|
||||||
|
# ===================================================================
|
||||||
|
# Production-ready Keycloak configuration with optimized settings
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
keycloak:
|
keycloak:
|
||||||
image: quay.io/keycloak/keycloak:${DOCKER_KEYCLOAK_VERSION:-26.0.7}
|
image: quay.io/keycloak/keycloak:${DOCKER_KEYCLOAK_VERSION:-26.4.0}
|
||||||
container_name: meldestelle-keycloak
|
container_name: meldestelle-keycloak
|
||||||
environment:
|
environment:
|
||||||
# Admin Configuration
|
# Admin Configuration - CHANGE IN PRODUCTION!
|
||||||
KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN:-admin}
|
KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN:-admin}
|
||||||
KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:-admin}
|
KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:-admin}
|
||||||
|
|
||||||
|
|
@ -70,21 +72,43 @@ services:
|
||||||
KC_DB_USERNAME: ${POSTGRES_USER:-meldestelle}
|
KC_DB_USERNAME: ${POSTGRES_USER:-meldestelle}
|
||||||
KC_DB_PASSWORD: ${POSTGRES_PASSWORD:-meldestelle}
|
KC_DB_PASSWORD: ${POSTGRES_PASSWORD:-meldestelle}
|
||||||
KC_DB_SCHEMA: keycloak
|
KC_DB_SCHEMA: keycloak
|
||||||
|
# Database connection pool optimization
|
||||||
|
KC_DB_POOL_INITIAL_SIZE: ${KC_DB_POOL_INITIAL_SIZE:-5}
|
||||||
|
KC_DB_POOL_MIN_SIZE: ${KC_DB_POOL_MIN_SIZE:-5}
|
||||||
|
KC_DB_POOL_MAX_SIZE: ${KC_DB_POOL_MAX_SIZE:-20}
|
||||||
|
|
||||||
# Keycloak Configuration
|
# Keycloak Server Configuration
|
||||||
KC_HTTP_PORT: 8080
|
KC_HTTP_PORT: 8080
|
||||||
KC_HOSTNAME_STRICT: false
|
KC_HOSTNAME_STRICT: ${KC_HOSTNAME_STRICT:-false}
|
||||||
KC_HOSTNAME_STRICT_HTTPS: false
|
KC_HOSTNAME_STRICT_HTTPS: ${KC_HOSTNAME_STRICT_HTTPS:-false}
|
||||||
KC_HTTP_ENABLED: true
|
KC_HTTP_ENABLED: ${KC_HTTP_ENABLED:-true}
|
||||||
KC_PROXY: edge
|
KC_PROXY: ${KC_PROXY:-edge}
|
||||||
|
KC_PROXY_HEADERS: ${KC_PROXY_HEADERS:-xforwarded}
|
||||||
|
|
||||||
# Development Settings
|
# Logging Configuration
|
||||||
KC_LOG_LEVEL: ${KEYCLOAK_LOG_LEVEL:-INFO}
|
KC_LOG_LEVEL: ${KEYCLOAK_LOG_LEVEL:-info}
|
||||||
KC_METRICS_ENABLED: true
|
KC_LOG_CONSOLE_COLOR: ${KC_LOG_CONSOLE_COLOR:-false}
|
||||||
KC_HEALTH_ENABLED: true
|
KC_LOG_CONSOLE_FORMAT: ${KC_LOG_CONSOLE_FORMAT:-json}
|
||||||
|
|
||||||
|
# Metrics and Health
|
||||||
|
KC_METRICS_ENABLED: ${KC_METRICS_ENABLED:-true}
|
||||||
|
KC_HEALTH_ENABLED: ${KC_HEALTH_ENABLED:-true}
|
||||||
|
|
||||||
|
# Cache Configuration (Infinispan)
|
||||||
|
KC_CACHE: ${KC_CACHE:-ispn}
|
||||||
|
KC_CACHE_STACK: ${KC_CACHE_STACK:-tcp}
|
||||||
|
|
||||||
|
# JVM Optimization for containers
|
||||||
|
JAVA_OPTS_APPEND: >-
|
||||||
|
-XX:MaxRAMPercentage=75.0
|
||||||
|
-XX:+UseG1GC
|
||||||
|
-XX:+UseStringDeduplication
|
||||||
|
-XX:+DisableExplicitGC
|
||||||
|
-Djava.net.preferIPv4Stack=true
|
||||||
|
-Duser.timezone=Europe/Vienna
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
- "8180:8080"
|
- "${KEYCLOAK_PORT:-8180}:8080"
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
@ -92,17 +116,20 @@ services:
|
||||||
- ./docker/services/keycloak:/opt/keycloak/data/import
|
- ./docker/services/keycloak:/opt/keycloak/data/import
|
||||||
- keycloak-data:/opt/keycloak/data
|
- keycloak-data:/opt/keycloak/data
|
||||||
command:
|
command:
|
||||||
- start-dev
|
# Production mode with optimizations
|
||||||
|
- start
|
||||||
|
- --optimized
|
||||||
- --import-realm
|
- --import-realm
|
||||||
- --http-port=8080
|
- --http-port=8080
|
||||||
|
# - --http-relative-path=/auth
|
||||||
networks:
|
networks:
|
||||||
- meldestelle-network
|
- meldestelle-network
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: [ "CMD", "curl", "-f", "http://localhost:8080/health/ready" ]
|
test: [ "CMD-SHELL", "exec 3<>/dev/tcp/localhost/8080 && echo -e 'GET /health/ready HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n' >&3 && cat <&3 | grep -q '200 OK'" ]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 5
|
retries: 5
|
||||||
start_period: 60s
|
start_period: 90s
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
|
|
@ -251,13 +278,14 @@ services:
|
||||||
CONSUL_PORT: ${CONSUL_PORT:-8500}
|
CONSUL_PORT: ${CONSUL_PORT:-8500}
|
||||||
CONSUL_ENABLED: "true"
|
CONSUL_ENABLED: "true"
|
||||||
GATEWAY_PORT: ${GATEWAY_PORT:-8081}
|
GATEWAY_PORT: ${GATEWAY_PORT:-8081}
|
||||||
# Keycloak-Integration
|
# Keycloak OAuth2 Integration (using Spring Security oauth2ResourceServer)
|
||||||
KEYCLOAK_SERVER_URL: http://keycloak:8080
|
KEYCLOAK_SERVER_URL: http://keycloak:8080
|
||||||
KEYCLOAK_ISSUER_URI: http://keycloak:8080/realms/meldestelle
|
KEYCLOAK_ISSUER_URI: http://keycloak:8080/realms/meldestelle
|
||||||
KEYCLOAK_JWK_SET_URI: http://keycloak:8080/realms/meldestelle/protocol/openid-connect/certs
|
KEYCLOAK_JWK_SET_URI: http://keycloak:8080/realms/meldestelle/protocol/openid-connect/certs
|
||||||
KEYCLOAK_REALM: meldestelle
|
KEYCLOAK_REALM: meldestelle
|
||||||
KEYCLOAK_CLIENT_ID: api-gateway
|
KEYCLOAK_CLIENT_ID: api-gateway
|
||||||
GATEWAY_SECURITY_KEYCLOAK_ENABLED: "true"
|
# Custom JWT filter disabled - using oauth2ResourceServer instead
|
||||||
|
GATEWAY_SECURITY_KEYCLOAK_ENABLED: "false"
|
||||||
ports:
|
ports:
|
||||||
- "${GATEWAY_PORT:-8081}:8081"
|
- "${GATEWAY_PORT:-8081}:8081"
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|
@ -267,6 +295,8 @@ services:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
redis:
|
redis:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
keycloak:
|
||||||
|
condition: service_healthy
|
||||||
networks:
|
networks:
|
||||||
- meldestelle-network
|
- meldestelle-network
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
|
|
||||||
297
docker/services/keycloak/meldestelle-realm.json
Normal file
297
docker/services/keycloak/meldestelle-realm.json
Normal file
|
|
@ -0,0 +1,297 @@
|
||||||
|
{
|
||||||
|
"realm": "meldestelle",
|
||||||
|
"enabled": true,
|
||||||
|
"displayName": "Meldestelle Authentication",
|
||||||
|
"displayNameHtml": "<div class=\"kc-logo-text\"><span>Meldestelle</span></div>",
|
||||||
|
"sslRequired": "external",
|
||||||
|
"registrationAllowed": false,
|
||||||
|
"registrationEmailAsUsername": false,
|
||||||
|
"rememberMe": true,
|
||||||
|
"verifyEmail": false,
|
||||||
|
"loginWithEmailAllowed": true,
|
||||||
|
"duplicateEmailsAllowed": false,
|
||||||
|
"resetPasswordAllowed": true,
|
||||||
|
"editUsernameAllowed": false,
|
||||||
|
"bruteForceProtected": true,
|
||||||
|
"permanentLockout": false,
|
||||||
|
"maxFailureWaitSeconds": 900,
|
||||||
|
"minimumQuickLoginWaitSeconds": 60,
|
||||||
|
"waitIncrementSeconds": 60,
|
||||||
|
"quickLoginCheckMilliSeconds": 1000,
|
||||||
|
"maxDeltaTimeSeconds": 43200,
|
||||||
|
"failureFactor": 5,
|
||||||
|
"defaultSignatureAlgorithm": "RS256",
|
||||||
|
"offlineSessionMaxLifespan": 5184000,
|
||||||
|
"offlineSessionMaxLifespanEnabled": false,
|
||||||
|
"accessTokenLifespan": 300,
|
||||||
|
"accessTokenLifespanForImplicitFlow": 900,
|
||||||
|
"ssoSessionIdleTimeout": 1800,
|
||||||
|
"ssoSessionMaxLifespan": 36000,
|
||||||
|
"refreshTokenMaxReuse": 0,
|
||||||
|
"accessCodeLifespan": 60,
|
||||||
|
"accessCodeLifespanUserAction": 300,
|
||||||
|
"accessCodeLifespanLogin": 1800,
|
||||||
|
"actionTokenGeneratedByAdminLifespan": 43200,
|
||||||
|
"actionTokenGeneratedByUserLifespan": 300,
|
||||||
|
"oauth2DeviceCodeLifespan": 600,
|
||||||
|
"oauth2DevicePollingInterval": 5,
|
||||||
|
"internationalizationEnabled": true,
|
||||||
|
"supportedLocales": ["de", "en"],
|
||||||
|
"defaultLocale": "de",
|
||||||
|
"roles": {
|
||||||
|
"realm": [
|
||||||
|
{
|
||||||
|
"name": "ADMIN",
|
||||||
|
"description": "Administrator role with full system access",
|
||||||
|
"composite": false,
|
||||||
|
"clientRole": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "USER",
|
||||||
|
"description": "Standard user role with limited access",
|
||||||
|
"composite": false,
|
||||||
|
"clientRole": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "MONITORING",
|
||||||
|
"description": "Monitoring role for system health checks",
|
||||||
|
"composite": false,
|
||||||
|
"clientRole": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GUEST",
|
||||||
|
"description": "Guest role with minimal access",
|
||||||
|
"composite": false,
|
||||||
|
"clientRole": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"clients": [
|
||||||
|
{
|
||||||
|
"clientId": "api-gateway",
|
||||||
|
"name": "API Gateway Client",
|
||||||
|
"description": "OAuth2 client for the Meldestelle API Gateway",
|
||||||
|
"enabled": true,
|
||||||
|
"alwaysDisplayInConsole": false,
|
||||||
|
"clientAuthenticatorType": "client-secret",
|
||||||
|
"secret": "**********",
|
||||||
|
"redirectUris": [
|
||||||
|
"http://localhost:8081/*",
|
||||||
|
"http://localhost:3000/*",
|
||||||
|
"https://app.meldestelle.at/*"
|
||||||
|
],
|
||||||
|
"webOrigins": [
|
||||||
|
"http://localhost:8081",
|
||||||
|
"http://localhost:3000",
|
||||||
|
"https://app.meldestelle.at"
|
||||||
|
],
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"bearerOnly": false,
|
||||||
|
"publicClient": false,
|
||||||
|
"standardFlowEnabled": true,
|
||||||
|
"implicitFlowEnabled": false,
|
||||||
|
"directAccessGrantsEnabled": true,
|
||||||
|
"serviceAccountsEnabled": true,
|
||||||
|
"authorizationServicesEnabled": false,
|
||||||
|
"fullScopeAllowed": true,
|
||||||
|
"frontchannelLogout": true,
|
||||||
|
"attributes": {
|
||||||
|
"access.token.lifespan": "300",
|
||||||
|
"client.secret.creation.time": "0",
|
||||||
|
"oauth2.device.authorization.grant.enabled": "false",
|
||||||
|
"backchannel.logout.session.required": "true",
|
||||||
|
"backchannel.logout.revoke.offline.tokens": "false"
|
||||||
|
},
|
||||||
|
"protocolMappers": [
|
||||||
|
{
|
||||||
|
"name": "realm-roles",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-usermodel-realm-role-mapper",
|
||||||
|
"consentRequired": false,
|
||||||
|
"config": {
|
||||||
|
"multivalued": "true",
|
||||||
|
"userinfo.token.claim": "true",
|
||||||
|
"id.token.claim": "true",
|
||||||
|
"access.token.claim": "true",
|
||||||
|
"claim.name": "realm_access.roles",
|
||||||
|
"jsonType.label": "String"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "client-roles",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-usermodel-client-role-mapper",
|
||||||
|
"consentRequired": false,
|
||||||
|
"config": {
|
||||||
|
"multivalued": "true",
|
||||||
|
"userinfo.token.claim": "true",
|
||||||
|
"id.token.claim": "true",
|
||||||
|
"access.token.claim": "true",
|
||||||
|
"claim.name": "resource_access.${client_id}.roles",
|
||||||
|
"jsonType.label": "String"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "username",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-usermodel-property-mapper",
|
||||||
|
"consentRequired": false,
|
||||||
|
"config": {
|
||||||
|
"userinfo.token.claim": "true",
|
||||||
|
"user.attribute": "username",
|
||||||
|
"id.token.claim": "true",
|
||||||
|
"access.token.claim": "true",
|
||||||
|
"claim.name": "preferred_username",
|
||||||
|
"jsonType.label": "String"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "email",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-usermodel-property-mapper",
|
||||||
|
"consentRequired": false,
|
||||||
|
"config": {
|
||||||
|
"userinfo.token.claim": "true",
|
||||||
|
"user.attribute": "email",
|
||||||
|
"id.token.claim": "true",
|
||||||
|
"access.token.claim": "true",
|
||||||
|
"claim.name": "email",
|
||||||
|
"jsonType.label": "String"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "full-name",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-full-name-mapper",
|
||||||
|
"consentRequired": false,
|
||||||
|
"config": {
|
||||||
|
"id.token.claim": "true",
|
||||||
|
"access.token.claim": "true",
|
||||||
|
"userinfo.token.claim": "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"clientId": "web-app",
|
||||||
|
"name": "Web Application Client",
|
||||||
|
"description": "Public client for web frontend",
|
||||||
|
"enabled": true,
|
||||||
|
"publicClient": true,
|
||||||
|
"standardFlowEnabled": true,
|
||||||
|
"implicitFlowEnabled": false,
|
||||||
|
"directAccessGrantsEnabled": false,
|
||||||
|
"redirectUris": [
|
||||||
|
"http://localhost:3000/*",
|
||||||
|
"https://app.meldestelle.at/*"
|
||||||
|
],
|
||||||
|
"webOrigins": [
|
||||||
|
"http://localhost:3000",
|
||||||
|
"https://app.meldestelle.at"
|
||||||
|
],
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"attributes": {
|
||||||
|
"pkce.code.challenge.method": "S256"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"username": "admin",
|
||||||
|
"enabled": true,
|
||||||
|
"emailVerified": true,
|
||||||
|
"firstName": "System",
|
||||||
|
"lastName": "Administrator",
|
||||||
|
"email": "admin@meldestelle.local",
|
||||||
|
"credentials": [
|
||||||
|
{
|
||||||
|
"type": "password",
|
||||||
|
"value": "Change_Me_In_Production!",
|
||||||
|
"temporary": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"realmRoles": ["ADMIN", "USER"],
|
||||||
|
"clientRoles": {
|
||||||
|
"api-gateway": ["ADMIN"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"groups": [],
|
||||||
|
"defaultRoles": ["USER", "GUEST"],
|
||||||
|
"requiredCredentials": ["password"],
|
||||||
|
"passwordPolicy": "length(8) and digits(1) and lowerCase(1) and upperCase(1) and specialChars(1) and notUsername",
|
||||||
|
"otpPolicyType": "totp",
|
||||||
|
"otpPolicyAlgorithm": "HmacSHA1",
|
||||||
|
"otpPolicyInitialCounter": 0,
|
||||||
|
"otpPolicyDigits": 6,
|
||||||
|
"otpPolicyLookAheadWindow": 1,
|
||||||
|
"otpPolicyPeriod": 30,
|
||||||
|
"otpSupportedApplications": ["FreeOTP", "Google Authenticator"],
|
||||||
|
"webAuthnPolicyRpEntityName": "meldestelle",
|
||||||
|
"webAuthnPolicySignatureAlgorithms": ["ES256", "RS256"],
|
||||||
|
"smtpServer": {},
|
||||||
|
"eventsEnabled": true,
|
||||||
|
"eventsListeners": ["jboss-logging"],
|
||||||
|
"enabledEventTypes": [
|
||||||
|
"LOGIN",
|
||||||
|
"LOGIN_ERROR",
|
||||||
|
"LOGOUT",
|
||||||
|
"REGISTER",
|
||||||
|
"REGISTER_ERROR",
|
||||||
|
"UPDATE_PASSWORD",
|
||||||
|
"UPDATE_PASSWORD_ERROR"
|
||||||
|
],
|
||||||
|
"adminEventsEnabled": true,
|
||||||
|
"adminEventsDetailsEnabled": true,
|
||||||
|
"identityProviders": [],
|
||||||
|
"identityProviderMappers": [],
|
||||||
|
"components": {
|
||||||
|
"org.keycloak.keys.KeyProvider": [
|
||||||
|
{
|
||||||
|
"name": "rsa-generated",
|
||||||
|
"providerId": "rsa-generated",
|
||||||
|
"subComponents": {},
|
||||||
|
"config": {
|
||||||
|
"priority": ["100"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hmac-generated",
|
||||||
|
"providerId": "hmac-generated",
|
||||||
|
"subComponents": {},
|
||||||
|
"config": {
|
||||||
|
"priority": ["100"],
|
||||||
|
"algorithm": ["HS256"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "aes-generated",
|
||||||
|
"providerId": "aes-generated",
|
||||||
|
"subComponents": {},
|
||||||
|
"config": {
|
||||||
|
"priority": ["100"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"internationalizationEnabled": true,
|
||||||
|
"supportedLocales": ["de", "en"],
|
||||||
|
"defaultLocale": "de",
|
||||||
|
"authenticationFlows": [],
|
||||||
|
"authenticatorConfig": [],
|
||||||
|
"requiredActions": [],
|
||||||
|
"browserFlow": "browser",
|
||||||
|
"registrationFlow": "registration",
|
||||||
|
"directGrantFlow": "direct grant",
|
||||||
|
"resetCredentialsFlow": "reset credentials",
|
||||||
|
"clientAuthenticationFlow": "clients",
|
||||||
|
"dockerAuthenticationFlow": "docker auth",
|
||||||
|
"attributes": {
|
||||||
|
"frontendUrl": "",
|
||||||
|
"acr.loa.map": "{}",
|
||||||
|
"clientOfflineSessionMaxLifespan": "0",
|
||||||
|
"clientSessionIdleTimeout": "0",
|
||||||
|
"clientSessionMaxLifespan": "0",
|
||||||
|
"clientOfflineSessionIdleTimeout": "0"
|
||||||
|
}
|
||||||
|
}
|
||||||
31
docker/services/postgres/01-init-keycloak-schema.sql
Normal file
31
docker/services/postgres/01-init-keycloak-schema.sql
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
-- ===================================================================
|
||||||
|
-- PostgreSQL Initialization Script for Keycloak
|
||||||
|
-- ===================================================================
|
||||||
|
-- This script creates a separate schema for Keycloak data within the
|
||||||
|
-- meldestelle database, providing isolation and better organization.
|
||||||
|
--
|
||||||
|
-- Execution: Automatically executed by PostgreSQL on first startup
|
||||||
|
-- via docker-entrypoint-initdb.d mechanism.
|
||||||
|
-- ===================================================================
|
||||||
|
|
||||||
|
-- Create Keycloak schema if it doesn't exist
|
||||||
|
CREATE SCHEMA IF NOT EXISTS keycloak;
|
||||||
|
|
||||||
|
-- Grant all privileges on the schema to the meldestelle user
|
||||||
|
GRANT ALL PRIVILEGES ON SCHEMA keycloak TO meldestelle;
|
||||||
|
|
||||||
|
-- Grant usage on the schema
|
||||||
|
GRANT USAGE ON SCHEMA keycloak TO meldestelle;
|
||||||
|
|
||||||
|
-- Set default privileges for future tables in the keycloak schema
|
||||||
|
ALTER DEFAULT PRIVILEGES IN SCHEMA keycloak GRANT ALL ON TABLES TO meldestelle;
|
||||||
|
ALTER DEFAULT PRIVILEGES IN SCHEMA keycloak GRANT ALL ON SEQUENCES TO meldestelle;
|
||||||
|
ALTER DEFAULT PRIVILEGES IN SCHEMA keycloak GRANT ALL ON FUNCTIONS TO meldestelle;
|
||||||
|
|
||||||
|
-- Log successful schema creation
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
RAISE NOTICE 'Keycloak schema created successfully';
|
||||||
|
RAISE NOTICE 'Schema: keycloak';
|
||||||
|
RAISE NOTICE 'Owner: meldestelle';
|
||||||
|
END $$;
|
||||||
|
|
@ -51,6 +51,10 @@ ENV GRADLE_USER_HOME=/home/gradle/.gradle
|
||||||
# Copy build files in optimal order for caching
|
# Copy build files in optimal order for caching
|
||||||
COPY gradlew gradlew.bat gradle.properties settings.gradle.kts ./
|
COPY gradlew gradlew.bat gradle.properties settings.gradle.kts ./
|
||||||
COPY gradle/ gradle/
|
COPY gradle/ gradle/
|
||||||
|
|
||||||
|
# Make gradlew executable (required on Linux/Unix systems)
|
||||||
|
RUN chmod +x gradlew
|
||||||
|
|
||||||
COPY platform/ platform/
|
COPY platform/ platform/
|
||||||
COPY core/ core/
|
COPY core/ core/
|
||||||
COPY build.gradle.kts ./
|
COPY build.gradle.kts ./
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,9 @@ ENV GRADLE_USER_HOME=/home/gradle/.gradle
|
||||||
COPY gradlew gradlew.bat gradle.properties settings.gradle.kts ./
|
COPY gradlew gradlew.bat gradle.properties settings.gradle.kts ./
|
||||||
COPY gradle/ gradle/
|
COPY gradle/ gradle/
|
||||||
|
|
||||||
|
# Make gradlew executable (required on Linux/Unix systems)
|
||||||
|
RUN chmod +x gradlew
|
||||||
|
|
||||||
# Copy platform dependencies (changes less frequently)
|
# Copy platform dependencies (changes less frequently)
|
||||||
COPY platform/ platform/
|
COPY platform/ platform/
|
||||||
COPY core/ core/
|
COPY core/ core/
|
||||||
|
|
|
||||||
45
dockerfiles/infrastructure/keycloak/Dockerfile
Normal file
45
dockerfiles/infrastructure/keycloak/Dockerfile
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
# syntax=docker/dockerfile:1.8
|
||||||
|
# ===================================================================
|
||||||
|
# Production-Ready Keycloak Dockerfile
|
||||||
|
# ===================================================================
|
||||||
|
# Based on: quay.io/keycloak/keycloak:26.0.7
|
||||||
|
# Features:
|
||||||
|
# - Pre-built optimized image (faster startup)
|
||||||
|
# - Security hardening
|
||||||
|
# - Custom theme support
|
||||||
|
# - Health monitoring
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
ARG KEYCLOAK_VERSION=26.4.0
|
||||||
|
|
||||||
|
# Build stage - optimize Keycloak
|
||||||
|
FROM quay.io/keycloak/keycloak:${KEYCLOAK_VERSION} AS builder
|
||||||
|
|
||||||
|
ENV KC_HEALTH_ENABLED=true
|
||||||
|
ENV KC_METRICS_ENABLED=true
|
||||||
|
ENV KC_DB=postgres
|
||||||
|
|
||||||
|
WORKDIR /opt/keycloak
|
||||||
|
|
||||||
|
# Pre-build Keycloak for faster startup
|
||||||
|
RUN /opt/keycloak/bin/kc.sh build \
|
||||||
|
--db=postgres \
|
||||||
|
--health-enabled=true \
|
||||||
|
--metrics-enabled=true \
|
||||||
|
--cache=ispn \
|
||||||
|
--cache-stack=tcp
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM quay.io/keycloak/keycloak:${KEYCLOAK_VERSION}
|
||||||
|
|
||||||
|
LABEL maintainer="Meldestelle Development Team"
|
||||||
|
LABEL description="Production-ready Keycloak for Meldestelle authentication"
|
||||||
|
LABEL version="${KEYCLOAK_VERSION}"
|
||||||
|
|
||||||
|
# Copy pre-built Keycloak
|
||||||
|
COPY --from=builder /opt/keycloak/ /opt/keycloak/
|
||||||
|
|
||||||
|
# Set user
|
||||||
|
USER 1000
|
||||||
|
|
||||||
|
ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]
|
||||||
|
|
@ -37,6 +37,10 @@ ENV GRADLE_OPTS="-Dorg.gradle.caching=true \
|
||||||
# Copy build files in optimal order for caching
|
# Copy build files in optimal order for caching
|
||||||
COPY gradlew gradlew.bat gradle.properties settings.gradle.kts ./
|
COPY gradlew gradlew.bat gradle.properties settings.gradle.kts ./
|
||||||
COPY gradle/ gradle/
|
COPY gradle/ gradle/
|
||||||
|
|
||||||
|
# Make gradlew executable (required on Linux/Unix systems)
|
||||||
|
RUN chmod +x gradlew
|
||||||
|
|
||||||
COPY platform/ platform/
|
COPY platform/ platform/
|
||||||
COPY core/ core/
|
COPY core/ core/
|
||||||
COPY build.gradle.kts ./
|
COPY build.gradle.kts ./
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,10 @@ ENV GRADLE_OPTS="-Dorg.gradle.caching=true \
|
||||||
# Copy build files in optimal order for caching
|
# Copy build files in optimal order for caching
|
||||||
COPY gradlew gradlew.bat gradle.properties settings.gradle.kts ./
|
COPY gradlew gradlew.bat gradle.properties settings.gradle.kts ./
|
||||||
COPY gradle/ gradle/
|
COPY gradle/ gradle/
|
||||||
|
|
||||||
|
# Make gradlew executable (required on Linux/Unix systems)
|
||||||
|
RUN chmod +x gradlew
|
||||||
|
|
||||||
COPY platform/ platform/
|
COPY platform/ platform/
|
||||||
COPY core/ core/
|
COPY core/ core/
|
||||||
COPY build.gradle.kts ./
|
COPY build.gradle.kts ./
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,10 @@ ENV GRADLE_OPTS="-Dorg.gradle.caching=true \
|
||||||
# Copy build files in optimal order for caching
|
# Copy build files in optimal order for caching
|
||||||
COPY gradlew gradlew.bat gradle.properties settings.gradle.kts ./
|
COPY gradlew gradlew.bat gradle.properties settings.gradle.kts ./
|
||||||
COPY gradle/ gradle/
|
COPY gradle/ gradle/
|
||||||
|
|
||||||
|
# Make gradlew executable (required on Linux/Unix systems)
|
||||||
|
RUN chmod +x gradlew
|
||||||
|
|
||||||
COPY platform/ platform/
|
COPY platform/ platform/
|
||||||
COPY core/ core/
|
COPY core/ core/
|
||||||
COPY build.gradle.kts ./
|
COPY build.gradle.kts ./
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,10 @@ ENV GRADLE_OPTS="-Dorg.gradle.caching=true \
|
||||||
# Copy build files in optimal order for caching
|
# Copy build files in optimal order for caching
|
||||||
COPY gradlew gradlew.bat gradle.properties settings.gradle.kts ./
|
COPY gradlew gradlew.bat gradle.properties settings.gradle.kts ./
|
||||||
COPY gradle/ gradle/
|
COPY gradle/ gradle/
|
||||||
|
|
||||||
|
# Make gradlew executable (required on Linux/Unix systems)
|
||||||
|
RUN chmod +x gradlew
|
||||||
|
|
||||||
COPY platform/ platform/
|
COPY platform/ platform/
|
||||||
COPY core/ core/
|
COPY core/ core/
|
||||||
COPY build.gradle.kts ./
|
COPY build.gradle.kts ./
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,10 @@ ENV GRADLE_OPTS="-Dorg.gradle.caching=true \
|
||||||
# Copy build files in optimal order for caching
|
# Copy build files in optimal order for caching
|
||||||
COPY gradlew gradlew.bat gradle.properties settings.gradle.kts ./
|
COPY gradlew gradlew.bat gradle.properties settings.gradle.kts ./
|
||||||
COPY gradle/ gradle/
|
COPY gradle/ gradle/
|
||||||
|
|
||||||
|
# Make gradlew executable (required on Linux/Unix systems)
|
||||||
|
RUN chmod +x gradlew
|
||||||
|
|
||||||
COPY platform/ platform/
|
COPY platform/ platform/
|
||||||
COPY core/ core/
|
COPY core/ core/
|
||||||
COPY build.gradle.kts ./
|
COPY build.gradle.kts ./
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,9 @@ ENV GRADLE_USER_HOME=/home/gradle/.gradle
|
||||||
COPY gradlew gradlew.bat gradle.properties settings.gradle.kts ./
|
COPY gradlew gradlew.bat gradle.properties settings.gradle.kts ./
|
||||||
COPY gradle/ gradle/
|
COPY gradle/ gradle/
|
||||||
|
|
||||||
|
# Make gradlew executable (required on Linux/Unix systems)
|
||||||
|
RUN chmod +x gradlew
|
||||||
|
|
||||||
# Copy platform dependencies (changes less frequently)
|
# Copy platform dependencies (changes less frequently)
|
||||||
COPY platform/ platform/
|
COPY platform/ platform/
|
||||||
|
|
||||||
|
|
|
||||||
458
docs/KEYCLOAK-SETUP.md
Normal file
458
docs/KEYCLOAK-SETUP.md
Normal file
|
|
@ -0,0 +1,458 @@
|
||||||
|
# Keycloak Integration - Setup und Konfiguration
|
||||||
|
|
||||||
|
## Übersicht
|
||||||
|
|
||||||
|
Dieses Dokument beschreibt die vollständige Keycloak-Integration für das Meldestelle-System, einschließlich Authentifizierung, Konfiguration und Best Practices.
|
||||||
|
|
||||||
|
## Architektur
|
||||||
|
|
||||||
|
### Authentifizierungsansatz
|
||||||
|
|
||||||
|
Das System verwendet **Spring Security OAuth2 Resource Server** für die JWT-Validierung:
|
||||||
|
|
||||||
|
- ✅ **Empfohlener Ansatz**: Spring Security `oauth2ResourceServer`
|
||||||
|
- Kryptographisch sichere JWT-Signaturvalidierung
|
||||||
|
- Automatische JWK-Set-Aktualisierung
|
||||||
|
- Standardkonform (RFC 7519, RFC 7517)
|
||||||
|
- Integriert mit Spring Security Authorization
|
||||||
|
|
||||||
|
- ❌ **NICHT verwendet**: Custom JWT Filter
|
||||||
|
- Frühere Implementierungen wurden entfernt
|
||||||
|
- Hatten Sicherheitslücken (fehlende Signaturvalidierung)
|
||||||
|
|
||||||
|
### Komponenten
|
||||||
|
|
||||||
|
1. **Keycloak Server** (Port 8180 extern, 8080 intern)
|
||||||
|
- OAuth2/OpenID Connect Provider
|
||||||
|
- PostgreSQL Backend
|
||||||
|
- Realm: `meldestelle`
|
||||||
|
|
||||||
|
2. **API Gateway**
|
||||||
|
- OAuth2 Resource Server
|
||||||
|
- JWT-Validierung via JWK-Set
|
||||||
|
- Rollenbasierte Autorisierung
|
||||||
|
|
||||||
|
3. **PostgreSQL Database**
|
||||||
|
- Keycloak-Schema: `keycloak`
|
||||||
|
- Automatische Schema-Initialisierung
|
||||||
|
|
||||||
|
## Konfiguration
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
#### Keycloak Service
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
keycloak:
|
||||||
|
image: quay.io/keycloak/keycloak:26.0.7
|
||||||
|
environment:
|
||||||
|
# Admin-Zugangsdaten (IN PRODUKTION ÄNDERN!)
|
||||||
|
KEYCLOAK_ADMIN: admin
|
||||||
|
KEYCLOAK_ADMIN_PASSWORD: admin
|
||||||
|
|
||||||
|
# Datenbank
|
||||||
|
KC_DB: postgres
|
||||||
|
KC_DB_URL: jdbc:postgresql://postgres:5432/meldestelle
|
||||||
|
KC_DB_SCHEMA: keycloak
|
||||||
|
|
||||||
|
# Connection Pool Optimierung
|
||||||
|
KC_DB_POOL_INITIAL_SIZE: 5
|
||||||
|
KC_DB_POOL_MIN_SIZE: 5
|
||||||
|
KC_DB_POOL_MAX_SIZE: 20
|
||||||
|
|
||||||
|
# JVM Optimierung
|
||||||
|
JAVA_OPTS_APPEND: >-
|
||||||
|
-XX:MaxRAMPercentage=75.0
|
||||||
|
-XX:+UseG1GC
|
||||||
|
-XX:+UseStringDeduplication
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Produktionsmodus
|
||||||
|
|
||||||
|
Der Service läuft im **Produktionsmodus** (`start --optimized`):
|
||||||
|
- Schnellerer Start durch Pre-Build
|
||||||
|
- Optimierte Performance
|
||||||
|
- Geeignet für Produktionsumgebungen
|
||||||
|
|
||||||
|
**Wichtig**: Für Entwicklung kann auf `start-dev` umgestellt werden.
|
||||||
|
|
||||||
|
### Realm-Konfiguration
|
||||||
|
|
||||||
|
**Datei**: `docker/services/keycloak/meldestelle-realm.json`
|
||||||
|
|
||||||
|
#### Realm: `meldestelle`
|
||||||
|
|
||||||
|
- **Display Name**: Meldestelle Authentication
|
||||||
|
- **Sprachen**: Deutsch (Standard), Englisch
|
||||||
|
- **SSL**: External (hinter Reverse Proxy)
|
||||||
|
|
||||||
|
#### Sicherheitseinstellungen
|
||||||
|
|
||||||
|
- **Brute Force Protection**: Aktiviert
|
||||||
|
- Max. 5 Fehlversuche
|
||||||
|
- 15 Minuten Sperrzeit
|
||||||
|
|
||||||
|
- **Password Policy**:
|
||||||
|
- Mindestens 8 Zeichen
|
||||||
|
- Mind. 1 Ziffer, 1 Kleinbuchstabe, 1 Großbuchstabe, 1 Sonderzeichen
|
||||||
|
- Nicht identisch mit Username
|
||||||
|
|
||||||
|
#### Token-Einstellungen
|
||||||
|
|
||||||
|
- **Access Token Lifespan**: 5 Minuten (300 Sek.)
|
||||||
|
- **SSO Session Idle**: 30 Minuten (1800 Sek.)
|
||||||
|
- **SSO Session Max**: 10 Stunden (36000 Sek.)
|
||||||
|
- **Refresh Token**: Einmalige Verwendung
|
||||||
|
|
||||||
|
### Clients
|
||||||
|
|
||||||
|
#### 1. api-gateway (Confidential Client)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"clientId": "api-gateway",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"publicClient": false,
|
||||||
|
"bearerOnly": false,
|
||||||
|
"standardFlowEnabled": true,
|
||||||
|
"directAccessGrantsEnabled": true,
|
||||||
|
"serviceAccountsEnabled": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verwendung**: Backend-Service-to-Service Kommunikation
|
||||||
|
|
||||||
|
**Secret**: Muss in Keycloak UI generiert und konfiguriert werden
|
||||||
|
|
||||||
|
#### 2. web-app (Public Client)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"clientId": "web-app",
|
||||||
|
"publicClient": true,
|
||||||
|
"standardFlowEnabled": true,
|
||||||
|
"attributes": {
|
||||||
|
"pkce.code.challenge.method": "S256"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verwendung**: Frontend Single-Page Application (mit PKCE)
|
||||||
|
|
||||||
|
### Rollen
|
||||||
|
|
||||||
|
| Rolle | Beschreibung | Verwendung |
|
||||||
|
|-------|--------------|------------|
|
||||||
|
| `ADMIN` | Vollzugriff | Systemadministration |
|
||||||
|
| `USER` | Standardbenutzer | Normale Anwendungsfunktionen |
|
||||||
|
| `MONITORING` | Überwachung | Metriken und Health Checks |
|
||||||
|
| `GUEST` | Gast | Minimaler Zugriff |
|
||||||
|
|
||||||
|
### Standard-Benutzer
|
||||||
|
|
||||||
|
**Username**: `admin`
|
||||||
|
**Passwort**: `Change_Me_In_Production!` (temporär, muss beim ersten Login geändert werden)
|
||||||
|
**Rollen**: ADMIN, USER
|
||||||
|
|
||||||
|
## Spring Security Konfiguration
|
||||||
|
|
||||||
|
### Gateway SecurityConfig
|
||||||
|
|
||||||
|
**Datei**: `infrastructure/gateway/src/main/kotlin/.../SecurityConfig.kt`
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
@Bean
|
||||||
|
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||||
|
return http
|
||||||
|
.oauth2ResourceServer { oauth2 ->
|
||||||
|
oauth2.jwt { jwt ->
|
||||||
|
jwt.jwtDecoder(jwtDecoder())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.authorizeExchange { exchanges ->
|
||||||
|
exchanges
|
||||||
|
.pathMatchers("/api/admin/**").hasRole("ADMIN")
|
||||||
|
.pathMatchers("/api/monitoring/**").hasAnyRole("ADMIN", "MONITORING")
|
||||||
|
.anyExchange().authenticated()
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Application Properties
|
||||||
|
|
||||||
|
**Datei**: `application-keycloak.yml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
spring:
|
||||||
|
security:
|
||||||
|
oauth2:
|
||||||
|
resourceserver:
|
||||||
|
jwt:
|
||||||
|
issuer-uri: http://keycloak:8080/realms/meldestelle
|
||||||
|
jwk-set-uri: http://keycloak:8080/realms/meldestelle/protocol/openid-connect/certs
|
||||||
|
|
||||||
|
gateway:
|
||||||
|
security:
|
||||||
|
keycloak:
|
||||||
|
enabled: false # Custom filter deaktiviert - oauth2ResourceServer wird verwendet
|
||||||
|
```
|
||||||
|
|
||||||
|
## Datenbank-Setup
|
||||||
|
|
||||||
|
### PostgreSQL Schema
|
||||||
|
|
||||||
|
**Datei**: `docker/services/postgres/01-init-keycloak-schema.sql`
|
||||||
|
|
||||||
|
Das Schema wird automatisch beim ersten Start von PostgreSQL erstellt:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE SCHEMA IF NOT EXISTS keycloak;
|
||||||
|
GRANT ALL PRIVILEGES ON SCHEMA keycloak TO meldestelle;
|
||||||
|
```
|
||||||
|
|
||||||
|
Keycloak erstellt seine Tabellen automatisch im `keycloak` Schema.
|
||||||
|
|
||||||
|
## Produktion Deployment
|
||||||
|
|
||||||
|
### Dockerfile
|
||||||
|
|
||||||
|
**Optional**: `dockerfiles/infrastructure/keycloak/Dockerfile`
|
||||||
|
|
||||||
|
Pre-built optimiertes Image für schnelleren Start:
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM quay.io/keycloak/keycloak:26.0.7 AS builder
|
||||||
|
RUN /opt/keycloak/bin/kc.sh build --db=postgres
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verwendung in docker-compose.yml**:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
keycloak:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: dockerfiles/infrastructure/keycloak/Dockerfile
|
||||||
|
```
|
||||||
|
|
||||||
|
### Umgebungsvariablen für Produktion
|
||||||
|
|
||||||
|
Erstellen Sie eine `.env` Datei:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Keycloak Admin (ÄNDERN!)
|
||||||
|
KEYCLOAK_ADMIN=admin
|
||||||
|
KEYCLOAK_ADMIN_PASSWORD=<starkes_passwort>
|
||||||
|
|
||||||
|
# Datenbank
|
||||||
|
POSTGRES_USER=meldestelle
|
||||||
|
POSTGRES_PASSWORD=<db_passwort>
|
||||||
|
POSTGRES_DB=meldestelle
|
||||||
|
|
||||||
|
# Keycloak Konfiguration
|
||||||
|
KC_HOSTNAME_STRICT=true
|
||||||
|
KC_HOSTNAME_STRICT_HTTPS=true
|
||||||
|
KC_HTTP_ENABLED=false
|
||||||
|
KC_PROXY=edge
|
||||||
|
|
||||||
|
# JVM Memory (optional)
|
||||||
|
KC_DB_POOL_MAX_SIZE=50
|
||||||
|
```
|
||||||
|
|
||||||
|
### SSL/TLS
|
||||||
|
|
||||||
|
Für HTTPS in Produktion:
|
||||||
|
|
||||||
|
1. **Empfohlen**: Reverse Proxy (nginx, Traefik)
|
||||||
|
```yaml
|
||||||
|
KC_PROXY: edge
|
||||||
|
KC_HTTP_ENABLED: true
|
||||||
|
KC_HOSTNAME_STRICT_HTTPS: false
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Direkt mit Keycloak**:
|
||||||
|
```yaml
|
||||||
|
KC_HTTPS_CERTIFICATE_FILE: /path/to/cert.pem
|
||||||
|
KC_HTTPS_CERTIFICATE_KEY_FILE: /path/to/key.pem
|
||||||
|
KC_HTTP_ENABLED: false
|
||||||
|
```
|
||||||
|
|
||||||
|
## Betrieb
|
||||||
|
|
||||||
|
### Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Alle Services starten
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Nur Infrastruktur (inkl. Keycloak)
|
||||||
|
docker-compose up -d postgres redis keycloak consul
|
||||||
|
```
|
||||||
|
|
||||||
|
### Keycloak Admin Console
|
||||||
|
|
||||||
|
**URL**: http://localhost:8180
|
||||||
|
**Username**: admin
|
||||||
|
**Passwort**: admin (Standard)
|
||||||
|
|
||||||
|
### Health Checks
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Keycloak Readiness
|
||||||
|
curl http://localhost:8180/health/ready
|
||||||
|
|
||||||
|
# Keycloak Liveness
|
||||||
|
curl http://localhost:8180/health/live
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
curl http://localhost:8180/metrics
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Keycloak Logs anzeigen
|
||||||
|
docker-compose logs -f keycloak
|
||||||
|
|
||||||
|
# Letzte 100 Zeilen
|
||||||
|
docker-compose logs --tail=100 keycloak
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Problem: Keycloak startet nicht
|
||||||
|
|
||||||
|
**Symptom**: Container stoppt sofort nach Start
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. Prüfen Sie PostgreSQL Verbindung:
|
||||||
|
```bash
|
||||||
|
docker-compose logs postgres
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Schema-Berechtigungen prüfen:
|
||||||
|
```sql
|
||||||
|
\dn+ keycloak
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Keycloak Logs prüfen:
|
||||||
|
```bash
|
||||||
|
docker-compose logs keycloak
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problem: JWT Validierung schlägt fehl
|
||||||
|
|
||||||
|
**Symptom**: 401 Unauthorized trotz gültigem Token
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. Issuer URI prüfen:
|
||||||
|
```bash
|
||||||
|
curl http://keycloak:8080/realms/meldestelle/.well-known/openid-configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
2. JWK-Set prüfen:
|
||||||
|
```bash
|
||||||
|
curl http://keycloak:8080/realms/meldestelle/protocol/openid-connect/certs
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Gateway-Logs prüfen:
|
||||||
|
```bash
|
||||||
|
docker-compose logs api-gateway | grep -i jwt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problem: Realm nicht importiert
|
||||||
|
|
||||||
|
**Symptom**: Realm `meldestelle` existiert nicht
|
||||||
|
|
||||||
|
**Lösung**:
|
||||||
|
1. Prüfen Sie Volume-Mount:
|
||||||
|
```bash
|
||||||
|
docker-compose config | grep -A 5 "keycloak:" | grep volumes
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Realm-Datei prüfen:
|
||||||
|
```bash
|
||||||
|
ls -la docker/services/keycloak/
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Container neu starten:
|
||||||
|
```bash
|
||||||
|
docker-compose restart keycloak
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration von alter Implementierung
|
||||||
|
|
||||||
|
### Entfernte Komponenten
|
||||||
|
|
||||||
|
Die folgenden Dateien wurden entfernt (waren unsicher/redundant):
|
||||||
|
|
||||||
|
- ❌ `infrastructure/gateway/security/KeycloakJwtAuthenticationFilter.kt`
|
||||||
|
- **Grund**: Keine kryptographische Signaturvalidierung
|
||||||
|
- **Ersetzt durch**: Spring Security oauth2ResourceServer
|
||||||
|
|
||||||
|
- ❌ `infrastructure/gateway/filter/KeycloakJwtAuthenticationFilter.kt`
|
||||||
|
- **Grund**: Redundant zu oauth2ResourceServer
|
||||||
|
- **Ersetzt durch**: Spring Security oauth2ResourceServer
|
||||||
|
|
||||||
|
### Konfigurationsänderungen
|
||||||
|
|
||||||
|
**Alt**:
|
||||||
|
```yaml
|
||||||
|
gateway:
|
||||||
|
security:
|
||||||
|
keycloak:
|
||||||
|
enabled: true # Custom Filter
|
||||||
|
```
|
||||||
|
|
||||||
|
**Neu**:
|
||||||
|
```yaml
|
||||||
|
spring:
|
||||||
|
security:
|
||||||
|
oauth2:
|
||||||
|
resourceserver:
|
||||||
|
jwt:
|
||||||
|
issuer-uri: http://keycloak:8080/realms/meldestelle
|
||||||
|
|
||||||
|
gateway:
|
||||||
|
security:
|
||||||
|
keycloak:
|
||||||
|
enabled: false # Spring Security oauth2ResourceServer verwenden
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Sicherheit
|
||||||
|
|
||||||
|
1. ✅ **Verwenden Sie starke Admin-Passwörter**
|
||||||
|
2. ✅ **Aktivieren Sie HTTPS in Produktion**
|
||||||
|
3. ✅ **Regelmäßige Backups der Keycloak-Datenbank**
|
||||||
|
4. ✅ **Überwachen Sie Failed Login Attempts**
|
||||||
|
5. ✅ **Verwenden Sie Service Accounts für Backend-Services**
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
1. ✅ **Database Connection Pool anpassen** (KC_DB_POOL_MAX_SIZE)
|
||||||
|
2. ✅ **JVM Memory optimieren** (JAVA_OPTS_APPEND)
|
||||||
|
3. ✅ **Pre-built Image verwenden** (Dockerfile)
|
||||||
|
4. ✅ **Cache aktivieren** (KC_CACHE=ispn)
|
||||||
|
|
||||||
|
### Monitoring
|
||||||
|
|
||||||
|
1. ✅ **Metrics überwachen** (/metrics Endpoint)
|
||||||
|
2. ✅ **Health Checks konfigurieren**
|
||||||
|
3. ✅ **Event Logging aktivieren**
|
||||||
|
4. ✅ **Admin Events protokollieren**
|
||||||
|
|
||||||
|
## Weitere Ressourcen
|
||||||
|
|
||||||
|
- [Keycloak Official Documentation](https://www.keycloak.org/documentation)
|
||||||
|
- [Spring Security OAuth2 Resource Server](https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/index.html)
|
||||||
|
- [OpenID Connect Specification](https://openid.net/specs/openid-connect-core-1_0.html)
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
Bei Problemen oder Fragen:
|
||||||
|
|
||||||
|
1. Prüfen Sie die Logs
|
||||||
|
2. Konsultieren Sie dieses Dokument
|
||||||
|
3. Kontaktieren Sie das Development Team
|
||||||
|
|
@ -47,7 +47,7 @@ zipkinReporter = "3.5.1"
|
||||||
|
|
||||||
# --- Authentication ---
|
# --- Authentication ---
|
||||||
auth0Jwt = "4.5.0"
|
auth0Jwt = "4.5.0"
|
||||||
keycloak = "25.0.6"
|
keycloak = "26.4.0"
|
||||||
|
|
||||||
# --- Testing ---
|
# --- Testing ---
|
||||||
junitJupiter = "5.12.2"
|
junitJupiter = "5.12.2"
|
||||||
|
|
|
||||||
|
|
@ -1,196 +0,0 @@
|
||||||
package at.mocode.infrastructure.gateway.filter
|
|
||||||
|
|
||||||
import at.mocode.infrastructure.auth.client.JwtService
|
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import org.springframework.beans.factory.annotation.Value
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
|
||||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain
|
|
||||||
import org.springframework.cloud.gateway.filter.GlobalFilter
|
|
||||||
import org.springframework.core.Ordered
|
|
||||||
import org.springframework.http.HttpStatus
|
|
||||||
import org.springframework.http.server.reactive.ServerHttpResponse
|
|
||||||
import org.springframework.stereotype.Component
|
|
||||||
import org.springframework.util.AntPathMatcher
|
|
||||||
import org.springframework.web.server.ServerWebExchange
|
|
||||||
import reactor.core.publisher.Mono
|
|
||||||
import java.util.Base64
|
|
||||||
|
|
||||||
@Component
|
|
||||||
@ConditionalOnProperty(
|
|
||||||
value = ["gateway.security.keycloak.enabled"],
|
|
||||||
havingValue = "true",
|
|
||||||
matchIfMissing = false
|
|
||||||
)
|
|
||||||
class KeycloakJwtAuthenticationFilter(
|
|
||||||
private val jwtService: JwtService
|
|
||||||
) : GlobalFilter, Ordered {
|
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(KeycloakJwtAuthenticationFilter::class.java)
|
|
||||||
private val pathMatcher = AntPathMatcher()
|
|
||||||
private val objectMapper = jacksonObjectMapper()
|
|
||||||
|
|
||||||
@Value("\${keycloak.realm:meldestelle}")
|
|
||||||
private lateinit var realm: String
|
|
||||||
|
|
||||||
@Value("\${keycloak.issuer-uri:http://keycloak:8080/realms/meldestelle}")
|
|
||||||
private lateinit var issuerUri: String
|
|
||||||
|
|
||||||
// Öffentliche Pfade aus Konfiguration
|
|
||||||
@Value("\${gateway.security.public-paths:/,/health/**,/actuator/**,/api/ping/**,/api/auth/**,/fallback/**,/docs/**,/swagger-ui/**}")
|
|
||||||
private lateinit var publicPathsConfig: String
|
|
||||||
|
|
||||||
private val publicPaths by lazy {
|
|
||||||
publicPathsConfig.split(",").map { it.trim() }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun filter(exchange: ServerWebExchange, chain: GatewayFilterChain): Mono<Void> {
|
|
||||||
val request = exchange.request
|
|
||||||
val path = request.path.value()
|
|
||||||
|
|
||||||
logger.debug("Processing request for path: {}", path)
|
|
||||||
|
|
||||||
// Prüfe öffentliche Pfade
|
|
||||||
if (isPublicPath(path)) {
|
|
||||||
logger.debug("Path {} is public, allowing without authentication", path)
|
|
||||||
return chain.filter(exchange)
|
|
||||||
}
|
|
||||||
|
|
||||||
// JWT Token extrahieren
|
|
||||||
val authHeader = request.headers.getFirst("Authorization")
|
|
||||||
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
|
||||||
logger.warn("Missing or invalid Authorization header for path: {}", path)
|
|
||||||
return handleUnauthorized(exchange, "Missing or invalid Authorization header")
|
|
||||||
}
|
|
||||||
|
|
||||||
val token = authHeader.substring(7)
|
|
||||||
return validateToken(token, exchange, chain)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun validateToken(
|
|
||||||
token: String,
|
|
||||||
exchange: ServerWebExchange,
|
|
||||||
chain: GatewayFilterChain
|
|
||||||
): Mono<Void> {
|
|
||||||
return try {
|
|
||||||
// Verwende JwtService für Validierung
|
|
||||||
val validationResult = jwtService.validateToken(token)
|
|
||||||
if (validationResult.isFailure) {
|
|
||||||
logger.warn("JWT validation failed: {}", validationResult.exceptionOrNull()?.message)
|
|
||||||
return handleUnauthorized(exchange, "Invalid JWT token")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Claims extrahieren
|
|
||||||
val claims = parseJwtClaims(token)
|
|
||||||
|
|
||||||
// Issuer validieren
|
|
||||||
val issuer = claims["iss"]?.toString()
|
|
||||||
if (!issuer.equals(issuerUri)) {
|
|
||||||
logger.warn("Invalid issuer in token: {} (expected: {})", issuer, issuerUri)
|
|
||||||
return handleUnauthorized(exchange, "Invalid token issuer")
|
|
||||||
}
|
|
||||||
|
|
||||||
// User-Informationen extrahieren
|
|
||||||
val userId = claims["sub"]?.toString() ?: "unknown"
|
|
||||||
val username = claims["preferred_username"]?.toString()
|
|
||||||
?: claims["name"]?.toString()
|
|
||||||
?: "unknown"
|
|
||||||
val email = claims["email"]?.toString() ?: ""
|
|
||||||
val roles = extractRoles(claims)
|
|
||||||
val userRole = determineUserRole(roles)
|
|
||||||
|
|
||||||
logger.debug("Token validated for user: {} (ID: {}) with roles: {}", username, userId, roles)
|
|
||||||
|
|
||||||
// Request mit User-Context erweitern
|
|
||||||
val mutatedRequest = exchange.request.mutate()
|
|
||||||
.header("X-User-ID", userId)
|
|
||||||
.header("X-User-Name", username)
|
|
||||||
.header("X-User-Email", email)
|
|
||||||
.header("X-User-Role", userRole)
|
|
||||||
.header("X-User-Roles", roles.joinToString(","))
|
|
||||||
.header("X-Auth-Method", "keycloak-jwt")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val mutatedExchange = exchange.mutate()
|
|
||||||
.request(mutatedRequest)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
chain.filter(mutatedExchange)
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logger.error("JWT validation failed unexpectedly: {}", e.message, e)
|
|
||||||
handleUnauthorized(exchange, "JWT validation failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isPublicPath(path: String): Boolean {
|
|
||||||
return publicPaths.any { publicPath ->
|
|
||||||
pathMatcher.match(publicPath, path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseJwtClaims(token: String): Map<String, Any> {
|
|
||||||
val parts = token.split(".")
|
|
||||||
if (parts.size != 3) {
|
|
||||||
throw IllegalArgumentException("Invalid JWT format")
|
|
||||||
}
|
|
||||||
|
|
||||||
val payload = parts[1]
|
|
||||||
val decoded = Base64.getUrlDecoder().decode(payload)
|
|
||||||
return objectMapper.readValue(decoded, Map::class.java) as Map<String, Any>
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun extractRoles(claims: Map<String, Any>): List<String> {
|
|
||||||
return try {
|
|
||||||
// Keycloak realm roles
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val realmAccess = claims["realm_access"] as? Map<String, Any>
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val realmRoles = realmAccess?.get("roles") as? List<String> ?: emptyList()
|
|
||||||
|
|
||||||
// Keycloak resource access (client-specific roles)
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val resourceAccess = claims["resource_access"] as? Map<String, Any>
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val clientAccess = resourceAccess?.get("api-gateway") as? Map<String, Any>
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val clientRoles = clientAccess?.get("roles") as? List<String> ?: emptyList()
|
|
||||||
|
|
||||||
(realmRoles + clientRoles).distinct()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logger.warn("Could not extract roles from token: {}", e.message)
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun determineUserRole(roles: List<String>): String {
|
|
||||||
return when {
|
|
||||||
"ADMIN" in roles -> "ADMIN"
|
|
||||||
"USER" in roles -> "USER"
|
|
||||||
"MONITORING" in roles -> "MONITORING"
|
|
||||||
else -> "GUEST"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleUnauthorized(exchange: ServerWebExchange, message: String): Mono<Void> {
|
|
||||||
val response: ServerHttpResponse = exchange.response
|
|
||||||
response.statusCode = HttpStatus.UNAUTHORIZED
|
|
||||||
response.headers.add("Content-Type", "application/json")
|
|
||||||
response.headers.add("WWW-Authenticate", "Bearer realm=\"$realm\"")
|
|
||||||
|
|
||||||
val errorResponse = mapOf(
|
|
||||||
"error" to "UNAUTHORIZED",
|
|
||||||
"message" to message,
|
|
||||||
"timestamp" to java.time.Instant.now().toString(),
|
|
||||||
"status" to 401,
|
|
||||||
"realm" to realm,
|
|
||||||
"path" to exchange.request.path.value()
|
|
||||||
)
|
|
||||||
|
|
||||||
val errorJson = objectMapper.writeValueAsString(errorResponse)
|
|
||||||
val buffer = response.bufferFactory().wrap(errorJson.toByteArray())
|
|
||||||
return response.writeWith(Mono.just(buffer))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getOrder(): Int = Ordered.HIGHEST_PRECEDENCE + 3
|
|
||||||
}
|
|
||||||
|
|
@ -1,184 +0,0 @@
|
||||||
package at.mocode.infrastructure.gateway.security
|
|
||||||
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
|
||||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain
|
|
||||||
import org.springframework.cloud.gateway.filter.GlobalFilter
|
|
||||||
import org.springframework.core.Ordered
|
|
||||||
import org.springframework.http.HttpStatus
|
|
||||||
import org.springframework.http.server.reactive.ServerHttpResponse
|
|
||||||
import org.springframework.stereotype.Component
|
|
||||||
import org.springframework.util.AntPathMatcher
|
|
||||||
import org.springframework.web.reactive.function.client.WebClient
|
|
||||||
import org.springframework.web.server.ServerWebExchange
|
|
||||||
import reactor.core.publisher.Mono
|
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
|
||||||
import com.fasterxml.jackson.module.kotlin.readValue
|
|
||||||
import java.util.Base64
|
|
||||||
|
|
||||||
@Component
|
|
||||||
@ConditionalOnProperty(value = ["gateway.security.keycloak.enabled"], havingValue = "true", matchIfMissing = false)
|
|
||||||
class KeycloakJwtAuthenticationFilter(
|
|
||||||
private val webClient: WebClient.Builder
|
|
||||||
) : GlobalFilter, Ordered {
|
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(KeycloakJwtAuthenticationFilter::class.java)
|
|
||||||
private val pathMatcher = AntPathMatcher()
|
|
||||||
private val objectMapper = jacksonObjectMapper()
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val KEYCLOAK_SERVER_URL = "http://keycloak:8080"
|
|
||||||
private const val REALM = "meldestelle"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Öffentliche Pfade, die keine Authentifizierung erfordern
|
|
||||||
private val publicPaths = listOf(
|
|
||||||
"/",
|
|
||||||
"/health",
|
|
||||||
"/actuator/**",
|
|
||||||
"/api/ping/**", // Ping-Service für Monitoring
|
|
||||||
"/api/auth/login",
|
|
||||||
"/api/auth/register",
|
|
||||||
"/api/auth/refresh",
|
|
||||||
"/fallback/**",
|
|
||||||
"/docs/**",
|
|
||||||
"/swagger-ui/**"
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun filter(exchange: ServerWebExchange, chain: GatewayFilterChain): Mono<Void> {
|
|
||||||
val request = exchange.request
|
|
||||||
val path = request.path.value()
|
|
||||||
|
|
||||||
logger.debug("Processing request for path: {}", path)
|
|
||||||
|
|
||||||
// Prüfe, ob der Pfad öffentlich zugänglich ist
|
|
||||||
if (isPublicPath(path)) {
|
|
||||||
logger.debug("Path {} is public, allowing without authentication", path)
|
|
||||||
return chain.filter(exchange)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extrahiere JWT aus Authorization Header
|
|
||||||
val authHeader = request.headers.getFirst("Authorization")
|
|
||||||
|
|
||||||
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
|
||||||
logger.warn("Missing or invalid Authorization header for path: {}", path)
|
|
||||||
return handleUnauthorized(exchange, "Missing or invalid Authorization header")
|
|
||||||
}
|
|
||||||
|
|
||||||
val token = authHeader.substring(7)
|
|
||||||
|
|
||||||
// Validiere JWT-Token mit Keycloak
|
|
||||||
return validateKeycloakToken(token, exchange, chain)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isPublicPath(path: String): Boolean {
|
|
||||||
return publicPaths.any { publicPath ->
|
|
||||||
pathMatcher.match(publicPath, path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun validateKeycloakToken(
|
|
||||||
token: String,
|
|
||||||
exchange: ServerWebExchange,
|
|
||||||
chain: GatewayFilterChain
|
|
||||||
): Mono<Void> {
|
|
||||||
|
|
||||||
return try {
|
|
||||||
// JWT-Token-Struktur validieren
|
|
||||||
if (!isValidJwtFormat(token)) {
|
|
||||||
return handleUnauthorized(exchange, "Invalid JWT token format")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Claims aus Token extrahieren
|
|
||||||
val claims = parseJwtClaims(token)
|
|
||||||
val issuer = claims["iss"]?.toString()
|
|
||||||
val realm = issuer?.substringAfterLast("/")
|
|
||||||
|
|
||||||
if (realm != REALM) {
|
|
||||||
return handleUnauthorized(exchange, "Invalid realm in token")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Benutzerinformationen extrahieren
|
|
||||||
val userId = claims["sub"]?.toString() ?: "unknown"
|
|
||||||
val username = claims["preferred_username"]?.toString() ?: "unknown"
|
|
||||||
val roles = extractRoles(claims)
|
|
||||||
val userRole = determineUserRole(roles)
|
|
||||||
|
|
||||||
logger.debug("Token validated for user: {} with roles: {}", username, roles)
|
|
||||||
|
|
||||||
// Request mit Benutzerinformationen erweitern
|
|
||||||
val mutatedRequest = exchange.request.mutate()
|
|
||||||
.header("X-User-ID", userId)
|
|
||||||
.header("X-User-Name", username)
|
|
||||||
.header("X-User-Role", userRole)
|
|
||||||
.header("X-User-Roles", roles.joinToString(","))
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val mutatedExchange = exchange.mutate()
|
|
||||||
.request(mutatedRequest)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
chain.filter(mutatedExchange)
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logger.error("JWT validation failed: {}", e.message, e)
|
|
||||||
handleUnauthorized(exchange, "JWT validation failed: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isValidJwtFormat(token: String): Boolean {
|
|
||||||
val parts = token.split(".")
|
|
||||||
return parts.size == 3 && parts.all { it.isNotEmpty() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseJwtClaims(token: String): Map<String, Any> {
|
|
||||||
val parts = token.split(".")
|
|
||||||
val payload = parts[1]
|
|
||||||
|
|
||||||
// Base64 URL decode
|
|
||||||
val decoded = Base64.getUrlDecoder().decode(payload)
|
|
||||||
return objectMapper.readValue<Map<String, Any>>(decoded)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun extractRoles(claims: Map<String, Any>): List<String> {
|
|
||||||
return try {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val realmAccess = claims["realm_access"] as? Map<String, Any>
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val roles = realmAccess?.get("roles") as? List<String>
|
|
||||||
roles ?: emptyList()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logger.warn("Could not extract roles from token: {}", e.message)
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun determineUserRole(roles: List<String>): String {
|
|
||||||
return when {
|
|
||||||
"ADMIN" in roles -> "ADMIN"
|
|
||||||
"USER" in roles -> "USER"
|
|
||||||
"MONITORING" in roles -> "MONITORING"
|
|
||||||
else -> "GUEST"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleUnauthorized(exchange: ServerWebExchange, message: String): Mono<Void> {
|
|
||||||
val response: ServerHttpResponse = exchange.response
|
|
||||||
response.statusCode = HttpStatus.UNAUTHORIZED
|
|
||||||
response.headers.add("Content-Type", "application/json")
|
|
||||||
response.headers.add("WWW-Authenticate", "Bearer realm=\"$REALM\"")
|
|
||||||
|
|
||||||
val errorJson = """{
|
|
||||||
"error": "UNAUTHORIZED",
|
|
||||||
"message": "$message",
|
|
||||||
"timestamp": "${java.time.LocalDateTime.now()}",
|
|
||||||
"status": 401,
|
|
||||||
"realm": "$REALM"
|
|
||||||
}"""
|
|
||||||
|
|
||||||
val buffer = response.bufferFactory().wrap(errorJson.toByteArray())
|
|
||||||
return response.writeWith(Mono.just(buffer))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getOrder(): Int = Ordered.HIGHEST_PRECEDENCE + 3
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package at.mocode.infrastructure.gateway.security
|
package at.mocode.infrastructure.gateway.security
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Value
|
import org.springframework.beans.factory.annotation.Value
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||||
import org.springframework.context.annotation.Bean
|
import org.springframework.context.annotation.Bean
|
||||||
|
|
@ -64,15 +65,11 @@ import java.time.Duration
|
||||||
@EnableWebFluxSecurity
|
@EnableWebFluxSecurity
|
||||||
@EnableConfigurationProperties(GatewaySecurityProperties::class)
|
@EnableConfigurationProperties(GatewaySecurityProperties::class)
|
||||||
class SecurityConfig(
|
class SecurityConfig(
|
||||||
private val securityProperties: GatewaySecurityProperties
|
private val securityProperties: GatewaySecurityProperties,
|
||||||
|
@Value("\${keycloak.issuer-uri:}") private val issuerUri: String,
|
||||||
|
@Value("\${keycloak.jwk-set-uri:}") private val jwkSetUri: String
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@Value("\${keycloak.issuer-uri:http://keycloak:8080/realms/meldestelle}")
|
|
||||||
private lateinit var issuerUri: String
|
|
||||||
|
|
||||||
@Value("\${keycloak.jwk-set-uri:http://keycloak:8080/realms/meldestelle/protocol/openid-connect/certs}")
|
|
||||||
private lateinit var jwkSetUri: String
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hauptkonfiguration der Spring-Security-Filterkette.
|
* Hauptkonfiguration der Spring-Security-Filterkette.
|
||||||
*
|
*
|
||||||
|
|
@ -86,7 +83,7 @@ class SecurityConfig(
|
||||||
*/
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||||
return http
|
val httpSecurity = http
|
||||||
.cors { it.configurationSource(corsConfigurationSource()) }
|
.cors { it.configurationSource(corsConfigurationSource()) }
|
||||||
.csrf { it.disable() }
|
.csrf { it.disable() }
|
||||||
.authorizeExchange { exchanges ->
|
.authorizeExchange { exchanges ->
|
||||||
|
|
@ -107,22 +104,38 @@ class SecurityConfig(
|
||||||
"/test/**", // Test paths for integration tests
|
"/test/**", // Test paths for integration tests
|
||||||
"/mock/**" // Mock controller paths for tests
|
"/mock/**" // Mock controller paths for tests
|
||||||
).permitAll()
|
).permitAll()
|
||||||
|
.apply {
|
||||||
|
// Only enforce role-based authorization when oauth2ResourceServer is active
|
||||||
|
// In tests without oauth2, JwtAuthenticationFilter handles auth via GlobalFilter
|
||||||
|
if (jwkSetUri.isNotBlank()) {
|
||||||
// Admin paths
|
// Admin paths
|
||||||
.pathMatchers("/api/admin/**").hasRole("ADMIN")
|
pathMatchers("/api/admin/**").hasRole("ADMIN")
|
||||||
// Monitoring paths
|
// Monitoring paths
|
||||||
.pathMatchers("/api/monitoring/**").hasAnyRole("ADMIN", "MONITORING")
|
pathMatchers("/api/monitoring/**").hasAnyRole("ADMIN", "MONITORING")
|
||||||
// All other requests require authentication
|
// All other requests require authentication
|
||||||
.anyExchange().authenticated()
|
anyExchange().authenticated()
|
||||||
|
} else {
|
||||||
|
// Permissive mode for tests - JwtAuthenticationFilter handles auth
|
||||||
|
anyExchange().permitAll()
|
||||||
}
|
}
|
||||||
.oauth2ResourceServer { oauth2 ->
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only configure oauth2ResourceServer if Keycloak JWK URI is configured
|
||||||
|
// In tests, this will be empty, allowing JwtAuthenticationFilter to handle auth
|
||||||
|
if (jwkSetUri.isNotBlank()) {
|
||||||
|
httpSecurity.oauth2ResourceServer { oauth2 ->
|
||||||
oauth2.jwt { jwt ->
|
oauth2.jwt { jwt ->
|
||||||
jwt.jwtDecoder(jwtDecoder())
|
jwt.jwtDecoder(jwtDecoder())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.build()
|
}
|
||||||
|
|
||||||
|
return httpSecurity.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
@ConditionalOnProperty(name = ["keycloak.jwk-set-uri"])
|
||||||
fun jwtDecoder(): ReactiveJwtDecoder {
|
fun jwtDecoder(): ReactiveJwtDecoder {
|
||||||
return NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri)
|
return NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri)
|
||||||
.build()
|
.build()
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,29 @@
|
||||||
|
# ===================================================================
|
||||||
|
# Keycloak Profile Configuration
|
||||||
|
# ===================================================================
|
||||||
|
# This profile configures OAuth2/JWT authentication with Keycloak.
|
||||||
|
# Uses Spring Security's oauth2ResourceServer for secure JWT validation.
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
security:
|
security:
|
||||||
oauth2:
|
oauth2:
|
||||||
resourceserver:
|
resourceserver:
|
||||||
jwt:
|
jwt:
|
||||||
issuer-uri: ${KEYCLOAK_ISSUER_URI:http://localhost:8180/realms/meldestelle}
|
# Issuer URI for JWT validation - Docker internal: keycloak:8080, External: localhost:8180
|
||||||
jwk-set-uri: ${KEYCLOAK_JWK_SET_URI:http://localhost:8180/realms/meldestelle/protocol/openid-connect/certs}
|
issuer-uri: ${KEYCLOAK_ISSUER_URI:http://keycloak:8080/realms/meldestelle}
|
||||||
|
# JWK Set URI for fetching public keys to validate JWT signatures
|
||||||
|
jwk-set-uri: ${KEYCLOAK_JWK_SET_URI:http://keycloak:8080/realms/meldestelle/protocol/openid-connect/certs}
|
||||||
|
|
||||||
# Keycloak-spezifische Konfiguration
|
# Keycloak-spezifische Konfiguration
|
||||||
keycloak:
|
keycloak:
|
||||||
server-url: ${KEYCLOAK_SERVER_URL:http://localhost:8180}
|
# Internal Docker service name, external via port 8180
|
||||||
|
server-url: ${KEYCLOAK_SERVER_URL:http://keycloak:8080}
|
||||||
|
issuer-uri: ${KEYCLOAK_ISSUER_URI:http://keycloak:8080/realms/meldestelle}
|
||||||
|
jwk-set-uri: ${KEYCLOAK_JWK_SET_URI:http://keycloak:8080/realms/meldestelle/protocol/openid-connect/certs}
|
||||||
realm: ${KEYCLOAK_REALM:meldestelle}
|
realm: ${KEYCLOAK_REALM:meldestelle}
|
||||||
resource: ${KEYCLOAK_CLIENT_ID:api-gateway}
|
resource: ${KEYCLOAK_CLIENT_ID:api-gateway}
|
||||||
|
client-id: ${KEYCLOAK_CLIENT_ID:api-gateway}
|
||||||
public-client: false
|
public-client: false
|
||||||
bearer-only: true
|
bearer-only: true
|
||||||
|
|
||||||
|
|
@ -18,19 +31,11 @@ keycloak:
|
||||||
gateway:
|
gateway:
|
||||||
security:
|
security:
|
||||||
jwt:
|
jwt:
|
||||||
|
# Enable JWT validation via Spring Security OAuth2 Resource Server
|
||||||
enabled: true
|
enabled: true
|
||||||
keycloak:
|
keycloak:
|
||||||
enabled: true
|
# Custom JWT filter DISABLED - using Spring Security oauth2ResourceServer instead
|
||||||
server-url: ${KEYCLOAK_SERVER_URL:http://localhost:8180}
|
# This prevents duplicate authentication and ensures proper JWT signature validation
|
||||||
|
enabled: false
|
||||||
|
server-url: ${KEYCLOAK_SERVER_URL:http://keycloak:8080}
|
||||||
realm: ${KEYCLOAK_REALM:meldestelle}
|
realm: ${KEYCLOAK_REALM:meldestelle}
|
||||||
public-paths:
|
|
||||||
- "/"
|
|
||||||
- "/health"
|
|
||||||
- "/actuator/**"
|
|
||||||
- "/api/ping/**" # Ping-Service öffentlich zugänglich
|
|
||||||
- "/api/auth/login"
|
|
||||||
- "/api/auth/register"
|
|
||||||
- "/api/auth/refresh"
|
|
||||||
- "/fallback/**"
|
|
||||||
- "/docs/**"
|
|
||||||
- "/swagger-ui/**"
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,11 @@ spring:
|
||||||
name: api-gateway-test
|
name: api-gateway-test
|
||||||
main:
|
main:
|
||||||
web-application-type: reactive
|
web-application-type: reactive
|
||||||
|
autoconfigure:
|
||||||
|
exclude:
|
||||||
|
# Disable OAuth2 ResourceServer auto-configuration in tests
|
||||||
|
# Tests use mock JwtAuthenticationFilter instead of real JWT validation
|
||||||
|
- org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.ReactiveOAuth2ResourceServerAutoConfiguration
|
||||||
cloud:
|
cloud:
|
||||||
discovery:
|
discovery:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
@ -63,8 +68,3 @@ logging:
|
||||||
level:
|
level:
|
||||||
org.springframework.cloud.gateway: WARN
|
org.springframework.cloud.gateway: WARN
|
||||||
at.mocode.infrastructure.gateway: DEBUG
|
at.mocode.infrastructure.gateway: DEBUG
|
||||||
|
|
||||||
gateway:
|
|
||||||
security:
|
|
||||||
jwt:
|
|
||||||
enabled: false
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user