Spring Boot & Keycloak: Sichere Authentifizierung und flexible Verwaltung domänen-spezifischer Daten

Spring Boot & Keycloak: Sichere Authentifizierung und flexible Verwaltung domänen-spezifischer Daten

1. Einleitung

Moderne Anwendungen müssen weit mehr als nur die grundlegende Authentifizierung und Autorisierung von Benutzern gewährleisten. Neben diesen Sicherheitsaspekten spielt auch das effiziente Management domänenspezifischer Daten eine entscheidende Rolle. Typische Anwendungsfälle sind:

  • E-Commerce:
    Eine Webanwendung, die nicht nur den Zugriff auf ein Kundenkonto absichert, sondern auch Bestellhistorien, individuelle Rabatte oder Wunschlisten verwalten muss. Während Keycloak hier die Authentifizierung übernimmt, werden alle kundenbezogenen Daten (z. B. frühere Bestellungen, Rabattsätze oder personalisierte Produktempfehlungen) in einer separaten Datenbank verwaltet.
  • Gesundheitswesen:
    In Gesundheits-Apps sind nicht nur Authentifizierungs- und Berechtigungsmechanismen erforderlich, sondern auch die Verwaltung von Patientenprofilen, medizinischen Berichten und Terminplänen. Während Keycloak als zentraler Identity Provider fungiert, sorgt eine dedizierte Applikationsdatenbank für die sichere und flexible Speicherung und Verarbeitung sensibler medizinischer Daten.
  • Analytics-Plattformen:
    Anwendungen, die individuelle Dashboards und Berichte anbieten, benötigen häufig umfangreiche Benutzerdaten, um personalisierte Analysen zu ermöglichen. Hier übernimmt Keycloak das Rollen- und Berechtigungsmanagement, während detaillierte Benutzerstatistiken und Metriken in einer separaten Datenbank gepflegt werden, um komplexe Abfragen und Analysen zu unterstützen.

Ein weiterer zentraler Aspekt moderner Authentifizierungsmechanismen ist die Unterscheidung zwischen ID Token und Access Token im Rahmen von OAuth2/OpenID Connect. Das ID Token enthält primär Basisinformationen zur Identität des Benutzers (z. B. Name, E-Mail), während der Access Token zur Autorisierung beim Zugriff auf geschützte Ressourcen verwendet wird. In unserem Ansatz wird das ID Token schlank gehalten – ausschliesslich mit Basis-Identitätsinformationen –, während der Access Token durch einen Token-Enrichment-Prozess um zusätzliche, domänenspezifische Daten (wie Rabatte oder Bestellhistorien) erweitert wird.

Dabei steht dieser Ansatz in Konkurrenz zu anderen Möglichkeiten, wie etwa:

  • Verwendung von Keycloak-Attributen, bei denen alle zusätzlichen Daten direkt in Keycloak gespeichert und in das ID Token integriert werden.
  • Direkte Datenbankabfragen, bei denen das jeweilige Token nur minimale Informationen enthält und die Applikationslogik bei jedem Request direkt auf die Datenbank zugreift.
  • Hybrid-Ansätze, die eine Kombination aus Token-Enrichment und direkten DB-Abfragen zur Optimierung von Performance und Aktualität bieten.

In diesem Blog-Beitrag konzentrieren wir uns auf den oben genannten Ansatz mit einem Token-Enrichment-Prozess, bei welchem Keycloak ausschliesslich als Identity Provider eingesetzt wird und alle zusätzlichen, geschäftsrelevanten Daten in einer separaten Applikationsdatenbank verwaltet werden. Wir erläutern, wie sich beide Systeme optimal kombinieren lassen, um Sicherheit, Flexibilität und Performance zu gewährleisten.

2. Architektur-Übersicht

Bevor wir in die konkreten Code-Beispiele und Implementierungsdetails eintauchen, werfen wir einen ganzheitlichen Blick auf die Architektur. Eine klare Übersicht über die einzelnen Komponenten und deren Zusammenspiel hilft, den Ablauf von der Authentifizierung bis zur Datenanreicherung besser zu verstehen.

