Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Security: mapping OAuth2 claims with roles to secure Resource Server endpoints

I'm setting up a Resource Server with Spring Boot and to secure the endpoints I'm using OAuth2 provided by Spring Security. So I'm using the Spring Boot 2.1.8.RELEASE which for instance uses Spring Security 5.1.6.RELEASE.

As Authorization Server I'm using Keycloak. All processes between authentication, issuing access tokens and validation of the tokens in the Resource Server are working correctly. Here is an example of an issued and decoded token (with some parts are cut):

{   "jti": "5df54cac-8b06-4d36-b642-186bbd647fbf",   "exp": 1570048999,   "aud": [     "myservice",     "account"   ],   "azp": "myservice",   "realm_access": {     "roles": [       "offline_access",       "uma_authorization"     ]   },   "resource_access": {     "myservice": {       "roles": [         "ROLE_user",         "ROLE_admin"       ]     },     "account": {       "roles": [         "manage-account",         "manage-account-links",         "view-profile"       ]     }   },   "scope": "openid email offline_access microprofile-jwt profile address phone", }  

How can I configure Spring Security to use the information in the access token to provide conditional authorization for different endpoints?

Ultimately I want to write a controller like this:

@RestController public class Controller {      @Secured("ROLE_user")     @GetMapping("userinfo")     public String userinfo() {         return "not too sensitive action";     }      @Secured("ROLE_admin")     @GetMapping("administration")     public String administration() {         return "TOOOO sensitive action";     } } 
like image 675
rigon Avatar asked Oct 02 '19 16:10

rigon


2 Answers

After messing around a bit more, I was able to find a solution implementing a custom jwtAuthenticationConverter, which is able to append resource-specific roles to the authorities collection.

    http.oauth2ResourceServer()                 .jwt()                 .jwtAuthenticationConverter(new JwtAuthenticationConverter()                 {                     @Override                     protected Collection<GrantedAuthority> extractAuthorities(final Jwt jwt)                     {                         Collection<GrantedAuthority> authorities = super.extractAuthorities(jwt);                         Map<String, Object> resourceAccess = jwt.getClaim("resource_access");                         Map<String, Object> resource = null;                         Collection<String> resourceRoles = null;                         if (resourceAccess != null &&                             (resource = (Map<String, Object>) resourceAccess.get("my-resource-id")) !=                             null && (resourceRoles = (Collection<String>) resource.get("roles")) != null)                             authorities.addAll(resourceRoles.stream()                                                             .map(x -> new SimpleGrantedAuthority("ROLE_" + x))                                                             .collect(Collectors.toSet()));                         return authorities;                     }                 }); 

Where my-resource-id is both the resource identifier as it appears in the resource_access claim and the value associated to the API in the ResourceServerSecurityConfigurer.

Notice that extractAuthorities is actually deprecated, so a more future-proof solution should be implementing a full-fledged converter

    import org.springframework.core.convert.converter.Converter;     import org.springframework.security.authentication.AbstractAuthenticationToken;     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.JwtAuthenticationToken;     import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;      import java.util.Collection;     import java.util.Collections;     import java.util.Map;     import java.util.stream.Collectors;     import java.util.stream.Stream;      public class CustomJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken>     {         private static Collection<? extends GrantedAuthority> extractResourceRoles(final Jwt jwt, final String resourceId)         {             Map<String, Object> resourceAccess = jwt.getClaim("resource_access");             Map<String, Object> resource;             Collection<String> resourceRoles;             if (resourceAccess != null && (resource = (Map<String, Object>) resourceAccess.get(resourceId)) != null &&                 (resourceRoles = (Collection<String>) resource.get("roles")) != null)                 return resourceRoles.stream()                                     .map(x -> new SimpleGrantedAuthority("ROLE_" + x))                                     .collect(Collectors.toSet());             return Collections.emptySet();         }          private final JwtGrantedAuthoritiesConverter defaultGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();          private final String resourceId;          public CustomJwtAuthenticationConverter(String resourceId)         {             this.resourceId = resourceId;         }          @Override         public AbstractAuthenticationToken convert(final Jwt source)         {             Collection<GrantedAuthority> authorities = Stream.concat(defaultGrantedAuthoritiesConverter.convert(source)                                                                                                        .stream(),                                                                      extractResourceRoles(source, resourceId).stream())                                                              .collect(Collectors.toSet());             return new JwtAuthenticationToken(source, authorities);         }     } 

I have tested both solutions using Spring Boot 2.1.9.RELEASE, Spring Security 5.2.0.RELEASE and an official Keycloak 7.0.0 Docker image.

Generally speaking, I suppose that whatever the actual Authorization Server (i.e. IdentityServer4, Keycloak...) this seems to be the proper place to convert claims into Spring Security grants.

like image 133
BladeWise Avatar answered Oct 07 '22 17:10

BladeWise


Here is another solution

    private JwtAuthenticationConverter jwtAuthenticationConverter() {         JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();         jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("roles");         jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");         JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();         jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);         return jwtAuthenticationConverter;     }      @Override     protected void configure(HttpSecurity httpSecurity) throws Exception {         httpSecurity                 .authorizeRequests()                 .anyRequest().authenticated()                 .and()                 .oauth2ResourceServer().jwt()                 .jwtAuthenticationConverter(jwtAuthenticationConverter());     } 
like image 27
hillel_guy Avatar answered Oct 07 '22 19:10

hillel_guy