main_java

Identity and Access Management (IAM) mit Keycloak und Open Liberty

Umfassender Leitfaden fΓΌr Authentifizierung und Autorisierung


πŸ“‹ Inhaltsverzeichnis

  1. Überblick
  2. Architektur
  3. Keycloak Server-Konfiguration
  4. Open Liberty Server-Konfiguration
  5. Frontend/Client-Konfiguration
  6. Schritt-fΓΌr-Schritt Anleitung
  7. Troubleshooting
  8. Best Practices

Überblick

Was ist IAM?

Identity and Access Management (IAM) umfasst:

Technologie-Stack

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    JavaFX Frontend                      β”‚
β”‚         (de.ruu.app.jeeeraaah.frontend.ui.fx)           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                     β”‚ HTTP + JWT Bearer Token
                     β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              Open Liberty Server (Backend)              β”‚
β”‚         (de.ruu.app.jeeeraaah.backend.api.ws.rs)        β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”‚
β”‚   β”‚  MicroProfile JWT (mpJwt-2.1)                 β”‚     β”‚
β”‚   β”‚  - JWT Token Validierung                      β”‚     β”‚
β”‚   β”‚  - Signatur-PrΓΌfung via JWKS                  β”‚     β”‚
β”‚   β”‚  - Rollen-Extraktion aus "groups" Claim       β”‚     β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                     β”‚ JWKS (Public Keys)
                     β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  Keycloak Server                        β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”‚
β”‚   β”‚  Realm: jeeeraaah-realm                       β”‚     β”‚
β”‚   β”‚  - User Management                            β”‚     β”‚
β”‚   β”‚  - Roles & Groups                             β”‚     β”‚
β”‚   β”‚  - OAuth2 / OpenID Connect                    β”‚     β”‚
β”‚   β”‚  - JWT Token Issuer                           β”‚     β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

OAuth2 / OpenID Connect Flow

Frontend                Keycloak                 Backend
   β”‚                        β”‚                        β”‚
   β”‚ 1. Login Request       β”‚                        β”‚
   β”‚ ──────────────────────>β”‚                        β”‚
   β”‚   (username/password)  β”‚                        β”‚
   β”‚                        β”‚                        β”‚
   β”‚ 2. JWT Access Token    β”‚                        β”‚
   β”‚ <──────────────────────│                        β”‚
   β”‚                        β”‚                        β”‚
   β”‚ 3. API Request         β”‚                        β”‚
   β”‚    + Bearer Token      β”‚                        β”‚
   β”‚ ──────────────────────────────────────────────> β”‚
   β”‚                        β”‚                        β”‚
   β”‚                        β”‚ 4. Token Validation    β”‚
   β”‚                        β”‚ <──────────────────────│
   β”‚                        β”‚    (JWKS Public Keys)  β”‚
   β”‚                        β”‚                        β”‚
   β”‚                        β”‚ 5. Signature OK        β”‚
   β”‚                        β”‚ ──────────────────────>β”‚
   β”‚                        β”‚                        β”‚
   β”‚                        β”‚ 6. Extract Roles       β”‚
   β”‚                        β”‚    from "groups" claim β”‚
   β”‚                        β”‚                        β”‚
   β”‚ 7. API Response        β”‚                        β”‚
   β”‚ <───────────────────────────────────────────────│
   β”‚                        β”‚                        β”‚

Architektur

Komponenten-Übersicht

1. Keycloak Server (Port 8080)

2. Open Liberty Server (Port 9080)

3. JavaFX Frontend


Keycloak Server-Konfiguration

1. Realm erstellen

Ein Realm ist eine isolierte DomΓ€ne fΓΌr Benutzer und Anwendungen.

// Automatische Realm-Erstellung via KeycloakRealmSetup.java
RealmRepresentation realm = new RealmRepresentation();
realm.setRealm("jeeeraaah-realm");
realm.setEnabled(true);
realm.setDisplayName("Jeeeraaah Task Management");
realm.setLoginTheme("keycloak");

keycloak.realms().create(realm);

Manuelle Erstellung in der Admin Console:

  1. Login: http://localhost:8080/admin (admin/admin)
  2. Klicke oben links auf β€œmaster” β†’ β€œCreate Realm”
  3. Realm Name: jeeeraaah-realm
  4. Enabled: βœ…
  5. Klicke β€œCreate”

