Migrating from Legacy Keycloak Spring Boot Adapters

Guilliano Molaire Guilliano Molaire Updated March 15, 2026 8 min read

Last updated: March 2026
If you have been running Keycloak with Spring Boot for any length of time, you have almost certainly used the keycloak-spring-boot-starter or keycloak-spring-security-adapter libraries. These adapters made it straightforward to wire up authentication and role-based access control with just a few configuration properties. Starting with Keycloak 20, however, the Keycloak team removed these adapters entirely and recommended that developers migrate to Spring Security’s built-in OAuth2 and OpenID Connect support.

This guide walks you through the complete migration path, from removing the old dependencies to configuring Spring Security’s native resource server and client libraries, mapping Keycloak roles, and verifying that everything works.

Why the Adapters Were Deprecated

The Keycloak Spring Boot and Spring Security adapters were originally created because Spring Security lacked first-class support for OAuth2 and OIDC. Over the years, the Spring Security team invested heavily in native OAuth2 capabilities, eventually providing everything the Keycloak adapters offered and more.

Maintaining a parallel set of adapters became redundant. The Keycloak team outlined several reasons for the deprecation:

  • Duplication of effort. Spring Security’s OAuth2 resource server and client modules cover the same functionality with better integration into the broader Spring ecosystem.
  • Version coupling. The adapters tied your Keycloak server version to a specific Spring Boot version, creating upgrade headaches.
  • Community alignment. By standardizing on Spring Security’s OAuth2 support, applications become portable across any OIDC-compliant identity provider, not just Keycloak.
  • Reduced maintenance burden. The Keycloak project could focus on the server and admin console rather than maintaining client-side framework integrations.

The practical impact is that if you upgrade to Keycloak 20 or later — or if you are deploying a managed Keycloak instance on Skycloak — you need to migrate away from the legacy adapters.

The Old Way: keycloak-spring-boot-starter

Before the deprecation, a typical Spring Boot application using Keycloak adapters looked something like this.

Maven Dependencies

<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-spring-boot-starter</artifactId>
    <version>19.0.3</version>
</dependency>
<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-spring-security-adapter</artifactId>
    <version>19.0.3</version>
</dependency>

Application Properties

keycloak.realm=myrealm
keycloak.auth-server-url=https://keycloak.example.com/auth
keycloak.resource=my-spring-app
keycloak.credentials.secret=my-client-secret
keycloak.ssl-required=external
keycloak.use-resource-role-mappings=true

Security Configuration

@KeycloakConfiguration
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        http.authorizeRequests()
            .antMatchers("/api/public/**").permitAll()
            .antMatchers("/api/admin/**").hasRole("admin")
            .anyRequest().authenticated();
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) {
        var provider = keycloakAuthenticationProvider();
        provider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
        auth.authenticationProvider(provider);
    }

    @Bean
    @Override
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new RegisterSessionAuthenticationStrategy(
            new SessionRegistryImpl()
        );
    }
}

This pattern worked well, but it relied entirely on Keycloak-specific classes. If you ever wanted to swap out your identity provider or upgrade independently, you were stuck.

The New Way: Spring Security Native OAuth2

The migration target uses two standard Spring Security starters:

  • spring-boot-starter-oauth2-resource-server for protecting APIs with JWT tokens.
  • spring-boot-starter-oauth2-client for web applications that need browser-based login flows.

Most backend services only need the resource server dependency. Web applications that serve HTML pages and need to initiate login flows will need the client dependency as well.

Step-by-Step Migration

Step 1: Update Your Dependencies

Remove the old Keycloak dependencies and add the Spring Security OAuth2 starters.

Remove:

<!-- Remove these -->
<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-spring-boot-starter</artifactId>
</dependency>
<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-spring-security-adapter</artifactId>
</dependency>

Add:

<!-- For API/resource server -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

<!-- For web apps with login flows (optional) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

<!-- Spring Security core (usually already present) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

If you are using Gradle, the equivalent changes apply to your build.gradle:

// Remove
implementation 'org.keycloak:keycloak-spring-boot-starter:19.0.3'
implementation 'org.keycloak:keycloak-spring-security-adapter:19.0.3'

// Add
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-security'

Step 2: Update application.yml

Replace the old keycloak.* properties with Spring Security’s OAuth2 configuration. The key difference is that Spring Security uses the issuer URI to auto-discover endpoints via the .well-known/openid-configuration document.

