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"; } }
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.
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()); }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With