2. Client konfigurieren

Ein Client reprΓ€sentiert die Frontend-Anwendung.

Client Settings

// Automatische Client-Erstellung
ClientRepresentation client = new ClientRepresentation();
client.setClientId("jeeeraaah-frontend");
client.setName("Jeeeraaah Frontend");
client.setEnabled(true);
client.setPublicClient(true);  // Kein Client Secret (Public Client)
client.setDirectAccessGrantsEnabled(true);  // Resource Owner Password Flow
client.setStandardFlowEnabled(true);  // Authorization Code Flow
client.setServiceAccountsEnabled(false);

// Redirect URIs
client.setRedirectUris(Arrays.asList(
    "http://localhost:*",
    "https://localhost:*"
));

// Web Origins (CORS)
client.setWebOrigins(Arrays.asList("*"));

Manuelle Konfiguration:

  1. WΓ€hle Realm: jeeeraaah-realm
  2. Klicke β€œClients” β†’ β€œCreate client”
  3. General Settings:
    • Client type: OpenID Connect
    • Client ID: jeeeraaah-frontend
  4. Capability config:
    • Client authentication: Off (Public Client)
    • Authorization: Off
    • Authentication flow:
      • βœ… Standard flow
      • βœ… Direct access grants
      • ❌ Implicit flow
      • ❌ Service accounts roles
  5. Login settings:
    • Valid redirect URIs: http://localhost:*
    • Valid post logout redirect URIs: http://localhost:*
    • Web origins: *

3. Protocol Mappers konfigurieren

Protocol Mappers fΓΌgen zusΓ€tzliche Claims (Daten) zum JWT Token hinzu.

3.1 Groups Claim Mapper (WICHTIG!)

Open Liberty erwartet Rollen im Top-Level β€œgroups” Claim, nicht verschachtelt in realm_access.roles.

// Automatische Mapper-Erstellung
ProtocolMapperRepresentation mapper = new ProtocolMapperRepresentation();
mapper.setName("groups-claim-mapper");
mapper.setProtocol("openid-connect");
mapper.setProtocolMapper("oidc-usermodel-realm-role-mapper");

Map<String, String> config = new HashMap<>();
config.put("claim.name", "groups");           // Liberty erwartet "groups"
config.put("jsonType.label", "String");
config.put("multivalued", "true");            // Array von Rollen
config.put("id.token.claim", "true");         // In ID Token
config.put("access.token.claim", "true");     // In Access Token
config.put("userinfo.token.claim", "true");   // In UserInfo Endpoint

mapper.setConfig(config);

Manuelle Konfiguration:

  1. Client: jeeeraaah-frontend β†’ β€œClient scopes” β†’ β€œjeeeraaah-frontend-dedicated”
  2. β€œAdd mapper” β†’ β€œBy configuration”
  3. WΓ€hle: User Realm Role
  4. Konfiguration:
    • Name: groups-claim-mapper
    • Token Claim Name: groups
    • Claim JSON Type: String
    • Multivalued: βœ…
    • Add to ID token: βœ…
    • Add to access token: βœ…
    • Add to userinfo: βœ…

Vorher (Keycloak Standard):

{
  "realm_access": {
    "roles": ["admin", "user"]
  }
}

Nachher (Liberty-kompatibel):

{
  "groups": ["admin", "user"]
}

3.2 Audience Mapper (Optional)

FΓΌgt die Audience jeeeraaah-backend zum Token hinzu.

ProtocolMapperRepresentation audienceMapper = new ProtocolMapperRepresentation();
audienceMapper.setName("audience-mapper");
audienceMapper.setProtocol("openid-connect");
audienceMapper.setProtocolMapper("oidc-audience-mapper");

Map<String, String> config = new HashMap<>();
config.put("included.custom.audience", "jeeeraaah-backend");
config.put("access.token.claim", "true");
config.put("id.token.claim", "false");

audienceMapper.setConfig(config);

Manuelle Konfiguration:

  1. Client: jeeeraaah-frontend β†’ β€œClient scopes” β†’ β€œjeeeraaah-frontend-dedicated”
  2. β€œAdd mapper” β†’ β€œBy configuration” β†’ β€œAudience”
  3. Konfiguration:
    • Name: audience-mapper
    • Included Custom Audience: jeeeraaah-backend
    • Add to access token: βœ…
    • Add to ID token: ❌

