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.