In unserer Architektur übernimmt Keycloak ausschließlich die Authentifizierung und das Rollenmanagement. Alle domänenspezifischen Daten (z. B. Bestellungen, Rabatte) werden in einer separaten Datenbank verwaltet. Der entscheidende Schritt ist der Token-Enrichment-Prozess: Das von Keycloak generierte Token wird um zusätzliche Daten aus der Applikationsdatenbank erweitert – und zwar im Access Token. Damit bleibt das ID Token schlank und ausschließlich für die Identitätsverifizierung zuständig.

Komponenten im Überblick

KomponenteAufgaben
Keycloak* Authentifizierung der Benutzer
* Generierung von OAuth2/JWT-Tokens
* Rollen- & Gruppenmanagement
Anwendungs-Datenbank* Verwaltung von Benutzerprofilen
* Speicherung von Bestellungen, Rabatten und App-spezifischen Metriken

Sequence Flow

Das folgende Sequenzdiagramm veranschaulicht den Ablauf der Authentifizierung sowie den anschliessenden Token-Enrichment-Prozess:

Ablauf der Authentifizierung und Token-Enrichment-Phasen

Ablauf:

  1. Access & Redirect:
    Der Benutzer fordert den geschützten Endpunkt /orders an. Da er noch nicht authentifiziert ist, leitet die Spring Boot App ihn zur Keycloak-Login-Seite weiter.
  2. Authentication at Keycloak:
    Der Benutzer gibt seine Anmeldedaten ein. Nach erfolgreicher Authentifizierung erfolgt ein Redirect zurück zur Anwendung mit einem Authorization Code.
  3. Token Request:
    Die Anwendung tauscht den Authorization Code (und das Secret) bei Keycloak gegen ein JWT aus, das mindestens den sub-Claim (die Keycloak-ID) enthält.
  4. Data Enrichment:
    Mithilfe des sub-Claims wird in der Applikationsdatenbank nach zusätzlichen Benutzerdaten (z. B. Rabatte, Bestellhistorien) gesucht. Anschliessend wird das ursprüngliche Access Token um diese Daten erweitert.
  5. Usage:
    Das angereicherte Token wird in der Anwendung ausgewertet, sodass dem Benutzer alle relevanten Informationen angezeigt werden.

3. Implementierung

Im Folgenden zeigen wir, wie Sie den oben beschriebenen Ansatz in einer Spring Boot-Anwendung umsetzen können.

Schritt 1: Integration von Keycloak in Spring Boot

Die Integration von Keycloak erfolgt über die Konfiguration von Spring Security. Dabei definieren wir, welche Endpunkte welchen Rollen vorbehalten sind und wie das JWT in Spring Security umgewandelt wird.

/**
 * Security configuration class that sets up the security filter chain and configures
 * the OAuth2 resource server for JWT-based authentication.
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    /**
     * Configures the HTTP security filter chain.
     *
     * <p>This method sets up the following security rules:
     * <ul>
     *   <li>Requests to endpoints under "/orders/**" require the role "CUSTOMER".</li>
     *   <li>All other requests require authentication.</li>
     * </ul>
     * Additionally, it configures the OAuth2 resource server to use JWT tokens for authentication,
     * integrating a custom JWT converter to extract roles from the token.
     *
     * @param http the {@link HttpSecurity} to be configured
     * @return the built {@link SecurityFilterChain} instance
     * @throws Exception if an error occurs during configuration
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                // Endpoints under "/orders/**" explicitly require the CUSTOMER role
                .requestMatchers("/orders/**").hasRole("CUSTOMER")
                // All other requests must be authenticated
                .anyRequest().authenticated()
            )
            // Configures the OAuth2 resource server with JWT support
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    // Integrates a custom JWT converter to extract roles from the token
                    .jwtAuthenticationConverter(jwtAuthConverter())
                )
            );
        return http.build();
    }

    /**
     * Creates a custom JWT authentication converter.
     *
     * <p>This converter transforms a {@link Jwt} into a Spring Security {@link AbstractAuthenticationToken},
     * extracting roles from the token (for example, from {@code realm_access.roles}) and converting
     * them into authorities recognized by Spring Security.
     *
     * @return a {@link Converter} that converts {@link Jwt} tokens into {@link AbstractAuthenticationToken} objects
     */
    private Converter<Jwt, AbstractAuthenticationToken> jwtAuthConverter() {
        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
        // Sets a custom granted authorities converter to map JWT roles to Spring Security authorities
        converter.setJwtGrantedAuthoritiesConverter(new KeycloakRoleConverter());
        return converter;
    }
}