Remove the old keycloak block and add:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://keycloak.example.com/realms/myrealm
          jwk-set-uri: https://keycloak.example.com/realms/myrealm/protocol/openid-connect/certs
      client:
        registration:
          keycloak:
            client-id: my-spring-app
            client-secret: my-client-secret
            scope: openid,profile,email
            authorization-grant-type: authorization_code
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
        provider:
          keycloak:
            issuer-uri: https://keycloak.example.com/realms/myrealm
            user-name-attribute: preferred_username

Note that the URL format changed in newer Keycloak versions. The old format used /auth/realms/myrealm, while Keycloak 17+ uses /realms/myrealm without the /auth prefix. Verify your server’s actual issuer URI, which you can find at https://your-keycloak-host/realms/your-realm/.well-known/openid-configuration.

If you are using Skycloak’s managed Keycloak hosting, the issuer URI follows the format https://your-instance.app.skycloak.io/realms/your-realm.

Step 3: Create the SecurityFilterChain

Replace the old KeycloakWebSecurityConfigurerAdapter with a SecurityFilterChain bean. The WebSecurityConfigurerAdapter pattern was deprecated in Spring Security 5.7, so this migration addresses both deprecations at once.

For a resource server (API):

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("admin")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .jwtAuthenticationConverter(jwtAuthenticationConverter())
                )
            );

        return http.build();
    }

    private JwtAuthenticationConverter jwtAuthenticationConverter() {
        var converter = new JwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(new KeycloakRoleConverter());
        return converter;
    }
}

