Migrating from Legacy Keycloak Spring Boot Adapters
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-serverfor protecting APIs with JWT tokens.spring-boot-starter-oauth2-clientfor 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:
- Delete
keycloak.jsonif it exists insrc/main/resources/orWEB-INF/. - Remove any
@KeycloakConfigurationannotations. - Remove imports from
org.keycloak.adapters.*. - Remove the
KeycloakSpringBootConfigResolverbean if you had one. - Update any custom
KeycloakAuthenticationProviderreferences.
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-uriin your configuration, including trailing slashes and the/authprefix. A mismatch causes token validation to fail silently. - Missing ROLE_ prefix. Spring Security expects authorities to start with
ROLE_forhasRole()checks. The custom converter above handles this, but if you usehasAuthority()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
JwtDecoderthat 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.
Ready to simplify your authentication?
Deploy production-ready Keycloak in minutes. Unlimited users, flat pricing, no SSO tax.