Code: SecurityConfig.java

Hinweis:

  • KeycloakRoleConverter: Diese Hilfsklasse extrahiert die Rollen aus dem JWT. Stellen Sie sicher, dass in Keycloak die korrekten Rollen und Mappings konfiguriert sind.
  • Rollen-basierte Zugriffskontrolle: Endpunkte wie /orders/** werden explizit nur für Benutzer mit der Rolle CUSTOMER zugänglich gemacht.

Schritt 2: Verknüpfung von Benutzer-Daten via Keycloak-ID

Um zusätzliche Informationen zu einem Benutzer zu speichern, definieren wir in unserer Datenbank ein Schema, in dem die Keycloak-ID als Primärschlüssel dient.

Bitte beachten Sie, dass das Beispiel auf ein Minimum beschränkt ist, um zu zeigen, wie zusätzliche anwendungsspezifische Daten (wie Rabatte oder Bestell-Zeitstempel) mit dem Benutzer über seine Keycloak-ID verknüpft werden können.

CREATE TABLE app_user (
    keycloak_id VARCHAR(255) PRIMARY KEY,  ## corresponds to the 'sub' claim from the JWT
    discount DECIMAL(5,2) DEFAULT 0.00,
    last_order_date TIMESTAMP
);

Datenbank-Schema

Anschliessend erstellen wir die Java-Entität AppUser, die das Schema der Tabelle app_user abbildet.

/**
 * Represents an application user with additional information linked via Keycloak ID.
 *
 * <p>This entity maps to the {@code app_user} table in the database and holds extra data
 * such as a discount value and the timestamp of the last order. The {@code keycloakId} field,
 * which corresponds to the 'sub' claim from the JWT, is used as the primary key.</p>
 */
@Entity
public class AppUser {

    
     // The Keycloak ID of the user, which corresponds to the 'sub' claim from the JWT.
    @Id
    @Column(name = "keycloak_id", nullable = false, length = 255)
    private String keycloakId;
   
    // The discount assigned to the user.
    @Column(name = "discount", precision = 5, scale = 2)
    private BigDecimal discount = BigDecimal.valueOf(0.00);

     // The timestamp of the user's last order.
    @Column(name = "last_order_date")
    private LocalDateTime lastOrderDate;

  // The constructor, setters and getters have been omitted for the sake of clarity.
  
    }

Entity: AppUser.java

Im letzten Schritt implementieren wir das Repository AppUserRepository, das neben den Standard-CRUD-Operationen auch eine benutzerdefinierte Abfrage bereitstellt, um einen Benutzer anhand seiner Keycloak-ID zu finden. Dieses Repository stellt die Verbindung zwischen dem Keycloak-Sicherheitskontext und den zusätzlichen Benutzerdaten in unserer Anwendung her.

**
 * Repository interface for performing CRUD operations on {@link AppUser} entities.
 */
public interface AppUserRepository extends JpaRepository<AppUser, String> {

    /**
     * Finds an {@link AppUser} by its Keycloak ID.
     *
     * @param keycloakId the Keycloak ID of the user
     * @return an {@link Optional} containing the found user, or empty if no user is found
     */
    @Query("SELECT u FROM AppUser u WHERE u.keycloakId = :keycloakId")
    Optional<AppUser> findByKeycloakId(@Param("keycloakId") String keycloakId);
}

Repository: AppUserRepository.java

Ergänzende Hinweise:

  • Nutzen Sie Transaktionen und geeignete Fehlerbehandlung, um Datenkonsistenz zwischen Keycloak und Ihrer Applikationsdatenbank sicherzustellen.
  • Durch die Verwendung der Keycloak-ID als Fremdschlüssel wird eine eindeutige Verknüpfung gewährleistet.

Schritt 3: Token-Enrichment – Anreicherung des JWT mit zusätzlichen App-Daten

