I updated to Spring Boot 3 in a project that uses the Keycloak Spring Adapter. Unfortunately, it doesn't start because the KeycloakWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter which was first deprecated in Spring Security and then removed. Is there currently another way to implement security with Keycloak? Or to put it in other words: How can I use Spring Boot 3 in combination with the Keycloak adapter?
I searched the Internet, but couldn't find any other version of the adapter.
You can't use Keycloak adapters with spring-boot 3 for the reason you found, plus a few others related to transitive dependencies. As most Keycloak adapters were deprecated in early 2022, it is very likely that no update will be published to fix that.
Directly use spring-security OAuth2 instead. Don't panic, it's an easy task with spring-boot.
spring-addons starters for resource server (app exposes a REST API)
I maintain 4 thin wrappers around "official" boot resource-server starter because, in my opinion, auto-configuration can be pushed one step further to:
make OAuth2 configuration more portable: with a configurable authorities converter, switching from an OIDC provider to another is just a matter of editing properties (Keycloak, Auth0, Cognito, Azure AD, etc.)
ease app deployment on different environments: CORS configuration is controlled from properties file
reduce drastically the amount of Java code (things get even more complicated if you are in multi-tenancy scenario)
reduce chances of misconfiguration (easy to de-synchronise CSRF protection and sessions configuration for instance)
It is very thin (each is composed of three files only) and greatly simplifies resource-servers configuration:
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<!-- replace "webmvc" with "weblux" if your app is reactive -->
<!-- replace "jwt" with "introspecting" to use token introspection instead of JWT decoding -->
<artifactId>spring-addons-webmvc-jwt-resource-server</artifactId>
<!-- this version is to be used with spring-boot 3.0.1, use 5.4.x for spring-boot 2.6.x or before -->
<version>6.0.13</version>
</dependency>
#Configuration
#EnableMethodSecurity
public static class WebSecurityConfig { }
com.c4-soft.springaddons.security.issuers[0].location=https://localhost:8443/realms/realm1
com.c4-soft.springaddons.security.issuers[0].authorities.claims=realm_access.roles,ressource_access.some-client.roles,ressource_access.other-client.roles
com.c4-soft.springaddons.security.cors[0].path=/some-api
com.c4-soft.springaddons.security.permit=all=/actuator/health/readiness,/actuator/health/liveness,/v3/api-docs/**
Nothing more is needed to configure a multi-tenant resource-server with fine tuned CORS policy and authorities mapping. Bootiful, isn't it?
By "multi-tenant", I mean that, as you can guess from this issuers property being an array, you can trust as many OIDC authorization-server instances as you need (multiple Keycloak realms & instances, or even mix with other OIDC providers like Auth0, Cognito, etc.), each with it's own authorities mapping configuration.
Client configuration (UI with oauth2Login())
If your Spring application exposes secured UI elements you want to be accessible with a browser (with OAuth2 login), you'll have to provide a FilterChain with "client" configuration.
If this app exposes both a REST API and a UI to manipulate it (with oauth2Login()), then you'll have to setup two security filter-chains: one with client config and the other with resource-server config.
Add this to pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
Here we demo a SecurityFilterChain applying only to a list of routes defined with a securityMatcher.
This assumes that an additional resource-server SecurityFilterChain is defined, with lower order and no securityMatcher so that all routes are intercepted after all filter chains are evaluated in order. This other filter chain could be defined either implicitly (by spring-addons as described above) or explicitly (with Spring Boot official starter as described below).
Remove the securityMatcher section if your app is solely a client:
// Give higher precedence to security filter-chains with "securityMatcher"
#Order(Ordered.HIGHEST_PRECEDENCE)
#Bean
SecurityFilterChain uiFilterChain(
HttpSecurity http,
ServerProperties serverProperties,
GrantedAuthoritiesMapper authoritiesMapper) throws Exception {
http.securityMatcher(new OrRequestMatcher(
// add path to your UI elements instead
new AntPathRequestMatcher("/ui/**"),
// those two are required to access Spring generated login page
// and OAuth2 client callback endpoints
new AntPathRequestMatcher("/login/**"),
new AntPathRequestMatcher("/oauth2/**")));
http.oauth2Login().userInfoEndpoint().userAuthoritiesMapper(authoritiesMapper);
http.authorizeHttpRequests()
.requestMatchers("/ui/index.html").permitAll()
.requestMatchers("/login/**").permitAll()
.requestMatchers("/oauth2/**").permitAll()
.anyRequest().authenticated();
// If SSL enabled, disable http (https only)
if (serverProperties.getSsl() != null && serverProperties.getSsl().isEnabled()) {
http.requiresChannel().anyRequest().requiresSecure();
}
// Many defaults are kept compared to API filter-chain:
// - sessions (and CSRF protection) are enabled
// - unauthorized requests to secured resources will be redirected to login (302 to login is Spring's default response when authorisation is missing or invalid)
return http.build();
}
#Bean
GrantedAuthoritiesMapper userAuthoritiesMapper(Converter<Map<String, Object>, Collection<? extends GrantedAuthority>> authoritiesConverter) {
return (authorities) -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
authorities.forEach(authority -> {
if (authority instanceof OidcUserAuthority oidcAuth) {
mappedAuthorities.addAll(authoritiesConverter.convert(oidcAuth.getIdToken().getClaims()));
} else if (authority instanceof OAuth2UserAuthority oauth2Auth) {
mappedAuthorities.addAll(authoritiesConverter.convert(oauth2Auth.getAttributes()));
}
});
return mappedAuthorities;
};
}
The code above assumes that a Converter<Map<String, Object>, Collection<? extends GrantedAuthority>> bean is exposed. One is auto-configured by spring-addons starters for resource-server and the "official" starter section below defines one. Take the later as sample if your app is solely a client.
Last, client properties:
spring.security.oauth2.client.provider.keycloak.issuer-uri=https://localhost:8443/realms/master
spring.security.oauth2.client.registration.spring-addons-public.provider=keycloak
spring.security.oauth2.client.registration.spring-addons-public.client-name=spring-addons-public
spring.security.oauth2.client.registration.spring-addons-public.client-id=spring-addons-public
spring.security.oauth2.client.registration.spring-addons-public.scope=openid,offline_access,profile
spring.security.oauth2.client.registration.spring-addons-public.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.spring-addons-public.redirect-uri=http://bravo-ch4mp:8080/login/oauth2/code/spring-addons-public
"Official" Spring Boot resource-server starter
As spring-addons-{webmvc|webflux}-{jwt|introspecting}-resource-server are thin wrappers around spring-boot-starter-oauth2-resource-server, you can of course do the same with just the later.
Here is what it takes to configure a resource-server with a unique Keycloak realm as authorization-server:
#Configuration
#EnableWebSecurity
#EnableMethodSecurity
public class WebSecurityConfig {
public interface Jwt2AuthoritiesConverter extends Converter<Jwt, Collection<? extends GrantedAuthority>> {
}
#SuppressWarnings("unchecked")
#Bean
public Jwt2AuthoritiesConverter authoritiesConverter() {
// This is a converter for roles as embedded in the JWT by a Keycloak server
// Roles are taken from both realm_access.roles & resource_access.{client}.roles
return jwt -> {
final var realmAccess = (Map<String, Object>) jwt.getClaims().getOrDefault("realm_access", Map.of());
final var realmRoles = (Collection<String>) realmAccess.getOrDefault("roles", List.of());
final var resourceAccess = (Map<String, Object>) jwt.getClaims().getOrDefault("resource_access", Map.of());
// We assume here you have "spring-addons-confidential" and "spring-addons-public" clients configured with "client roles" mapper in Keycloak
final var confidentialClientAccess = (Map<String, Object>) resourceAccess.getOrDefault("spring-addons-confidential", Map.of());
final var confidentialClientRoles = (Collection<String>) confidentialClientAccess.getOrDefault("roles", List.of());
final var publicClientAccess = (Map<String, Object>) resourceAccess.getOrDefault("spring-addons-public", Map.of());
final var publicClientRoles = (Collection<String>) publicClientAccess.getOrDefault("roles", List.of());
// Merge the 3 sources of roles and map it to spring-security authorities
return Stream.concat(
realmRoles.stream(),
Stream.concat(confidentialClientRoles.stream(), publicClientRoles.stream()))
.map(SimpleGrantedAuthority::new).toList();
};
}
// spring-boot looks for a Converter<Jwt, ? extends AbstractAuthenticationToken> bean
// that is a converter from Jwt to something extending AbstractAuthenticationToken (and not AbstractAuthenticationToken itself)
// In this conf, we use JwtAuthenticationToken as AbstractAuthenticationToken implementation
public interface Jwt2AuthenticationConverter extends Converter<Jwt, JwtAuthenticationToken> {
}
#Bean
public Jwt2AuthenticationConverter authenticationConverter(Jwt2AuthoritiesConverter authoritiesConverter) {
return jwt -> new JwtAuthenticationToken(jwt, authoritiesConverter.convert(jwt));
}
// Give lower precedence to security filter-chains without "securityMatcher" so that the filter-chains with a "securityMatcher" get a chance to be matched
#Order(Ordered.LOWEST_PRECEDENCE)
#Bean
public SecurityFilterChain apiFilterChain(
HttpSecurity http,
ServerProperties serverProperties,
Converter<Jwt, ? extends AbstractAuthenticationToken> authenticationConverter) throws Exception {
// Enable OAuth2 with custom authorities mapping
http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(authenticationConverter);
// As the authentication bean is the one expected by spring-boot,
// an alternative would be to use just
// http.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
// Enable anonymous
http.anonymous();
// Enable and configure CORS
http.cors().configurationSource(corsConfigurationSource());
// State-less session (state in access-token only)
// with Disable CSRF because of disabled sessions
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.csrf().disable();
// Return 401 (unauthorized) instead of 302 (redirect to login) when authorization is missing or invalid
http.exceptionHandling().authenticationEntryPoint((request, response, authException) -> {
response.addHeader(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"Restricted Content\"");
response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
});
// If SSL enabled, disable http (https only)
if (serverProperties.getSsl() != null && serverProperties.getSsl().isEnabled()) {
http.requiresChannel().anyRequest().requiresSecure();
}
// Route security: authenticated to all routes but actuator and Swagger-UI
http.authorizeRequests()
.antMatchers("/actuator/health/readiness", "/actuator/health/liveness", "/v3/api-docs/**").permitAll()
.anyRequest().authenticated();
return http.build();
}
private CorsConfigurationSource corsConfigurationSource() {
// Very permissive CORS config...
final var configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("*"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setExposedHeaders(Arrays.asList("*"));
// Limited to API routes (neither actuator nor Swagger-UI)
final var source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/greet/**", configuration);
return source;
}
}
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://localhost:8443/realms/master
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://localhost:8443/realms/master/protocol/openid-connect/certs
As mentioned in preamble, this is quite more verbose than spring-addons starters, it's not ready for multi-tenancy and each time CORS policy changes (new API routes for instance) or when the claims source for authorities change (new OAuth2 client with client-roles mapping or other OIDC provider than Keycloak), you'll have to edit source-code and re-publish your app...
Use the standard Spring Security OAuth2 client instead of a specific Keycloak adapter and SecurityFilterChain instead of WebSecurityAdapter.
Something like this:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(jsr250Enabled = true, prePostEnabled = true)
class OAuth2SecurityConfig {
#Bean
fun customOauth2FilterChain(http: HttpSecurity): SecurityFilterChain {
log.info("Configure HttpSecurity with OAuth2")
http {
oauth2ResourceServer {
jwt { jwtAuthenticationConverter = CustomBearerJwtAuthenticationConverter() }
}
oauth2Login {}
csrf { disable() }
authorizeRequests {
// Kubernetes
authorize("/readiness", permitAll)
authorize("/liveness", permitAll)
authorize("/actuator/health/**", permitAll)
// ...
// everything else needs at least a valid login, roles are checked at method level
authorize(anyRequest, authenticated)
}
}
return http.build()
}
And then in application.yml:
spring:
security:
oauth2:
client:
provider:
abc:
issuer-uri: https://keycloak.../auth/realms/foo
registration:
abc:
client-secret: ...
provider: abc
client-id: foo
scope: [ openid, profile, email ]
resourceserver:
jwt:
issuer-uri: https://keycloak.../auth/realms/foo
Using Keycloak adapters is not possible because the KeycloakWebSecurityConfigurerAdapter inherited from the WebSecurityConfigurerAdapter class, which was deprecated in Spring Security and subsequently removed in the newer release.
I have published a detailed article on integrating Keycloak with Spring Boot 3.0 on Medium, which provides a step-by-step guide on how to integrate Keycloak with Spring Boot 3.0.
This guide is particularly helpful for those who are new to integrating Keycloak with Spring Boot 3.0 or migrating to Spring Boot 3.0 from an older version.
You can check out the article (https://medium.com/geekculture/using-keycloak-with-spring-boot-3-0-376fa9f60e0b) for a comprehensive explanation of the integration process.
Hope this helps! If you have any questions, further clarifications or suggestions, Please feel free to leave a comment.
Related
I updated to Spring Boot 3 in a project that uses the Keycloak Spring Adapter. Unfortunately, it doesn't start because the KeycloakWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter which was first deprecated in Spring Security and then removed. Is there currently another way to implement security with Keycloak? Or to put it in other words: How can I use Spring Boot 3 in combination with the Keycloak adapter?
I searched the Internet, but couldn't find any other version of the adapter.
You can't use Keycloak adapters with spring-boot 3 for the reason you found, plus a few others related to transitive dependencies. As most Keycloak adapters were deprecated in early 2022, it is very likely that no update will be published to fix that.
Directly use spring-security OAuth2 instead. Don't panic, it's an easy task with spring-boot.
spring-addons starters for resource server (app exposes a REST API)
I maintain 4 thin wrappers around "official" boot resource-server starter because, in my opinion, auto-configuration can be pushed one step further to:
make OAuth2 configuration more portable: with a configurable authorities converter, switching from an OIDC provider to another is just a matter of editing properties (Keycloak, Auth0, Cognito, Azure AD, etc.)
ease app deployment on different environments: CORS configuration is controlled from properties file
reduce drastically the amount of Java code (things get even more complicated if you are in multi-tenancy scenario)
reduce chances of misconfiguration (easy to de-synchronise CSRF protection and sessions configuration for instance)
It is very thin (each is composed of three files only) and greatly simplifies resource-servers configuration:
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<!-- replace "webmvc" with "weblux" if your app is reactive -->
<!-- replace "jwt" with "introspecting" to use token introspection instead of JWT decoding -->
<artifactId>spring-addons-webmvc-jwt-resource-server</artifactId>
<!-- this version is to be used with spring-boot 3.0.1, use 5.4.x for spring-boot 2.6.x or before -->
<version>6.0.13</version>
</dependency>
#Configuration
#EnableMethodSecurity
public static class WebSecurityConfig { }
com.c4-soft.springaddons.security.issuers[0].location=https://localhost:8443/realms/realm1
com.c4-soft.springaddons.security.issuers[0].authorities.claims=realm_access.roles,ressource_access.some-client.roles,ressource_access.other-client.roles
com.c4-soft.springaddons.security.cors[0].path=/some-api
com.c4-soft.springaddons.security.permit=all=/actuator/health/readiness,/actuator/health/liveness,/v3/api-docs/**
Nothing more is needed to configure a multi-tenant resource-server with fine tuned CORS policy and authorities mapping. Bootiful, isn't it?
By "multi-tenant", I mean that, as you can guess from this issuers property being an array, you can trust as many OIDC authorization-server instances as you need (multiple Keycloak realms & instances, or even mix with other OIDC providers like Auth0, Cognito, etc.), each with it's own authorities mapping configuration.
Client configuration (UI with oauth2Login())
If your Spring application exposes secured UI elements you want to be accessible with a browser (with OAuth2 login), you'll have to provide a FilterChain with "client" configuration.
If this app exposes both a REST API and a UI to manipulate it (with oauth2Login()), then you'll have to setup two security filter-chains: one with client config and the other with resource-server config.
Add this to pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
Here we demo a SecurityFilterChain applying only to a list of routes defined with a securityMatcher.
This assumes that an additional resource-server SecurityFilterChain is defined, with lower order and no securityMatcher so that all routes are intercepted after all filter chains are evaluated in order. This other filter chain could be defined either implicitly (by spring-addons as described above) or explicitly (with Spring Boot official starter as described below).
Remove the securityMatcher section if your app is solely a client:
// Give higher precedence to security filter-chains with "securityMatcher"
#Order(Ordered.HIGHEST_PRECEDENCE)
#Bean
SecurityFilterChain uiFilterChain(
HttpSecurity http,
ServerProperties serverProperties,
GrantedAuthoritiesMapper authoritiesMapper) throws Exception {
http.securityMatcher(new OrRequestMatcher(
// add path to your UI elements instead
new AntPathRequestMatcher("/ui/**"),
// those two are required to access Spring generated login page
// and OAuth2 client callback endpoints
new AntPathRequestMatcher("/login/**"),
new AntPathRequestMatcher("/oauth2/**")));
http.oauth2Login().userInfoEndpoint().userAuthoritiesMapper(authoritiesMapper);
http.authorizeHttpRequests()
.requestMatchers("/ui/index.html").permitAll()
.requestMatchers("/login/**").permitAll()
.requestMatchers("/oauth2/**").permitAll()
.anyRequest().authenticated();
// If SSL enabled, disable http (https only)
if (serverProperties.getSsl() != null && serverProperties.getSsl().isEnabled()) {
http.requiresChannel().anyRequest().requiresSecure();
}
// Many defaults are kept compared to API filter-chain:
// - sessions (and CSRF protection) are enabled
// - unauthorized requests to secured resources will be redirected to login (302 to login is Spring's default response when authorisation is missing or invalid)
return http.build();
}
#Bean
GrantedAuthoritiesMapper userAuthoritiesMapper(Converter<Map<String, Object>, Collection<? extends GrantedAuthority>> authoritiesConverter) {
return (authorities) -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
authorities.forEach(authority -> {
if (authority instanceof OidcUserAuthority oidcAuth) {
mappedAuthorities.addAll(authoritiesConverter.convert(oidcAuth.getIdToken().getClaims()));
} else if (authority instanceof OAuth2UserAuthority oauth2Auth) {
mappedAuthorities.addAll(authoritiesConverter.convert(oauth2Auth.getAttributes()));
}
});
return mappedAuthorities;
};
}
The code above assumes that a Converter<Map<String, Object>, Collection<? extends GrantedAuthority>> bean is exposed. One is auto-configured by spring-addons starters for resource-server and the "official" starter section below defines one. Take the later as sample if your app is solely a client.
Last, client properties:
spring.security.oauth2.client.provider.keycloak.issuer-uri=https://localhost:8443/realms/master
spring.security.oauth2.client.registration.spring-addons-public.provider=keycloak
spring.security.oauth2.client.registration.spring-addons-public.client-name=spring-addons-public
spring.security.oauth2.client.registration.spring-addons-public.client-id=spring-addons-public
spring.security.oauth2.client.registration.spring-addons-public.scope=openid,offline_access,profile
spring.security.oauth2.client.registration.spring-addons-public.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.spring-addons-public.redirect-uri=http://bravo-ch4mp:8080/login/oauth2/code/spring-addons-public
"Official" Spring Boot resource-server starter
As spring-addons-{webmvc|webflux}-{jwt|introspecting}-resource-server are thin wrappers around spring-boot-starter-oauth2-resource-server, you can of course do the same with just the later.
Here is what it takes to configure a resource-server with a unique Keycloak realm as authorization-server:
#Configuration
#EnableWebSecurity
#EnableMethodSecurity
public class WebSecurityConfig {
public interface Jwt2AuthoritiesConverter extends Converter<Jwt, Collection<? extends GrantedAuthority>> {
}
#SuppressWarnings("unchecked")
#Bean
public Jwt2AuthoritiesConverter authoritiesConverter() {
// This is a converter for roles as embedded in the JWT by a Keycloak server
// Roles are taken from both realm_access.roles & resource_access.{client}.roles
return jwt -> {
final var realmAccess = (Map<String, Object>) jwt.getClaims().getOrDefault("realm_access", Map.of());
final var realmRoles = (Collection<String>) realmAccess.getOrDefault("roles", List.of());
final var resourceAccess = (Map<String, Object>) jwt.getClaims().getOrDefault("resource_access", Map.of());
// We assume here you have "spring-addons-confidential" and "spring-addons-public" clients configured with "client roles" mapper in Keycloak
final var confidentialClientAccess = (Map<String, Object>) resourceAccess.getOrDefault("spring-addons-confidential", Map.of());
final var confidentialClientRoles = (Collection<String>) confidentialClientAccess.getOrDefault("roles", List.of());
final var publicClientAccess = (Map<String, Object>) resourceAccess.getOrDefault("spring-addons-public", Map.of());
final var publicClientRoles = (Collection<String>) publicClientAccess.getOrDefault("roles", List.of());
// Merge the 3 sources of roles and map it to spring-security authorities
return Stream.concat(
realmRoles.stream(),
Stream.concat(confidentialClientRoles.stream(), publicClientRoles.stream()))
.map(SimpleGrantedAuthority::new).toList();
};
}
// spring-boot looks for a Converter<Jwt, ? extends AbstractAuthenticationToken> bean
// that is a converter from Jwt to something extending AbstractAuthenticationToken (and not AbstractAuthenticationToken itself)
// In this conf, we use JwtAuthenticationToken as AbstractAuthenticationToken implementation
public interface Jwt2AuthenticationConverter extends Converter<Jwt, JwtAuthenticationToken> {
}
#Bean
public Jwt2AuthenticationConverter authenticationConverter(Jwt2AuthoritiesConverter authoritiesConverter) {
return jwt -> new JwtAuthenticationToken(jwt, authoritiesConverter.convert(jwt));
}
// Give lower precedence to security filter-chains without "securityMatcher" so that the filter-chains with a "securityMatcher" get a chance to be matched
#Order(Ordered.LOWEST_PRECEDENCE)
#Bean
public SecurityFilterChain apiFilterChain(
HttpSecurity http,
ServerProperties serverProperties,
Converter<Jwt, ? extends AbstractAuthenticationToken> authenticationConverter) throws Exception {
// Enable OAuth2 with custom authorities mapping
http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(authenticationConverter);
// As the authentication bean is the one expected by spring-boot,
// an alternative would be to use just
// http.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
// Enable anonymous
http.anonymous();
// Enable and configure CORS
http.cors().configurationSource(corsConfigurationSource());
// State-less session (state in access-token only)
// with Disable CSRF because of disabled sessions
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.csrf().disable();
// Return 401 (unauthorized) instead of 302 (redirect to login) when authorization is missing or invalid
http.exceptionHandling().authenticationEntryPoint((request, response, authException) -> {
response.addHeader(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"Restricted Content\"");
response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
});
// If SSL enabled, disable http (https only)
if (serverProperties.getSsl() != null && serverProperties.getSsl().isEnabled()) {
http.requiresChannel().anyRequest().requiresSecure();
}
// Route security: authenticated to all routes but actuator and Swagger-UI
http.authorizeRequests()
.antMatchers("/actuator/health/readiness", "/actuator/health/liveness", "/v3/api-docs/**").permitAll()
.anyRequest().authenticated();
return http.build();
}
private CorsConfigurationSource corsConfigurationSource() {
// Very permissive CORS config...
final var configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("*"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setExposedHeaders(Arrays.asList("*"));
// Limited to API routes (neither actuator nor Swagger-UI)
final var source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/greet/**", configuration);
return source;
}
}
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://localhost:8443/realms/master
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://localhost:8443/realms/master/protocol/openid-connect/certs
As mentioned in preamble, this is quite more verbose than spring-addons starters, it's not ready for multi-tenancy and each time CORS policy changes (new API routes for instance) or when the claims source for authorities change (new OAuth2 client with client-roles mapping or other OIDC provider than Keycloak), you'll have to edit source-code and re-publish your app...
Use the standard Spring Security OAuth2 client instead of a specific Keycloak adapter and SecurityFilterChain instead of WebSecurityAdapter.
Something like this:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(jsr250Enabled = true, prePostEnabled = true)
class OAuth2SecurityConfig {
#Bean
fun customOauth2FilterChain(http: HttpSecurity): SecurityFilterChain {
log.info("Configure HttpSecurity with OAuth2")
http {
oauth2ResourceServer {
jwt { jwtAuthenticationConverter = CustomBearerJwtAuthenticationConverter() }
}
oauth2Login {}
csrf { disable() }
authorizeRequests {
// Kubernetes
authorize("/readiness", permitAll)
authorize("/liveness", permitAll)
authorize("/actuator/health/**", permitAll)
// ...
// everything else needs at least a valid login, roles are checked at method level
authorize(anyRequest, authenticated)
}
}
return http.build()
}
And then in application.yml:
spring:
security:
oauth2:
client:
provider:
abc:
issuer-uri: https://keycloak.../auth/realms/foo
registration:
abc:
client-secret: ...
provider: abc
client-id: foo
scope: [ openid, profile, email ]
resourceserver:
jwt:
issuer-uri: https://keycloak.../auth/realms/foo
Using Keycloak adapters is not possible because the KeycloakWebSecurityConfigurerAdapter inherited from the WebSecurityConfigurerAdapter class, which was deprecated in Spring Security and subsequently removed in the newer release.
I have published a detailed article on integrating Keycloak with Spring Boot 3.0 on Medium, which provides a step-by-step guide on how to integrate Keycloak with Spring Boot 3.0.
This guide is particularly helpful for those who are new to integrating Keycloak with Spring Boot 3.0 or migrating to Spring Boot 3.0 from an older version.
You can check out the article (https://medium.com/geekculture/using-keycloak-with-spring-boot-3-0-376fa9f60e0b) for a comprehensive explanation of the integration process.
Hope this helps! If you have any questions, further clarifications or suggestions, Please feel free to leave a comment.
I'm building a standard spring boot mvc + thymeleaf + Oauth2. So far I added the oauth2-client lib for authentication purpose but now I need authorization to protect my app.
Does it mean I also need to configure it as a resource server?
If not, how can I map authorities from the private claim (in the ID token) my authorization-server puts roles into?
Resource-server configuration is adapted to secure REST resources.
Client configuration is just fine unless you expose your API. If so, add a second security filter-chain. Details in this other answer: Use Keycloak Spring Adapter with Spring Boot 3
Authorities mapping in OAuth2 client
As mentioned in my comment, Spring-security documentation is always better than whatever one on Stackoverflow could write (but Spring-security team members of course, who happen to post answers)
Double check the claim in which your authorization-server puts user roles into and provide an authorities mapper, either:
explicitly with http.oauth2Login().userInfoEndpoint().userAuthoritiesMapper(userAuthoritiesMapper());
as a #Bean of type GrantedAuthoritiesMapper which should be auto-configured by spring-boot
In both cases, the code for the mapper is the same (double check the name of the claim for user roles with your authorization-server, but it might be groups with Azure AD):
#Bean
GrantedAuthoritiesMapper userAuthoritiesMapper() {
return (authorities) -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
authorities.forEach(authority -> {
if (authority instanceof OidcUserAuthority oidcAuth) {
oidcAuth.getIdToken().getClaimAsStringList("groups").forEach(a -> mappedAuthorities.add(new SimpleGrantedAuthority(a)));
} else if (authority instanceof OAuth2UserAuthority oauth2Auth) {
((List<String>) oauth2Auth.getAttributes().getOrDefault("groups", List.of())).forEach(a -> mappedAuthorities.add(new SimpleGrantedAuthority(a)));
}
});
return mappedAuthorities;
};
}
I have a Spring (not Spring Boot) REST API that is secured using Spring Security. Many endpoints require authentication in the form of a JWT before they can be accessed. Often the principal is fetched to access information from the token:
CustomObject customObject = (CustomObject) SecurityContextHolder
.getContext()
.getAuthentication()
.getPrincipal();
I'm attempting to change the REST API to accept Keycloak tokens. Following the Keycloak documentation I've added the keycloak-spring-security-adapter dependency, setup a local Keycloak instance using Docker for development and added a keycloak.json to the API project. Everything seems to work, the API accepts an access token in the Authorization header of requests. However, when attempting to access information from the token an exception occurs because the KeycloakPrincipal class can't be cast to our CustomObject class.
I don't want to go through the whole project and change all casts when getting the principal from CustomObject to KeycloakPrincipal as that would be a significant amount of work. Besides, using the KeycloakPrincipal object makes our code implementation specific (Keycloak in this case), what if we want to move to a different token provider.
Is it possible to change the default KeycloakPrincipal set on the security context to a custom object so the above code for getting the principal still works? If so, what would be the best way to do that, through a Spring filter maybe?
Do not use Keycloak adapters, it is very deprecated.
I strongly recommend you take the time to read this set of 3 tutorials which end with exactly what you want: configure spring-security to populate security-context with a custom Authentication implementation containing data retrieved from a JWT access-token (issued by Keycloak or whatever).
Bad news is it based on spring-boot. Good news is everything is open-source and you can inspect any #Bean configured.
If you don't have spring-boot, you'll have a little more work to provide your application context with a SecurityFilterChain bean for an OAuth2 resource-server with a JWT decoder. I let you refer to the doc to have one defined in your conf.
Once you have it, defining your own Converter<Jwt, ? extends AbstractAuthenticationToken> on resource-server JWT decoder configurer should be enough:
interface ClaimsToAuthoritiesConverter extends Converter<Map<String, Object>, Collection<? extends GrantedAuthority>> {}
#Bean
ClaimsToAuthoritiesConverter authoritiesConverter() {
return (Map<String, Object> claims) -> {
// Override this with the actual private-claim(s) your authorization-server puts roles into
// like resource_access.some-client.roles or whatever
final var realmAccessClaim = (Map<String, Object>) claims.getOrDefault("realm_access", Map.of());
final var rolesClaim = (Collection<String>) realmAccessClaim.getOrDefault("roles", List.of());
return rolesClaim.stream().map(SimpleGrantedAuthority::new).toList();
};
}
#Bean
SecurityFilterChain filterChain(HttpSecurity http, Converter<Map<String, Object>, Collection<? extends GrantedAuthority>> authoritiesConverter) throws Exception {
http.oauth2ResourceServer().jwt()
// omitted regular JWT decoder configuration like issuer, jwk-set-uri, etc.
.jwtAuthenticationConverter(jwt -> new YourOwnAuthentication(jwt.getTokenValue(), jwt.getClaims(), authoritiesConverter));
// Some more security conf like CORS etc.
return http.build();
}
This converter from Jwt to your own implementation of AbstractAuthenticationToken is called after the JWT access-token was sucessfully decoded and validated. In other words, it is pretty safe to interfere at this stage, it is just a matter of formating valid authentication data in the most convenient way for your business code and security rules.
Tips for designing YourOwnAuthentication:
public class YourOwnAuthentication extends AbstractAuthenticationToken {
private final CustomObject principal;
private final String bearerString;
public YourOwnAuthentication (String bearerString, Map<String, Object> claims, Converter<Map<String, Object>, Collection<? extends GrantedAuthority>> authoritiesConverter) {
super(authoritiesConverter.convert(claims));
super.setAuthenticated(true);
this.bearerString = bearerString;
this.principal = new CustomObject(claims); // I connot figure out how you'll actually build that
}
#Override
public String getCredentials() {
return bearerString;
}
#Override
public CustomObject getPrincipal() {
return principal;
}
public String getBearerString() {
return bearerString;
}
}
Side note: in addition to "manually" access your CustomObject instance from security-context as you do in your question you'll also be able to access it with spring "magic" authentication parameters for #Controller methods:
#PreAuthorise("isAuthenticated()") ResponseEntity<?> controllerMethod(YourOwnAuthentication auth) and then declare CustomObject customObject = auth.getPrincipal(); (note there is no cast here)
#PreAuthorise("isAuthenticated()") ResponseEntity<?> controllerMethod(#AuthenticationPrincipal CustomObject customObject)
Last, a production ready authorities converter here
I have a Spring Boot + Keycloak project and I found out that the Spring Boot does not validate the JWT with the keycloak. For example if I get a token from Keycloak and turn off the Keycloak, I still can use this JWT token to access my end points. I have this security configurer class:
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
#RequiredArgsConstructor
public class KeycloakSecurityConfigurer extends WebSecurityConfigurerAdapter {
private final RoleConverter converter;
#Value("${spring.security.oauth2.keycloak.jwt.issuer-uri}")
private String issuerUri;
#Override
public void configure(final HttpSecurity http) throws Exception {
http.headers().frameOptions().disable()
.and()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.oauth2ResourceServer(
oauth2ResourceServer -> oauth2ResourceServer.jwt(
jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())));
http.authorizeRequests().antMatchers("/**").authenticated();
}
private Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter() {
JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter();
jwtConverter.setJwtGrantedAuthoritiesConverter(converter);
return jwtConverter;
}
#Bean
public JwtDecoder jwtDecoder() {
return JwtDecoders.fromOidcIssuerLocation(issuerUri);
}
}
The "converter" is nothing special, just extracts the roles out of JWT token and returns a list of them.
How to force the Spring Security to validate the JWT token?
application.yml:
spring:
security:
oauth2:
keycloak:
jwt:
issuer-uri: http://localhost:8180/auth/realms/test-realm
You can look at the implementation of JwtDecoders.fromOidcIssuerLocation(issuerUri).
What is happening is that the keys are being fetched at the startup of your application and the application caches them in order to perform the validation after. With this in mind, even if you turn off Keycloak the JWT will still be validated because the keys are still cached.
The JWT tokens are been cached in your springboot application, this is the default cache store. In order to delete this token from your springboot app use should use some custom caches like redis cache to be configured in your app instead of default. There is no possible way to delete the tokens stored in default caches. The token will automatically get invalidate only after the timeout that's been set inside token
JWTs are meant to be validated offline, and it is what usually happens. The receiving application (consumer of JWT), does not need constant access to the Authorization Server in order to be able to validate a JWT. Even though Spring can't talk to your Keycloak, it does not mean that tokens are not validated. As others pointed out, Spring caches the keys used to validate JWTs' signature and will use the cache if it can.
If, for some reason, you want your service / API to validate the JWT online (maybe because you want to implement a mechanism to revoke tokens), you could switch to using opaque tokens with Token Introspection. On every request your service will have to call Keycloak to exchange the opaque token for a JWT. Mind that this solution will use much more resources, and you should use it only if you have strong reasons for it.
i have a route that needs to authenticated for more than one user. performing integration testing on spring cloud gateway service to test all the routes security working as expected or not. how can i add more than 1 user role to single pathMatcher/route?
Using Spring Boot 2.1.6, Spring Cloud Finchely.SR2, Spring Cloud Gateway, Spring WebFlux Security(Reactive Spring Security)
#EnableWebFluxSecurity
public class SecurityConfig {
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.csrf().disable()
.formLogin().disable()
.logout().disable()
.authorizeExchange()
.pathMatchers(prefix + "/publish/**")
.hasRole("XYZ_ROLE") //Here i want to add more than one user role
.anyExchange()
.authenticated().and().httpBasic();
}
}
EDIT:
After looking through the spring security source code and github issues i found out that hasAnyRole and hasAnyAuthority has been implemented for spring security for webflux and is planned to be released in Spring security v5.2.0.
As of writing current stable version is 5.1.6 but the 5.2.0 is in milestone 4 so it should be released very soon. You can use the snapshot version of 5.2.0 if needed.
The only other current option if not using the snapshot is to implement your own custom ReactiveAuthorizationManager and use the ServerHttpSecurity.AuthorizeExchangeSpec#access function.
OLD ANSWER ONLY APPLICABLE TO STANDARD SPRING SECURITY NOT WEBFLUX:
You can try out the hasAnyRole(String... roles)
after digging the rabbit hole, i found solution to Authorize for multiple roles using Reactive Spring Security. find the solution below:
public class SecurityConfig {
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.csrf().disable()
.formLogin().disable()
.logout().disable()
.authorizeExchange()
.pathMatchers(prefix + "/publish/**").access((mono, context) -> mono
.map(auth -> auth.getAuthorities().stream()
//Authorizing for multiple user roles
.filter(e -> (e.getAuthority().equals("ROLE_ABC") || e.getAuthority().equals("ROLE_XYZ")))
.count() > 0)
.map(AuthorizationDecision::new))
.anyExchange()
.authenticated().and().httpBasic();
}
}```