4. Rollen erstellen

Realm Roles definieren die Berechtigungen.

// Automatisch
RoleRepresentation adminRole = new RoleRepresentation();
adminRole.setName("admin");
adminRole.setDescription("Administrator role with full access");

RoleRepresentation userRole = new RoleRepresentation();
userRole.setName("user");
userRole.setDescription("Regular user role with limited access");

keycloak.realm("jeeeraaah-realm").roles().create(adminRole);
keycloak.realm("jeeeraaah-realm").roles().create(userRole);

Manuelle Erstellung:

  1. Realm: jeeeraaah-realm β†’ β€œRealm roles”
  2. β€œCreate role”
  3. Admin Role:
    • Role name: admin
    • Description: Administrator role with full access
  4. User Role:
    • Role name: user
    • Description: Regular user role with limited access

5. Benutzer erstellen

// Automatisch
UserRepresentation user = new UserRepresentation();
user.setUsername("testuser");
user.setEmail("testuser@example.com");
user.setFirstName("Test");
user.setLastName("User");
user.setEnabled(true);
user.setEmailVerified(true);

// Benutzer erstellen
Response response = keycloak.realm("jeeeraaah-realm")
    .users().create(user);
String userId = CreatedResponseUtil.getCreatedId(response);

// Passwort setzen
CredentialRepresentation credential = new CredentialRepresentation();
credential.setType(CredentialRepresentation.PASSWORD);
credential.setValue("password123");
credential.setTemporary(false);

keycloak.realm("jeeeraaah-realm")
    .users().get(userId)
    .resetPassword(credential);

// Rollen zuweisen
RoleRepresentation adminRole = keycloak.realm("jeeeraaah-realm")
    .roles().get("admin").toRepresentation();

keycloak.realm("jeeeraaah-realm")
    .users().get(userId)
    .roles().realmLevel()
    .add(Arrays.asList(adminRole));

Manuelle Erstellung:

  1. Realm: jeeeraaah-realm β†’ β€œUsers”
  2. β€œCreate new user”
  3. User Details:
    • Username: testuser
    • Email: testuser@example.com
    • First name: Test
    • Last name: User
    • Email verified: βœ…
    • Enabled: βœ…
  4. Klicke β€œCreate”
  5. Credentials Tab:
    • Set password: password123
    • Password temporary: ❌
  6. Role mapping Tab:
    • β€œAssign role” β†’ WΓ€hle admin und/oder user

6. Keycloak Setup automatisieren

Das Projekt enthΓ€lt eine automatische Setup-Klasse:

# Docker-Container laufen lassen
docker-compose up -d

# Setup ausfΓΌhren (im Projekt)
cd root/lib/keycloak_admin
mvn exec:java -Dexec.mainClass="de.ruu.lib.keycloak.admin.setup.KeycloakRealmSetup"

Was macht KeycloakRealmSetup.java?

  1. βœ… Realm jeeeraaah-realm erstellen (falls nicht vorhanden)
  2. βœ… Client jeeeraaah-frontend konfigurieren
  3. βœ… Groups Claim Mapper hinzufΓΌgen
  4. βœ… Audience Mapper hinzufΓΌgen
  5. βœ… Rollen admin und user erstellen
  6. βœ… Testbenutzer erstellen und Rollen zuweisen

Ausgabe:

βœ… Realm 'jeeeraaah-realm' ist bereit
βœ… Client 'jeeeraaah-frontend' konfiguriert
βœ… 'groups' Claim Mapper erfolgreich erstellt
   β†’ Rollen werden nun als Top-Level 'groups' Claim ins Token geschrieben
   β†’ Liberty Server kann Rollen jetzt lesen!
βœ… Audience Mapper erstellt
βœ… Rolle 'admin' erstellt
βœ… Rolle 'user' erstellt
βœ… Benutzer 'testuser' erstellt
βœ… Rolle 'admin' zu Benutzer 'testuser' zugewiesen

Open Liberty Server-Konfiguration

1. Feature Manager

In server.xml mΓΌssen folgende Features aktiviert sein:

