Neuigkeiten von trion.
Immer gut informiert.

OAuth 2.0 Resource Server: Keycloak Rollen auf Spring Security Authorities mappen

In vielen modernen Anwendungen nutzen wir Keycloak zur Authentifizierung für den Zugriff auf Spring Boot Anwendungen. Dabei werden Benutzer und ihre Rollen in Keycloak gespeichert bzw. von Keycloak bereitgestellt. In einer Spring-Anwendung möchten wir diese Rollen nun nutzen, um den Zugriff auf Ressourcen zu autorisieren. In einem weiteren Blogpost betrachten wir Fall in dem unsere Anwendung als OAuth 2.0 Client agiert und einen OAuth 2.0 Login implementiert.

Standardmäßig extrahiert Spring Security automatisch Authorities aus dem OAuth2 Scope Claim. Die vergebenen Rollen werden durch Keycloak in einem separaten Claim bereitgestellt und stehen damit nicht automatisch für die Nutzung zur Autorisierung zur Verfügung. Nachfolgend ist kurz beschrieben, wie wir die Keycloak-Rollen als Spring Security Authorities verfügbar machen.

Für diesen Blog-Post nehmen wir an, dass user JWT folgende Struktur aufweist:

JWT mit Keycloak Rolle
{
  "resource_access": {
    "client-id": {
      "roles": [
        "ADMIN"
      ]
    }
  }
  // more claims ...
}

Die Rollen, für die wir uns interessieren, liegen also im Claim resource_access. Wie in vielen Bereich von Spring Security können wir auch für diesen Fall mit einer eigenen Implementierung den Standard überschreiben uns auch aus diesem Claim Granted Authorities machen.

Eigenen JWT-Authentication-Converter bereitstellen
private JwtAuthenticationConverter jwtAuthenticationConverter() {
    Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter =
        new KeycloakRolesJwtGrantedAuthoritiesConverter();

    JwtAuthenticationConverter jwtAuthenticationConverter =
        new JwtAuthenticationConverter();
    jwtAuthenticationConverter
        .setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
    return jwtAuthenticationConverter;
}

Bei der Verarbeitung eines Request wird ein JwtAuthenticationConverter aufgerufen. Dieser wandelt das im Request enthaltene JWT in ein Spring Security Authentication-Objekt um.

Um diese Aufgabe zu erfüllen, greift dieser Converter selbst auf einen weiteren Konverter zu, den JwtGrantedAuthoritiesConverter. Dieser übernimmt schließlich das Auslesen der Authorities aus dem JWT. Standardmäßig werden dafür die Einträge des Scope-Claims im Token herangezogen und mit dem Präfix SCOPE_ versehen.

Um hier unsere Logik nutzen zu können, stellen wir eine eigene Implementierung bereit, den KeycloakRolesJwtGrantedAuthoritiesConverter. Dazu muss das Interface Converter<Jwt, Collection<GrantedAuthority>> aus dem Spring-Security-Framework implementiert werden. Das Interface schreibt lediglich eine einzelne Methode vor, die ein JWT entgegennimmt und es in eine Sammlung von GrantedAuthority-Objekten umwandelt.

KeycloakRolesJwtGrantedAuthoritiesConverter
public class KeycloakRolesJwtGrantedAuthoritiesConverter
        implements Converter<Jwt, Collection<GrantedAuthority>> {

    private final JwtGrantedAuthoritiesConverter defaultConverter
            = new JwtGrantedAuthoritiesConverter();

    @Override
    public Collection<GrantedAuthority> convert(final Jwt jwt) {
        return Stream.concat(
                        defaultConverter.convert(jwt).stream(),
                        extractRoles(jwt).stream()
                )
                .collect(Collectors.toSet());
    }

    private Collection<? extends GrantedAuthority> extractRoles(final Jwt jwt) {
        return Optional.ofNullable(
                    jwt.getClaimAsMap("resource_access")
            )
            .map(resourceAccess -> resourceAccess.get("client-id"))
            .map(Map.class::cast)
            .map(client -> client.get("roles"))
            .map(Collection.class::cast)
            .map(roles -> ((Collection<String>) roles).stream()
                    .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
                    .collect(Collectors.toSet())
            ).orElse(Collections.emptySet());
    }
}

Die Methode convert kombiniert die Authorities aus dem Standard-Converter mit den extrahierten Keycloak-Rollen. Die Methode extractRoles navigiert durch die verschachtelte Struktur des JWTs, um die Rollen zu finden und in Granted Authorities mit dem Präfix ROLE_ umzuwandeln.

In diesem Fall werden die Rollen im Token als Array unter resource_access.client-id.roles erwartet. Bei Realm-Rollen muss der Code entsprechend der Struktur des Tokens angepasst werden.

Security Filter Chain
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) {
    return httpSecurity
        .oauth2ResourceServer(oauth2 ->
            oauth2.jwt(jwt ->
                jwt.jwtAuthenticationConverter(
                        jwtAuthenticationConverter()
                )
            )
        )
        .authorizeHttpRequests(matcher -> matcher
                .requestMatchers("/api/private").hasRole("ADMIN")
                .anyRequest().authenticated()
        )
        .build();
}

Schließlich muss der angepasste JwtAuthenticationConverter noch in der SecurityFilterChain registriert werden. Nun können die hinterlegten Rollen komfortabel für die Zugriffskontrolle auf Request- oder Methodenebene verwendet werden.




Zu den Themen Keycloak und Spring Security bieten wir sowohl Beratung, Entwicklungsunterstützung als auch passende Schulungen an:

Vor der Inbetriebnahme einer Keycloak-Installation, sollte sichergestellt sein, dass das System tatsächlich bereit für den produktiven Einsatz ist und, dass alle wesentlichen Punkte bei Konfiguration, Integration und Betriebsprozessen berücksichtigt wurden. Hierbei hilft unser Production Readiness Review für Keycloak.

Auch für Ihren individuellen Bedarf können wir Workshops und Schulungen anbieten. Sprechen Sie uns gerne an.

Feedback oder Fragen zu einem Artikel - per Twitter @triondevelop oder E-Mail freuen wir uns auf eine Kontaktaufnahme!

Zur Desktop Version des Artikels