javaspring-bootoauth-2.0keycloak

SpringBoot OAuth2 with Keycloak not returning mapped Roles as Authorities


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?


Solution

  • 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

    Realm roles configuration

    Then, in your Spring Security configuration, define a GrantedAuthoritiesMapper which extracts the roles from the UserInfo endpoint and maps them to GrantedAuthoritys. 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 GrantedAuthoritys. 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;
    }