<featureManager>
    <!-- Jakarta EE 10.0 + MicroProfile 6.1 -->
    <feature>jakartaee-10.0</feature>
    <feature>microProfile-6.1</feature>
    
    <!-- MicroProfile JWT fΓΌr Token-Validierung -->
    <feature>mpJwt-2.1</feature>
    
    <!-- Application Security fΓΌr @RolesAllowed -->
    <feature>appSecurity-5.0</feature>
</featureManager>

2. MicroProfile JWT Konfiguration

<mpJwt id="jwtConsumer"
    jwksUri="http://localhost:8080/realms/jeeeraaah-realm/protocol/openid-connect/certs"
    issuer="http://localhost:8080/realms/jeeeraaah-realm"
    userNameAttribute="preferred_username"
    groupNameAttribute="groups">
    
    <!-- OPTIONAL: Audience Validierung (fΓΌr Production) -->
    <!-- audiences="jeeeraaah-backend" -->
</mpJwt>

ErklΓ€rung der Attribute:

Attribut Beschreibung Beispiel
jwksUri URL zu Keycloaks Public Keys (JWKS = JSON Web Key Set)
Liberty lΓ€dt diese Keys und prΓΌft die Token-Signatur
http://localhost:8080/realms/jeeeraaah-realm/protocol/openid-connect/certs
issuer Erwarteter Token-Aussteller (Issuer Claim iss)
Muss exakt mit Keycloak Realm URL ΓΌbereinstimmen
http://localhost:8080/realms/jeeeraaah-realm
userNameAttribute JWT Claim fΓΌr den Benutzernamen
Liberty extrahiert diesen Wert als Principal
preferred_username
groupNameAttribute JWT Claim fΓΌr Rollen/Gruppen
⚠️ WICHTIG: Muss auf groups gesetzt sein!
groups
audiences Erwartete Audience (Optional)
FΓΌr Production-Sicherheit aktivieren
jeeeraaah-backend

Wichtige URLs:

URL Beschreibung
http://localhost:8080/realms/jeeeraaah-realm/.well-known/openid-configuration OpenID Connect Discovery (Metadaten)
http://localhost:8080/realms/jeeeraaah-realm/protocol/openid-connect/certs JWKS Public Keys (Token-Signatur-Validierung)
http://localhost:8080/realms/jeeeraaah-realm/protocol/openid-connect/token Token Endpoint (Login)

3. REST Endpoint absichern

import jakarta.annotation.security.RolesAllowed;
import jakarta.annotation.security.PermitAll;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.SecurityContext;

@Path("/taskgroup")
public class TaskGroupResource
{
    /**
     * Γ–ffentlicher Endpoint - kein Login erforderlich
     */
    @GET
    @Path("/public")
    @Produces(MediaType.APPLICATION_JSON)
    @PermitAll
    public Response getPublicData()
    {
        return Response.ok("Public data").build();
    }
    
    /**
     * GeschΓΌtzter Endpoint - nur fΓΌr angemeldete Benutzer
     * (unabhΓ€ngig von der Rolle)
     */
    @GET
    @Path("/protected")
    @Produces(MediaType.APPLICATION_JSON)
    @RolesAllowed({"user", "admin"})
    public Response getProtectedData(@Context SecurityContext securityContext)
    {
        String username = securityContext.getUserPrincipal().getName();
        return Response.ok("Hello " + username).build();
    }
    
    /**
     * Admin-Endpoint - nur fΓΌr Admin-Rolle
     */
    @GET
    @Path("/admin")
    @Produces(MediaType.APPLICATION_JSON)
    @RolesAllowed("admin")
    public Response getAdminData(@Context SecurityContext securityContext)
    {
        String username = securityContext.getUserPrincipal().getName();
        boolean isAdmin = securityContext.isUserInRole("admin");
        
        return Response.ok("Admin access granted for " + username).build();
    }
}

Annotations Übersicht:

Annotation Beschreibung
@PermitAll Jeder darf zugreifen (kein Token erforderlich)
@DenyAll Niemand darf zugreifen
@RolesAllowed("admin") Nur Benutzer mit Rolle admin
@RolesAllowed({"user", "admin"}) Benutzer mit Rolle user ODER admin

4. SecurityContext nutzen