Um häufige Datenbankzugriffe zu vermeiden und dem Client mehr Kontext zu bieten, können Sie das JWT mit nicht-sensitiven, zusätzlichen Informationen anreichern. Beispielhaft fügen wir hier den Rabattwert des Benutzers hinzu.

**
 * Custom JWT converter that enriches the original JWT token with additional claims from the application database.
 * <p>
 * This converter extracts the Keycloak ID (subject) from the JWT, retrieves the corresponding {@link AppUser}
 * from the database using {@link AppUserRepository}, and adds custom claims (such as the user's discount) to the token.
 * </p>
 */
@Component
public class CustomJwtConverter implements Converter<Jwt, Jwt> {

    private final AppUserRepository userRepository;

    /**
     * Constructor for injecting the {@link AppUserRepository}.
     * <p>
     * Using constructor injection enhances testability and ensures that the required dependencies are provided.
     * </p>
     *
     * @param userRepository the repository used to fetch additional user data
     */
    public CustomJwtConverter(AppUserRepository userRepository) {
        this.userRepository = userRepository;
    }

    /**
     * Converts the given JWT by enriching it with additional claims from the application database.
     * <p>
     * This method performs the following steps:
     * <ol>
     *   <li>Extracts the Keycloak ID (subject) from the original JWT.</li>
     *   <li>Retrieves the {@link AppUser} corresponding to the Keycloak ID from the database.
     *       If no user is found, a {@link UsernameNotFoundException} is thrown.</li>
     *   <li>Creates a copy of the original JWT claims and adds custom claims, such as the user's discount.</li>
     *   <li>Returns a new JWT token that includes the enriched claims.</li>
     * </ol>
     * </p>
     *
     * @param source the original JWT token
     * @return a new JWT token enriched with additional claims
     * @throws UsernameNotFoundException if the user with the given Keycloak ID is not found
     */
    @Override
    public Jwt convert(Jwt source) {
        // Extract the Keycloak ID from the JWT (this corresponds to the 'sub' claim)
        String keycloakId = source.getSubject();

        // Retrieve additional user data from the application database using the Keycloak ID
        AppUser user = userRepository.findByKeycloakId(keycloakId)
            .orElseThrow(() -> new UsernameNotFoundException("User not found: " + keycloakId));

        // Create a copy of the original claims and add custom claims
        Map<String, Object> enrichedClaims = new HashMap<>(source.getClaims());
        enrichedClaims.put("discount", user.getDiscount());

        // Return a new Jwt instance with the enriched claims
        return new Jwt(
            source.getTokenValue(),
            source.getIssuedAt(),
            source.getExpiresAt(),
            source.getHeaders(),
            enrichedClaims
        );
    }
}

Code: CustomJwtConverter.java

Der Token-Enrichment-Prozess stellt sicher, dass alle notwendigen Daten – sowohl aus Keycloak als auch aus der Applikationsdatenbank – im JWT zur Verfügung stehen. Dadurch wird bei jedem weiteren Request keine zusätzliche Datenbankabfrage benötigt.

Wichtige Aspekte:

  • Sichtbarkeit der Claims: Achten Sie darauf, ausschliesslich nicht-sensitive Informationen in das Token aufzunehmen, da diese vom Client einsehbar sind.
  • Integration: Binden Sie den CustomJwtConverter in Ihren JWT-Konverter (in SecurityConfig) ein, um das Token während der Authentifizierung anzureichern.

4. Vergleich: Keycloak-Attribute vs. Separate Applikationsdatenbank vs. Direkte DB-Abfrage

Bei der Verwaltung zusätzlicher Benutzerdaten gibt es grundsätzlich drei Ansätze:

  1. Keycloak-Attribute:
    Zusätzliche Daten werden direkt in Keycloak als Benutzerattribute gespeichert und in das Token (häufig das ID Token) integriert.
  2. Separate Applikationsdatenbank mit Token Enrichment:
    Keycloak übernimmt ausschließlich die Authentifizierung und Autorisierung, während alle zusätzlichen, domänenspezifischen Daten in einer separaten Datenbank verwaltet und – über einen Token-Enrichment-Prozess – dem Access Token hinzugefügt werden.
  3. Direkte DB-Abfrage ohne Token Enrichment:
    Das Token enthält lediglich minimale Informationen (z. B. den sub-Claim) und die Applikationslogik fragt bei Bedarf direkt die Datenbank ab, um zusätzliche Informationen wie Rabatte oder Bestellhistorien zu erhalten.

Die folgende Tabelle fasst die wesentlichen Unterschiede zusammen:

KriteriumKeycloak-AttributeSeparate Applikationsdatenbank mit Token EnrichmentDirekte DB-Abfrage
Datenkomplexität- Eignet sich für einfache Datentypen (Strings, Zahlen, einfache Listen)- Ermöglicht die Speicherung komplexer, verschachtelter Daten (z. B. JSONB, strukturierte Objekte)- Keine zusätzliche Komplexität im Token; Datenmodell bleibt in der Datenbank
Sicherheit- Alle Daten werden im Token übertragen (z. B. im ID Token), was bei umfangreichen oder sensiblen Informationen ein Risiko darstellen kann- Sensible Daten verbleiben in der Datenbank; nur bei Bedarf werden nicht-sensible Daten dem Access Token hinzugefügt- Das Token bleibt schlank; direkte Abfragen ermöglichen gezielte Sicherheitskontrollen
Performance- Token-Grösse wächst mit zusätzlichen Attributen, was zu höherem Overhead führen kann- Access Token bleibt schlank, da es nur essentielle Informationen enthält; zusätzliche Daten werden einmalig abgerufen und im Token gespeichert- Zusätzliche Daten werden bei jedem Request abgefragt, was zu erhöhter Latenz führen kann, aber über Caching optimiert werden kann
Flexibilität und Änderbarkeit- Änderungen erfordern oft Anpassungen in Keycloak und ein erneutes Ausstellen der Tokens
- Eingeschränkte Modellierungsmöglichkeiten
- Datenmodell kann unabhängig von der Authentifizierung flexibel erweitert und angepasst werden
- Neue Felder oder Beziehungen können ohne grossen administrativen Aufwand implementiert werden
- Volle Flexibilität in der Datenbank; jedoch höhere Abhängigkeit von DB-Performance und Netzwerklatenz
Token-Inhalt (ID Token vs. Access Token)- ID Token enthält Basis-Identitätsinformationen (Name, E-Mail etc.)
- Nicht für umfangreiche, domänenspezifische Daten gedacht
- Access Token wird gezielt um zusätzliche Domänendaten (z. B. Rabatte) erweitert, ohne das ID Token unnötig aufzublähen- Token enthält nur minimale Informationen (z. B. sub); alle zusätzlichen Daten werden per Request aus der DB geholt

Zusammenfassung:

  • Keycloak-Attribute sind sinnvoll, wenn es sich um einfache, selten geänderte Daten handelt, die primär zur Verifizierung der Benutzeridentität benötigt werden.
  • Separate Applikationsdatenbank mit Token Enrichment bietet höhere Flexibilität, Sicherheit und Performance – insbesondere bei komplexen und dynamischen Domänendaten.
  • Direkte DB-Abfrage ermöglicht es, das Token möglichst schlank zu halten und zusätzliche Informationen bei Bedarf direkt aus der Datenbank abzurufen, was insbesondere bei hoher Aktualität vorteilhaft sein kann – erfordert jedoch optimierte Caching-Strategien.

5. Welche Attribute gehören in das ID Token und welche in den Access Token?

Eine oft gestellte Frage in OAuth2/OpenID Connect-Architekturen ist, welche Attribute idealerweise in das ID Token und welche in den Access Token aufgenommen werden sollten. Hier eine detaillierte Betrachtung:

ID Token – Minimalistische Identitätsinformationen

Das ID Token dient primär der Verifizierung der Benutzeridentität und wird vom OAuth2-Client genutzt, um die Authentifizierung zu bestätigen. Es sollte nur die unbedingt notwendigen Informationen enthalten:

  • Standard Claims:
    • iss: Der Aussteller (Issuer) des Tokens.
    • sub: Die eindeutige Benutzer-ID.
    • aud: Die Zielgruppe (Audience), also der Client, für den das Token bestimmt ist.
    • iat: Zeitpunkt der Ausstellung (Issued At).
    • exp: Ablaufzeit (Expiration).
    • nbf: (optional) Not Before – Zeitpunkt, ab dem das Token gültig ist.
  • Basisinformationen:
    • Name, Vorname, E-Mail-Adresse (z. B. given_namefamily_nameemail).
  • Prinzip der minimalen Offenlegung:
    • Sensible oder umfangreiche domänenspezifische Daten (wie Rabatte oder Bestellhistorien) sollten nicht in das ID Token aufgenommen werden, da dieses oft an den Client zurückgegeben und gespeichert wird.

Access Token – Erweiterte Autorisierungs- und Geschäftslogik-Daten

Der Access Token wird an den Ressourcen-Server übergeben, um den Zugriff auf geschützte APIs zu autorisieren. Neben den Standard Claims kann er auch zusätzliche Informationen enthalten, die für die Geschäftslogik relevant sind:

  • Standard Claims des Access Tokens:
    • iss: Der Aussteller.
    • sub: Die Benutzer-ID.
    • aud: Die Zielgruppe, oft eine oder mehrere API-Endpunkte.
    • exp: Ablaufzeit des Tokens.
    • iat: Zeitpunkt der Ausstellung.
    • nbf: (optional) Zeitpunkt, ab dem das Token gültig ist.
    • scope: Die genehmigten Zugriffsbereiche (Scopes).
  • Erweiterte Claims:
    • Rollen und Berechtigungen, die den Zugriff auf bestimmte Ressourcen regeln.
    • Domänenspezifische Daten wie Rabatte, Bestellhistorien oder andere relevante Geschäftsinformationen (via Token-Enrichment).

Durch diesen Ansatz wird sichergestellt, dass das ID Token schlank bleibt und ausschliesslich für die Authentifizierung genutzt wird, während der Access Token alle für die Autorisierung und Geschäftslogik notwendigen, erweiterten Informationen enthält.

Zusammengefasst:

  • Das ID Token sollte ausschliesslich grundlegende Identitätsinformationen enthalten, die für die Verifizierung der Authentifizierung erforderlich sind.
  • Der Access Token wird für API-Aufrufe verwendet und kann – über den Token-Enrichment-Prozess – um zusätzliche, für die Autorisierung relevante Daten ergänzt werden.

6. Diskussion und Bewertung der Lösung

Bewertung des vorgestellten Ansatzes

Der hier vorgestellte Ansatz, bei dem Keycloak ausschliesslich als Identity Provider eingesetzt wird und alle zusätzlichen, geschäftsrelevanten Daten in einer separaten Applikationsdatenbank verwaltet werden, hat sich in der Praxis vielfach bewährt:

  • Klare Verantwortlichkeit:
    Durch die Trennung von Authentifizierungs- und Domänendaten können beide Systeme unabhängig voneinander skaliert, gewartet und erweitert werden. Keycloak konzentriert sich auf die sichere Authentifizierung und das Rollenmanagement, während die Applikationsdatenbank flexibel an sich ändernde Geschäftsanforderungen angepasst werden kann.
  • Leistungsoptimierung:
    Das schlanke ID Token enthält ausschließlich Basis-Identitätsinformationen, während der Access Token über einen Token-Enrichment-Prozess um relevante Domänendaten erweitert wird. Dies reduziert unnötige Datenbankabfragen bei jedem Request und verbessert die Performance.
  • Sicherheitsaspekte:
    Sensible und umfangreiche Domänendaten verbleiben in der Applikationsdatenbank und werden nur bei Bedarf in den Access Token integriert. Dies minimiert das Risiko, dass zu viele Informationen in einem Token offengelegt werden.

Alternative Ansätze

Neben dem oben beschriebenen Ansatz gibt es zwei weitere Alternativen:

  1. Verwendung von Keycloak-Attributen:
    • Vorteile:
      • Alle Benutzerinformationen sind zentral in Keycloak verfügbar.
      • Direkte Integration in das ID Token ermöglicht eine einfache Handhabung.
    • Nachteile:
      • Bei komplexen oder häufig ändernden Domänendaten wächst das Token kontinuierlich, was zu Performance- und Sicherheitsproblemen führen kann.
      • Änderungen erfordern oft administrative Eingriffe in Keycloak und ein erneutes Ausstellen von Tokens.
  2. Direkte DB-Abfrage ohne Token Enrichment:
    • Vorteile:
      • Das Token bleibt schlank und enthält nur minimale Informationen (z. B. den sub-Claim).
      • Alle zusätzlichen Daten werden bei Bedarf direkt aus der Datenbank abgefragt, was zu aktuelleren Informationen führt.
    • Nachteile:
      • Erhöhte Latenz und DB-Last bei jedem Request, sofern kein effektives Caching implementiert wird.
      • Die Anwendung muss zusätzliche Logik zur Handhabung von DB-Zugriffen implementieren.
  3. Hybrid-Ansatz:
    • Eine Kombination, bei der grundlegende und häufig benötigte Informationen über Token-Enrichment bereitgestellt werden, während selten benötigte oder sehr umfangreiche Daten direkt per DB-Abfrage abgerufen werden.
    • Dies ermöglicht eine Balance zwischen Performance und Aktualität, erfordert jedoch sorgfältige Abstimmung und Caching-Strategien.

Gesamtbewertung

Der im Blog vorgestellte Ansatz – Keycloak als Identity Provider kombiniert mit einer separaten Applikationsdatenbank und gezieltem Token Enrichment – stellt einen robusten, skalierbaren und sicheren Ansatz dar. Er eignet sich besonders für Anwendungen, die:

  • Eine hohe Anzahl komplexer und dynamischer Domänendaten verwalten.
  • Eine klare Trennung zwischen Authentifizierung und Geschäftslogik benötigen.
  • Leistungs- und Sicherheitsanforderungen haben, die durch schlanke Token und gezielte Datenabfragen optimiert werden können.

Alternativ kann das direkte Auslesen der Datenbank (ohne Token Enrichment) in Szenarien sinnvoll sein, in denen höchste Aktualität und Flexibilität bei seltenen, umfangreichen Datenabfragen im Vordergrund stehen – vorausgesetzt, es werden entsprechende Caching- und Performance-Massnahmen getroffen.

7. Fazit

Die Kombination von Keycloak und einer separaten Applikationsdatenbank bietet das Beste aus beiden Welten:

  • Sicherheit und Standardisierung:
    Keycloak übernimmt die Authentifizierung und das Rollenmanagement. Das ID Token bleibt schlank und enthält ausschließlich Basis-Identitätsinformationen.
  • Flexibilität und Erweiterbarkeit:
    Komplexe, dynamische Benutzerdaten werden in einer separaten Datenbank verwaltet. Mithilfe des Token-Enrichment-Prozesses wird der Access Token gezielt um diese zusätzlichen Informationen erweitert.
  • Klare Trennung der Verantwortlichkeiten:
    Während das ID Token ausschließlich der Verifizierung dient, stellt der Access Token alle notwendigen Daten für die Geschäftslogik bereit – ohne bei jedem Request separate Datenbankabfragen ausführen zu müssen.
  • Alternative Ansätze:
    Neben dem Token-Enrichment können zusätzliche Daten auch direkt aus der Datenbank abgefragt werden – insbesondere, wenn höchste Aktualität erforderlich ist oder ein effektives Caching implementiert wurde. Die Wahl des Ansatzes hängt von den konkreten Anforderungen an Performance, Sicherheit und Flexibilität ab.
  • Praxisbewährte Architektur:
    Zahlreiche Open-Source-Projekte (z. B. JHipster) und Enterprise-Lösungen (wie Red Hat Single Sign-On und OpenShift) setzen auf diese Architektur, um eine robuste, skalierbare und sichere Lösung zu implementieren.

Mit diesem Ansatz und der sorgfältigen Abwägung der Alternativen sind Sie bestens gerüstet, moderne Anwendungen zu entwickeln, die den hohen Anforderungen an Sicherheit, Performance und flexible Datenverwaltung gerecht werden.

💡
Möchten Sie mehr zum Thema Spring Security erfahren? Besuchen Sie meinen Spring Security Kurs bei letsboot.com.

Anhang

A1. Herausforderungen & Lösungsansätze

Problem 1: Gelöschte Keycloak-Benutzer

Szenario: Ein Benutzer wird in Keycloak gelöscht, jedoch verbleiben Einträge in der Applikationsdatenbank.

Lösungsansatz:
Implementieren Sie einen Event Listener in Keycloak, der bei Benutzerlöschungen einen REST-Call an Ihre Anwendung sendet, um den entsprechenden Datensatz zu entfernen.

Beispiel (Keycloak SPI):

/**
 * Listens for user deletion events and removes the associated user data from the application's database.
 *
 * <p>This event listener listens for account deletion events. When a DELETE_ACCOUNT event is received,
 * it calls the {@link AppUserService#deleteByKeycloakId(String)} method to remove the corresponding user data.
 * </p>
 */
public class UserDeleteListener implements EventListenerProvider {

    private final AppUserService appUserService;

    /**
     * Constructs a new UserDeleteListener with the provided {@link AppUserService}.
     *
     * @param appUserService the service used for deleting user data from the application database
     */
    public UserDeleteListener(AppUserService appUserService) {
        this.appUserService = appUserService;
    }

    /**
     * Processes an event. If the event type is DELETE_ACCOUNT, the associated user data is deleted
     * from the application's database.
     *
     * @param event the event to process
     */
    @Override
    public void onEvent(Event event) {
        if (event.getType() == EventType.DELETE_ACCOUNT) {
            String userId = event.getUserId();
            // Delete the user data in the application database.
            appUserService.deleteByKeycloakId(userId);
        }
    }

    /**
     * Closes this listener and releases any allocated resources if necessary.
     */
    @Override
    public void close() {
        // Release resources if necessary.
    }
}

Code: UserDeleteListener.java

Problem 2: Kombinierter Registrierungsfluss

Szenario: Bei der Registrierung sollen sowohl Keycloak als auch die Applikationsdatenbank befüllt werden.

Lösungsansatz:
Erstellen Sie einen Endpunkt, der die Registrierung in beiden Systemen koordiniert.

Beispiel:

# Example for the registration endpoint
POST /api/signup
Content-Type: application/json

{
  "username": "user1",
  "password": "securePassword123",
  "discountCode": "DISCOUNT123"
}

Signup POST Request

Ablauf:

  1. Keycloak: Erstellen Sie den Benutzer via Keycloak Admin-API.
  2. App-Datenbank: Speichern Sie zusätzliche Informationen wie discountCode und initiale Profile in Ihrer Datenbank.
  3. Transaktionale Konsistenz: Nutzen Sie eine transaktionale Logik oder eine Event-basierte Architektur, um Inkonsistenzen zu vermeiden.

A2. Best Practices und weiterführende Hinweise

  1. Datenkonsistenz:
    • Verwenden Sie transaktionale Updates, wenn Änderungen sowohl in Keycloak als auch in der Applikationsdatenbank vorgenommen werden.
    • Implementieren Sie Mechanismen zur Fehlerbehandlung und Rollback-Strategien, um bei Kommunikationsausfällen zwischen den Systemen nicht in einen inkonsistenten Zustand zu geraten.
  2. Sicherheit:
    • Verschlüsselung: Überlegen Sie, ob sensible Felder (z. B. keycloak_id) zusätzlich in der Datenbank verschlüsselt werden sollten.
    • Minimierung der Token-Claims: Fügen Sie dem JWT ausschliesslich Informationen hinzu, die auch wirklich notwendig sind, um Missbrauch zu vermeiden.
  3. Performance & Caching:
    • Nutzen Sie Caching-Mechanismen (z. B. mit Spring Cache), um häufig abgerufene Benutzerdaten zu cachen
      @Cacheable(value = "users", key = "#keycloakId")
      public AppUser getUser(String keycloakId)
      {
      // Datenbankzugriff
      }
    • Überwachen Sie die Token-Grösse, um zu vermeiden, dass unnötig viele Daten übertragen werden.
  4. Testbarkeit & Wartbarkeit:
    • Setzen Sie auf Konstruktor-Injektion anstelle von Field-Injektion, um Unit-Tests zu erleichtern.
    • Dokumentieren Sie den Code ausreichend und integrieren Sie automatisierte Tests für sicherheitsrelevante Komponenten.