For a web application with login:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/", "/public/**").permitAll()
                .requestMatchers("/admin/**").hasRole("admin")
                .anyRequest().authenticated()
            )
            .oauth2Login(oauth2 -> oauth2
                .userInfoEndpoint(userInfo -> userInfo
                    .userAuthoritiesMapper(userAuthoritiesMapper())
                )
            )
            .logout(logout -> logout
                .logoutSuccessHandler(oidcLogoutSuccessHandler())
            );

        return http.build();
    }
}

Step 4: Implement Role Mapping

One of the most important parts of the migration is preserving your role mappings. Keycloak stores roles in a nested JWT claim structure that differs from what Spring Security expects by default. You need a custom converter to extract these roles.

The KeycloakRoleConverter replaces the functionality that the old adapter handled automatically:

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

    @Override
    public Collection<GrantedAuthority> convert(Jwt jwt) {
        var authorities = new ArrayList<GrantedAuthority>();

        // Extract realm roles
        Map<String, Object> realmAccess = jwt.getClaimAsMap("realm_access");
        if (realmAccess != null && realmAccess.containsKey("roles")) {
            @SuppressWarnings("unchecked")
            var roles = (List<String>) realmAccess.get("roles");
            roles.forEach(role ->
                authorities.add(new SimpleGrantedAuthority("ROLE_" + role))
            );
        }

        // Extract client roles
        Map<String, Object> resourceAccess = jwt.getClaimAsMap("resource_access");
        if (resourceAccess != null) {
            resourceAccess.forEach((clientId, access) -> {
                @SuppressWarnings("unchecked")
                var clientAccess = (Map<String, Object>) access;
                if (clientAccess.containsKey("roles")) {
                    @SuppressWarnings("unchecked")
                    var roles = (List<String>) clientAccess.get("roles");
                    roles.forEach(role ->
                        authorities.add(new SimpleGrantedAuthority(
                            "ROLE_" + clientId + "_" + role
                        ))
                    );
                }
            });
        }

        return authorities;
    }
}

This converter handles both realm-level and client-level roles. If your application only uses realm roles, you can simplify it accordingly. You can inspect your token’s role structure using the JWT Token Analyzer to see exactly how Keycloak structures the claims.

Step 5: Handle OIDC Logout

The old Keycloak adapter handled logout by redirecting to the Keycloak end-session endpoint. With Spring Security, you need to configure this explicitly:

@Bean
public OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler() {
    var handler = new OidcClientInitiatedLogoutSuccessHandler(
        clientRegistrationRepository
    );
    handler.setPostLogoutRedirectUri("{baseUrl}");
    return handler;
}

This ensures that when users log out of your application, their Keycloak session is also terminated.

Step 6: Remove Keycloak Adapter Artifacts

Clean up any remaining Keycloak adapter artifacts from your project:

  1. Delete keycloak.json if it exists in src/main/resources/ or WEB-INF/.
  2. Remove any @KeycloakConfiguration annotations.
  3. Remove imports from org.keycloak.adapters.*.
  4. Remove the KeycloakSpringBootConfigResolver bean if you had one.
  5. Update any custom KeycloakAuthenticationProvider references.

Testing the Migration

After completing the migration, verify everything works correctly.

Verify Token Validation

Start your application and make a request with a valid JWT:

# Get a token from Keycloak
TOKEN=$(curl -s -X POST 
  "https://keycloak.example.com/realms/myrealm/protocol/openid-connect/token" 
  -d "grant_type=client_credentials" 
  -d "client_id=my-spring-app" 
  -d "client_secret=my-client-secret" 
  | jq -r '.access_token')

# Test a protected endpoint
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/protected

Verify Role Mappings

Test that role-based access control works as expected:

# This should succeed for users with the admin role
curl -H "Authorization: Bearer $ADMIN_TOKEN" http://localhost:8080/api/admin/users

# This should return 403 for users without the admin role
curl -H "Authorization: Bearer $USER_TOKEN" http://localhost:8080/api/admin/users

Write Integration Tests

Add a test that validates the security configuration:

@SpringBootTest
@AutoConfigureMockMvc
class SecurityConfigTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void publicEndpointShouldBeAccessible() throws Exception {
        mockMvc.perform(get("/api/public/health"))
            .andExpect(status().isOk());
    }

    @Test
    void protectedEndpointShouldRequireAuth() throws Exception {
        mockMvc.perform(get("/api/protected"))
            .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser(roles = "admin")
    void adminEndpointShouldAllowAdminRole() throws Exception {
        mockMvc.perform(get("/api/admin/users"))
            .andExpect(status().isOk());
    }
}

Common Pitfalls

A few issues come up frequently during this migration:

  • Issuer URI mismatch. The issuer in the JWT must exactly match the issuer-uri in your configuration, including trailing slashes and the /auth prefix. A mismatch causes token validation to fail silently.
  • Missing ROLE_ prefix. Spring Security expects authorities to start with ROLE_ for hasRole() checks. The custom converter above handles this, but if you use hasAuthority() instead, you need to include the prefix in your security rules.
  • CORS configuration. The old adapter may have handled CORS for you. With native Spring Security, you need to configure CORS explicitly in your SecurityFilterChain.
  • Multi-tenancy. If your application supports multiple realms, you will need a custom JwtDecoder that can validate tokens from multiple issuers.

Benefits of the Migration

Beyond simply keeping your application working with modern Keycloak versions, this migration brings tangible improvements:

  • Provider portability. Your application now works with any OIDC-compliant provider. If you ever move from self-hosted Keycloak to a managed service, or switch providers entirely, the security layer stays the same.
  • Better Spring integration. Native OAuth2 support integrates cleanly with Spring Boot’s auto-configuration, actuator endpoints, and testing frameworks.
  • Simplified dependencies. Fewer third-party libraries mean fewer CVEs to track and fewer version conflicts.
  • Future-proof. You are aligned with both the Spring Security and Keycloak project roadmaps.

If you are exploring single sign-on capabilities for your application, the Spring Security OAuth2 integration provides a solid foundation. For detailed integration examples, check the Spring Boot integration guide in the Skycloak documentation.

Conclusion

Migrating from the legacy Keycloak Spring Boot adapters to Spring Security’s native OAuth2 support is a necessary step for any team running Keycloak 20 or later. The process involves replacing dependencies, updating configuration properties, rewriting the security configuration to use SecurityFilterChain, and implementing a custom role converter. While the migration requires touching several files, the end result is a cleaner, more portable, and secure application that follows current Spring Security best practices.

If you are looking for a managed Keycloak environment where you can focus on your application code rather than infrastructure, Skycloak provides fully managed Keycloak instances with automatic updates, backups, and monitoring. Combined with Spring Security’s native OAuth2 support, it is a straightforward path to production-ready identity management.

Guilliano Molaire
Written by Guilliano Molaire Founder

Guilliano is the founder of Skycloak and a cloud infrastructure specialist with deep expertise in product development and scaling SaaS products. He discovered Keycloak while consulting on enterprise IAM and built Skycloak to make managed Keycloak accessible to teams of every size.

Ready to simplify your authentication?

Deploy production-ready Keycloak in minutes. Unlimited users, flat pricing, no SSO tax.

© 2026 Skycloak. All Rights Reserved. Design by Yasser Soliman