@Context
private SecurityContext securityContext;

public void someMethod()
{
    // Benutzername abrufen
    String username = securityContext.getUserPrincipal().getName();
    
    // Rolle prΓΌfen
    boolean isAdmin = securityContext.isUserInRole("admin");
    boolean isUser = securityContext.isUserInRole("user");
    
    // JWT Token im Request Header verfΓΌgbar via:
    // @HeaderParam("Authorization") String authHeader
}

Frontend/Client-Konfiguration

1. Login-Flow im JavaFX Frontend

@Singleton
@Slf4j
public class KeycloakAuthService
{
    private String keycloakServerUrl;
    private String realm;
    private String clientId;
    private String tokenUrl;
    
    @Getter private String accessToken;
    @Getter private String refreshToken;
    
    @PostConstruct
    private void init()
    {
        // Konfiguration aus microprofile-config.properties
        keycloakServerUrl = ConfigProvider.getConfig()
            .getOptionalValue("keycloak.auth.server.url", String.class)
            .orElse("http://localhost:8080");
        
        realm = ConfigProvider.getConfig()
            .getOptionalValue("keycloak.realm", String.class)
            .orElse("jeeeraaah-realm");
        
        clientId = ConfigProvider.getConfig()
            .getOptionalValue("keycloak.client-id", String.class)
            .orElse("jeeeraaah-frontend");
        
        tokenUrl = String.format("%s/realms/%s/protocol/openid-connect/token",
            keycloakServerUrl, realm);
    }
    
    /**
     * Benutzer-Login mit Resource Owner Password Grant
     */
    public String login(String username, String password) 
        throws IOException, InterruptedException
    {
        String formData = String.format(
            "grant_type=password&client_id=%s&username=%s&password=%s",
            URLEncoder.encode(clientId, StandardCharsets.UTF_8),
            URLEncoder.encode(username, StandardCharsets.UTF_8),
            URLEncoder.encode(password, StandardCharsets.UTF_8)
        );
        
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(tokenUrl))
            .header("Content-Type", "application/x-www-form-urlencoded")
            .POST(HttpRequest.BodyPublishers.ofString(formData))
            .build();
        
        HttpClient client = HttpClient.newHttpClient();
        HttpResponse<String> response = client.send(request, 
            HttpResponse.BodyHandlers.ofString());
        
        if (response.statusCode() == 200)
        {
            JsonNode json = new ObjectMapper().readTree(response.body());
            accessToken = json.get("access_token").asText();
            refreshToken = json.get("refresh_token").asText();
            
            log.info("Login erfolgreich fΓΌr Benutzer: {}", username);
            return accessToken;
        }
        else
        {
            log.error("Login fehlgeschlagen: {}", response.body());
            throw new IOException("Login failed: " + response.statusCode());
        }
    }
}

2. Authentifizierte API-Aufrufe

@ApplicationScoped
public class TaskGroupServiceClient
{
    @Inject
    private KeycloakAuthService authService;
    
    private final Client client = ClientBuilder.newClient();
    
    public TaskGroupDTO getTaskGroup(Long id)
    {
        String token = authService.getAccessToken();
        
        Response response = client
            .target("http://localhost:9080/taskgroup/{id}")
            .resolveTemplate("id", id)
            .request(MediaType.APPLICATION_JSON)
            .header("Authorization", "Bearer " + token)  // ← JWT Token!
            .get();
        
        if (response.getStatus() == 200)
        {
            return response.readEntity(TaskGroupDTO.class);
        }
        else if (response.getStatus() == 401)
        {
            throw new RuntimeException("Unauthorized - Token ungΓΌltig");
        }
        else if (response.getStatus() == 403)
        {
            throw new RuntimeException("Forbidden - Keine Berechtigung");
        }
        else
        {
            throw new RuntimeException("API Error: " + response.getStatus());
        }
    }
}

3. Authorization Header Filter (Global)

Automatisch den JWT Token zu allen Requests hinzufΓΌgen:

@Provider
public class AuthorizationHeaderFilter implements ClientRequestFilter
{
    @Inject
    private KeycloakAuthService authService;
    
