I’m maintaining an application, which is using the Keycloak Spring Security adapter. Now that Keycloak deprecated their adapter, without providing a migration guide, I had to find a solution.

Migration from Keycloak adapter to default Spring Security

The application has a UI, which has a login-flow handled by Keycloak. First remove all Keycloak dependencies from your project, like org.keycloak:keycloak-spring-boot-starter. For this login-flow you need to add the org.springframework.security:spring-security-oauth2-client dependency.

Migrate Keycloak adapter properties to Spring properties

Now it’s time to convert the Keycloak properties to Spring Security configuration.

Listing 1. Keycloak application properties
keycloak:
  realm:               master
  auth-server-url:     [link to your keycloak server]
  resource:            master
  principal-attribute: preferred_username

  credentials:
    secret: ${keycloak_client_secret}
  security-constraints:
    - auth-roles:
        - admin
        - user
        - editor
      security-collections:
        - patterns:
            - /web/*
    - security-collections:
        - patterns:
            - /web/unsecured

First move the Keycloak server properties to the corresponding Spring Security properties

Listing 2. Spring Security properties
spring:
  security:
    oauth2:
      client:
        provider:
          keycloak:
            user-name-attribute: preferred_username
            issuer-uri: ${keycloak-server-url}/realms/master (1)
        registration:
          keycloak:
            client-id: ares
            client-secret: ${keycloak_client_secret}
            authorization-grant-type: authorization_code
            scope: openid
1 Spring Security will figure out the server’s configuration through the issuer-uri-property. It will get the configuration by requesting it from ${keycloak-server-url}/realms/{realm-name}/.well-known/openid-configuration
Migrate Keycloak adapter properties to Spring Security Configuration

Next migrate the secured URLs to Spring Security configuration. In this case everything under /web can only be accessed by users with roles admin, user, editor, except /web/unsecured!

Listing 3. Spring Security Configuration class
@Configuration
@EnableWebSecurity
public class SpringSecurityConfiguration {
    @Bean
    SecurityFilterChain webOAuth2FilterChain(final HttpSecurity httpSecurity) {
        return httpSecurity
                .authorizeHttpRequests(authorize ->
                        authorize
                                .requestMatchers("/web/**").hasAnyAuthority("user", "editor", "admin")
                                .requestMatchers("/web/unsecured").permitAll()
                                .anyRequest().permitAll()
                )
                .oauth2Login(withDefaults()) (1)
                .build();
    }
}
1 .oauth2Login(withDefaults()) takes care of redirecting the user to a login page, to actual login and will redirect to the originally requested page after logging in successfully.

When you run the application with this configuration, it won’t work. The roles defined in Keycloak aren’t parsed from the token. This is because JWT doesn’t have a standard way to define roles in a token.

Parsing Keycloak roles to GrantedAuthorities

Keycloak adds the roles in the token, but Spring Security isn’t configured to parse the Keycloak-specific roles part of the token. Keycloak stores them in $.realm_access.roles. So we need a way to parse these roles.

Listing 4. Add a GrantedAuthoritiesMapper to parse the roles from the token
@Bean
GrantedAuthoritiesMapper grantedAuthoritiesMapper() {
    return authorities -> authorities.stream()
            .filter(authority -> authority instanceof OidcUserAuthority)
            .map(authority -> (OidcUserAuthority) authority)
            .map(oidcUserAuthority -> (Map<String, Object>) oidcUserAuthority.getIdToken().getClaims().get("realm_access"))
            .map(realmAccess -> ((List<String>) realmAccess.get("roles"))
                    .stream().map(SimpleGrantedAuthority::new)
                    .collect(Collectors.toSet()))
            .flatMap(Collection::stream)
            .collect(Collectors.toCollection(() -> new HashSet<GrantedAuthority>()));
}

OAuth2 works with different tokens, like idToken, userToken and accessToken. Make sure you configure Keycloak properly to add the roles in the needed token. In this example the roles not only need to be included in the accessToken, but also in the idToken for Spring Security to parse the roles properly.

Now restart the application, and you’ll see it’s all working now.

Last words

I’ve read a lot on StackOverflow and tried to understand the Spring Security documentation, but you’ll need quite some understanding about OAuth2 to be able to understand the Spring Security documentation. If you, like me, need to migrate away from the Keycloak adapter but don’t have the time to fully understand Spring Security 6 and OAuth2, this blog might help you.

If you want more understanding about these subjects I can recommend examples created by Ch4mpy on GitHub. The user wrote a more human-friendly way to explain OAuth2 in combination with Spring Security 6 and has a lot of examples. Ch4mpy developed a library to easily migrate away from the Keycloak adapter, but you still depend on a library for your Spring Security configuration.

This situation is explained in the Spring Security documentation, in the OAuth2 Log In, Advanced Configuration section.

shadow-left