Reputation: 12405
I am creating a simple SpringBoot application and trying to integrate with OAuth 2.0 provider Keycloak. I have created a realm, client, roles (Member, PremiumMember) at realm level and finally created users and assigned roles (Member, PremiumMember).
If I use SpringBoot Adapter provided by Keycloak https://www.keycloak.org/docs/latest/securing_apps/index.html#_spring_boot_adapter then when I successfully login and check the Authorities of the loggedin user I am able to see the assigned roles such as Member, PremiumMember.
Collection<? extends GrantedAuthority> authorities =
SecurityContextHolder.getContext().getAuthentication().getAuthorities();
But if I use generic SpringBoot Auth2 Client Config I am able to login but when I check the Authorities it always show only ROLE_USER, SCOPE_email,SCOPE_openid,SCOPE_profile and didn't include the roles I mapped (Member, PremiumMember).
My SpringBoot OAuth2 config:
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
application.properties
spring.security.oauth2.client.provider.spring-boot-thymeleaf-client.issuer-uri=http://localhost:8181/auth/realms/myrealm
spring.security.oauth2.client.registration.spring-boot-thymeleaf-client.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.spring-boot-thymeleaf-client.client-id=spring-boot-app
spring.security.oauth2.client.registration.spring-boot-thymeleaf-client.client-secret=XXXXXXXXXXXXXX
spring.security.oauth2.client.registration.spring-boot-thymeleaf-client.scope=openid,profile,roles
spring.security.oauth2.client.registration.spring-boot-thymeleaf-client.redirect-uri=http://localhost:8080/login/oauth2/code/spring-boot-app
I am using SpringBoot 2.5.5 and Keycloak 15.0.2.
Using this generic OAuth2.0 config approach (without using Keycloak SpringBootAdapter) is there a way to get the assigned roles?
Upvotes: 10
Views: 13608
Reputation: 157
If you cannot or don't want to configure Keycloak you can also implement a customOidcUserService
which allows you to fetch authority information from a protected resource before the custom authorities for the user get mapped.
See https://docs.spring.io/spring-security/reference/servlet/oauth2/login/advanced.html#oauth2login-advanced-map-authorities-oauth2userservice
Upvotes: 0
Reputation: 681
You have another option if you do not want to customize Spring that much. According to the Spring documentation, you can change authority claim name and prefix.
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
grantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
With that, Spring expects the roles as a top level claim. To do so, you have to add client specific mapper on the Keycloak client, so that the realm roles will be sent additional on top level.
In my case, I added the mapper as a client specific one. But you should be able to set this as a default for a realm.
Upvotes: 0
Reputation: 1808
By default, Spring Security generates a list of GrantedAuthority
using the values in the scope
or scp
claim and the SCOPE_
prefix.
Keycloak keeps the realm roles in a nested claim realm_access.roles
. You have two options to extract the roles and map them to a list of GrantedAuthority
.
OAuth2 Client
If your application is configured as an OAuth2 Client, then you can extract the roles from either the ID Token or the UserInfo endpoint. Keycloak includes the roles only in the Access Token, so you need to change the configuration to include them also in either the ID Token or the UserInfo endpoint (which is what I use in the following example). You can do so from the Keycloak Admin Console, going to Client Scopes > roles > Mappers > realm roles
Then, in your Spring Security configuration, define a GrantedAuthoritiesMapper
which extracts the roles from the UserInfo endpoint and maps them to GrantedAuthority
s. Here, I'll include how the specific bean should look like. A full example is available on my GitHub: https://github.com/ThomasVitale/spring-security-examples/tree/main/oauth2/login-user-authorities
@Bean
public GrantedAuthoritiesMapper userAuthoritiesMapperForKeycloak() {
return authorities -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
var authority = authorities.iterator().next();
boolean isOidc = authority instanceof OidcUserAuthority;
if (isOidc) {
var oidcUserAuthority = (OidcUserAuthority) authority;
var userInfo = oidcUserAuthority.getUserInfo();
if (userInfo.hasClaim("realm_access")) {
var realmAccess = userInfo.getClaimAsMap("realm_access");
var roles = (Collection<String>) realmAccess.get("roles");
mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
}
} else {
var oauth2UserAuthority = (OAuth2UserAuthority) authority;
Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();
if (userAttributes.containsKey("realm_access")) {
var realmAccess = (Map<String,Object>) userAttributes.get("realm_access");
var roles = (Collection<String>) realmAccess.get("roles");
mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
}
}
return mappedAuthorities;
};
}
Collection<GrantedAuthority> generateAuthoritiesFromClaim(Collection<String> roles) {
return roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toList());
}
OAuth2 Resource Server
If your application is configured as an OAuth2 Resource Server, then you can extract the roles from the Access Token. In your Spring Security configuration, define a JwtAuthenticationConverter
bean which extracts the roles from the Access Token and maps them to GrantedAuthority
s. Here, I'll include how the specific bean should look like. A full example is available on my GitHub: https://github.com/ThomasVitale/spring-security-examples/tree/main/oauth2/resource-server-jwt-authorities
public JwtAuthenticationConverter jwtAuthenticationConverterForKeycloak() {
Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter = jwt -> {
Map<String, Collection<String>> realmAccess = jwt.getClaim("realm_access");
Collection<String> roles = realmAccess.get("roles");
return roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toList());
};
var jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
Upvotes: 36
Reputation: 26878
I use this configuration:
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// We can safely disable CSRF protection on the REST API because we do not rely on cookies (https://security.stackexchange.com/questions/166724/should-i-use-csrf-protection-on-rest-api-endpoints)
http.csrf(httpSecurityCsrfConfigurer -> httpSecurityCsrfConfigurer.ignoringAntMatchers("/api/**"));
http.cors();
http.authorizeRequests(registry -> {
registry.mvcMatchers("/api-docs/**", "/architecture-docs/**").permitAll();
registry.mvcMatchers("/api/integrationtest/**").permitAll();
registry.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll();
registry.mvcMatchers("/actuator/info", "/actuator/health").permitAll();
registry.anyRequest().authenticated();
});
http.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(jwtAuthenticationConverter());
}
@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
public AuthenticationManager authenticationManagerBean() throws Exception {
// Although this seems like useless code,
// it is required to prevent Spring Boot creating a default password
return super.authenticationManagerBean();
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(jwtToAuthorityConverter());
return converter;
}
@Bean
public Converter<Jwt, Collection<GrantedAuthority>> jwtToAuthorityConverter() {
return new Converter<Jwt, Collection<GrantedAuthority>>() {
@Override
public List<GrantedAuthority> convert(Jwt jwt) {
Map<String, Object> realmAccess = jwt.getClaimAsMap("realm_access");
if (realmAccess != null) {
@SuppressWarnings("unchecked")
List<String> roles = (List<String>) realmAccess.get("roles");
if (roles != null) {
return roles.stream()
.map(rn -> new SimpleGrantedAuthority("ROLE_" + rn))
.collect(Collectors.toList());
}
}
return Collections.emptyList();
}
};
}
}
With these dependencies:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
And this property:
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8181/auth/realms/myrealm
Extra tip: Use https://github.com/ch4mpy/spring-addons for testing. You can also take a look there at a configuration sample (which is different from what I do, but should work fine as well, see https://github.com/ch4mpy/spring-addons/issues/27 for more info about those differences): https://github.com/ch4mpy/starter/tree/master/api/webmvc/common-security-webmvc/src/main/java/com/c4_soft/commons/security
Upvotes: 1