    @Override
    public void filter(ClientRequestContext requestContext)
    {
        String token = authService.getAccessToken();
        
        if (token != null && !token.isEmpty())
        {
            requestContext.getHeaders()
                .putSingle("Authorization", "Bearer " + token);
        }
    }
}

// Filter registrieren
Client client = ClientBuilder.newClient()
    .register(AuthorizationHeaderFilter.class);

Schritt-fΓΌr-Schritt Anleitung

Setup von Grund auf

Schritt 1: Docker-Container starten

cd /home/r-uu/develop/github/main
docker-compose up -d

# Status prΓΌfen
docker ps | grep -E "keycloak|postgres"

Erwartete Ausgabe:

keycloak   Up 2 minutes (healthy)
postgres   Up 2 minutes (healthy)

Schritt 2: Keycloak automatisch konfigurieren

cd root/lib/keycloak_admin
mvn exec:java -Dexec.mainClass="de.ruu.lib.keycloak.admin.setup.KeycloakRealmSetup"

Erwartete Ausgabe:

βœ… Realm 'jeeeraaah-realm' ist bereit
βœ… Client 'jeeeraaah-frontend' konfiguriert
βœ… 'groups' Claim Mapper erfolgreich erstellt
βœ… Audience Mapper erstellt
βœ… Rollen erstellt: admin, user
βœ… Testbenutzer 'testuser' erstellt

Schritt 3: Keycloak Admin Console ΓΆffnen (Optional - zur Verifikation)

URL: http://localhost:8080/admin

Login:

PrΓΌfen:

  1. Realm: jeeeraaah-realm existiert
  2. Client: jeeeraaah-frontend existiert
  3. Users: testuser existiert mit Rolle admin
  4. Client Scopes: jeeeraaah-frontend-dedicated β†’ Mappers:
    • βœ… groups-claim-mapper
    • βœ… audience-mapper

Schritt 4: Open Liberty Server starten

cd root/app/jeeeraaah/backend/api/ws_rs

# Mit Maven
mvn liberty:dev

# Oder direkt
mvn liberty:run

Erwartete Ausgabe:

[AUDIT   ] CWWKZ0001I: Application jeeeraaah started in 5.123 seconds.
[AUDIT   ] CWWKF0012I: The server installed the following features: 
           [jakartaee-10.0, mpJwt-2.1, appSecurity-5.0, ...]

Server lΓ€uft auf: http://localhost:9080


Schritt 5: JavaFX Frontend starten

cd root/app/jeeeraaah/frontend/ui/fx

# App starten
mvn javafx:run

# Oder den AppRunner
java -jar target/r-uu.app.jeeeraaah.frontend.ui.fx-0.0.1.jar

Schritt 6: Login testen

  1. Frontend ΓΆffnet sich
  2. Login-Dialog erscheint
  3. Eingeben:
    • Username: testuser
    • Password: password123
  4. Klicke β€œLogin”

Erwartetes Ergebnis:

βœ… Login erfolgreich
βœ… Access Token erhalten
βœ… Dashboard wird angezeigt

Schritt 7: API-Aufruf testen (manuell)

# 1. Token abrufen
TOKEN=$(curl -s -X POST \
  "http://localhost:8080/realms/jeeeraaah-realm/protocol/openid-connect/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=password" \
  -d "client_id=jeeeraaah-frontend" \
  -d "username=testuser" \
  -d "password=password123" \
  | jq -r '.access_token')

# 2. API-Aufruf mit Token
curl -X GET \
  "http://localhost:9080/taskgroup/allFlat" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Accept: application/json"

Erwartete Antwort:

[
  {
    "id": 1,
    "name": "Feature Set 1",
    "description": "Backend Development"
  }
]

Troubleshooting

Problem 1: β€œUnauthorized 401” bei API-Aufrufen

Symptom:

HTTP 401 Unauthorized

MΓΆgliche Ursachen:

1.1 Token fehlt oder ist ungΓΌltig

# Token dekodieren (mit jwt.io oder jq)
echo $TOKEN | cut -d'.' -f2 | base64 -d | jq

PrΓΌfen:

LΓΆsung:

# Neuen Token abrufen
TOKEN=$(curl -s -X POST ...)

1.2 Liberty kann JWKS nicht erreichen

Liberty Logs prΓΌfen:

tail -f wlp/usr/servers/defaultServer/logs/messages.log

Fehler:

CWWKS6031E: The JSON Web Token (JWT) consumer cannot contact the URL 
[http://localhost:8080/realms/jeeeraaah-realm/protocol/openid-connect/certs]

LΓΆsung:

1.3 Issuer stimmt nicht ΓΌberein

Liberty Logs:

CWWKS6022E: The issuer [http://localhost:8080/realms/jeeeraaah-realm] 
specified in the token does not match the issuer attribute

LΓΆsung: server.xml prΓΌfen:

<mpJwt issuer="http://localhost:8080/realms/jeeeraaah-realm" />

Problem 2: β€œForbidden 403” bei API-Aufrufen

Symptom:

HTTP 403 Forbidden

Ursache: Token ist gΓΌltig, aber Benutzer hat nicht die erforderliche Rolle.

Diagnose:

# Token dekodieren
echo $TOKEN | cut -d'.' -f2 | base64 -d | jq '.groups'

Erwartete Ausgabe:

["admin", "user"]

Wenn groups fehlt:

β†’ Groups Claim Mapper fehlt!

LΓΆsung:

  1. Keycloak Admin Console ΓΆffnen
  2. Client: jeeeraaah-frontend β†’ Client scopes β†’ jeeeraaah-frontend-dedicated
  3. Mappers prΓΌfen: groups-claim-mapper muss existieren
  4. Falls nicht: KeycloakRealmSetup erneut ausfΓΌhren

Problem 3: β€œgroups” Claim fehlt im Token

Symptom:

{
  "realm_access": {
    "roles": ["admin"]
  }
}

Aber Liberty erwartet:

{
  "groups": ["admin"]
}

Ursache: Groups Claim Mapper fehlt oder falsch konfiguriert.

LΓΆsung:

// KeycloakRealmSetup erneut ausfΓΌhren
mvn exec:java -Dexec.mainClass="de.ruu.lib.keycloak.admin.setup.KeycloakRealmSetup"

Oder manuell:

  1. Keycloak Admin Console
  2. Client: jeeeraaah-frontend β†’ Client scopes β†’ jeeeraaah-frontend-dedicated
  3. β€œAdd mapper” β†’ β€œBy configuration” β†’ β€œUser Realm Role”
  4. Konfiguration:
    • Name: groups-claim-mapper
    • Token Claim Name: groups ← WICHTIG!
    • Claim JSON Type: String
    • Multivalued: βœ…
    • Add to access token: βœ…

Problem 4: Keycloak-Container startet nicht

Symptom:

docker ps | grep keycloak
# Kein Output oder Status: unhealthy

Diagnose:

docker logs keycloak

MΓΆgliche Ursachen:

4.1 PostgreSQL nicht bereit

Fehler:

Caused by: org.postgresql.util.PSQLException: Connection refused

LΓΆsung:

# PostgreSQL-Status prΓΌfen
docker ps | grep postgres

# Falls nicht healthy: Neustarten
docker-compose restart postgres

# Warten bis healthy, dann Keycloak starten
docker-compose up -d keycloak

4.2 Port 8080 bereits belegt

Fehler:

Error starting userland proxy: listen tcp4 0.0.0.0:8080: bind: address already in use

LΓΆsung:

# Port 8080 freimachen oder in docker-compose.yml Γ€ndern
lsof -i :8080
kill <PID>

Problem 5: Liberty findet keine Rollen

Symptom:

Diagnose:

Liberty server.xml prΓΌfen:

<mpJwt groupNameAttribute="groups" />

NICHT:

<mpJwt groupNameAttribute="realm_access.roles" />

Token prΓΌfen:

echo $TOKEN | cut -d'.' -f2 | base64 -d | jq '.groups'

Erwartete Ausgabe:

["admin", "user"]

Wenn null: β†’ Groups Claim Mapper fehlt (siehe Problem 3)


Best Practices

Security

1. Production: Audience validieren

Development:

<mpJwt id="jwtConsumer"
    jwksUri="..."
    issuer="..."
    userNameAttribute="preferred_username"
    groupNameAttribute="groups">
    <!-- audiences deaktiviert -->
</mpJwt>

Production:

<mpJwt id="jwtConsumer"
    jwksUri="..."
    issuer="..."
    audiences="jeeeraaah-backend"  ← Aktivieren!
    userNameAttribute="preferred_username"
    groupNameAttribute="groups" />

2. HTTPS verwenden

Production:

<!-- Keycloak -->
https://auth.example.com/realms/jeeeraaah-realm/...

<!-- Liberty -->
<httpEndpoint httpsPort="9443" httpPort="-1" />

3. Client Secret fΓΌr vertrauliche Clients

FΓΌr Backend-zu-Backend Kommunikation:

// Keycloak Client-Konfiguration
client.setPublicClient(false);  // Vertraulicher Client
client.setClientAuthenticatorType("client-secret");

// Token-Anfrage mit Client Secret
curl -X POST "http://localhost:8080/realms/jeeeraaah-realm/protocol/openid-connect/token" \
  -d "grant_type=client_credentials" \
  -d "client_id=jeeeraaah-backend-service" \
  -d "client_secret=YOUR_SECRET_HERE"

4. Token Expiration konfigurieren

Keycloak Admin Console:

  1. Realm: jeeeraaah-realm β†’ Realm settings β†’ Tokens
  2. Access Token Lifespan: 5 Minuten (Standard)
  3. Refresh Token Lifespan: 30 Minuten
  4. SSO Session Max: 10 Stunden

5. Refresh Tokens nutzen

public String refreshAccessToken() throws IOException, InterruptedException
{
    String formData = String.format(
        "grant_type=refresh_token&client_id=%s&refresh_token=%s",
        URLEncoder.encode(clientId, StandardCharsets.UTF_8),
        URLEncoder.encode(refreshToken, StandardCharsets.UTF_8)
    );
    
    HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create(tokenUrl))
        .header("Content-Type", "application/x-www-form-urlencoded")
        .POST(HttpRequest.BodyPublishers.ofString(formData))
        .build();
    
    HttpClient client = HttpClient.newHttpClient();
    HttpResponse<String> response = client.send(request, 
        HttpResponse.BodyHandlers.ofString());
    
    if (response.statusCode() == 200)
    {
        JsonNode json = new ObjectMapper().readTree(response.body());
        accessToken = json.get("access_token").asText();
        refreshToken = json.get("refresh_token").asText();
        return accessToken;
    }
    else
    {
        throw new IOException("Token refresh failed");
    }
}

Performance

1. JWKS Caching

Liberty cached die Public Keys automatisch. Konfigurierbar via:

<mpJwt jwksCacheLifetimeMs="600000" />  <!-- 10 Minuten -->

2. Connection Pooling

// Jersey Client mit Connection Pooling
ClientConfig config = new ClientConfig();
config.property(ClientProperties.CONNECT_TIMEOUT, 5000);
config.property(ClientProperties.READ_TIMEOUT, 30000);

Client client = ClientBuilder.newClient(config);

Monitoring

1. Liberty Logs ΓΌberwachen

tail -f wlp/usr/servers/defaultServer/logs/messages.log | grep -E "CWWKS|JWT"

2. Keycloak Events aktivieren

Admin Console:

  1. Realm: jeeeraaah-realm β†’ Events
  2. User events settings:
    • Save events: βœ…
    • Event listeners: jboss-logging
  3. Login events settings:
    • Save login events: βœ…

Events anzeigen:


Zusammenfassung

Wichtigste Konfigurationen

Komponente Konfiguration Wert
Keycloak Realm jeeeraaah-realm
Β  Client ID jeeeraaah-frontend
Β  Client Type Public Client
Β  Groups Claim Mapper Claim Name: groups ← KRITISCH!
Β  Token Endpoint http://localhost:8080/realms/jeeeraaah-realm/protocol/openid-connect/token
Liberty Feature mpJwt-2.1, appSecurity-5.0
Β  JWKS URI http://localhost:8080/realms/jeeeraaah-realm/protocol/openid-connect/certs
Β  Issuer http://localhost:8080/realms/jeeeraaah-realm
Β  groupNameAttribute groups ← KRITISCH!
Frontend Auth Service KeycloakAuthService.java
Β  Token Header Authorization: Bearer <token>

Checkliste fΓΌr Setup


Erstellt: 2026-02-16
Autor: GitHub Copilot
Version: 1.0
Status: βœ… Produktionsreif