Role-based authentication for OAuth2
In addition to bearer token authentication, I am also trying to validate user roles/authorities to ensure that they are permitted to access this resource. Here is what I have so far:
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Value("${oauth.enabled}")
boolean oauthEnabled;
@Value("${spring.security.oauth2.resourceserver.opaque.introspection-uri}")
String introspectionUri;
@Value("${spring.security.oauth2.resourceserver.opaque.introspection-client-id}")
String clientId;
@Value("${spring.security.oauth2.resourceserver.opaque.introspection-client-secret}")
String clientSecret;
@Override
public void configure(final HttpSecurity http) throws Exception {
if (oauthEnabled) {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().cors()
.and()
.authorizeRequests(urlRegistry -> urlRegistry
.antMatchers("/api/health").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(resourceServer -> resourceServer
.opaqueToken(opaqueToken -> opaqueToken
.introspectionUri(this.introspectionUri)
.introspectionClientCredentials(this.clientId, this.clientSecret)
)
);
}
@Slf4j
@RestController
@RequestMapping("/api")
@ControllerAdvice
@CrossOrigin(originPatterns = "*")
public class InventoryCountdownController extends BaseController {
@GetMapping("/icd")
//@PreAuthorize("permitAll()")
@PreAuthorize("hasAuthority('SOME_USER_ROLE')")
public ResponseEntity<List<Countdown>> getIcd(@RequestParam(value = "val") String val) {
...
}
The problem that I am running into is that I am getting back "Unexpected error: Access is denied". When I replace the "hasAuthority" annotation with @PreAuthorize("permitAll()")
, it seems to work fine. What am I missing?
As per https://github.com/spring-projects/spring-security/wiki/OAuth-2.0-Migration-Guide, I am using org.springframework.boot:spring-boot-starter-oauth2-resource-server
to implement my resource server.
Solution 1:
The default behaviour is to populate the authorities based on the "scope"
attribute that is typically included in the response from the introspection endpoint.
For example, if the introspection endpoint responds with { …, "scope" : "messages"}
then the authority list will be ["SCOPE_messages"]
.
You can customise this using a custom OpaqueTokenIntrospector
and exposing it as a bean.
@Bean
public OpaqueTokenIntrospector introspector() {
return new CustomAuthoritiesOpaqueTokenIntrospector();
}
where CustomAuthoritiesOpaqueTokenIntrospector
will look similar to this
public class CustomAuthoritiesOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
private OpaqueTokenIntrospector delegate =
new NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
public OAuth2AuthenticatedPrincipal introspect(String token) {
OAuth2AuthenticatedPrincipal principal = this.delegate.introspect(token);
return new DefaultOAuth2AuthenticatedPrincipal(
principal.getName(), principal.getAttributes(), extractAuthorities(principal));
}
private Collection<GrantedAuthority> extractAuthorities(OAuth2AuthenticatedPrincipal principal) {
List<String> scopes = principal.getAttribute(OAuth2IntrospectionClaimNames.SCOPE);
return scopes.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
}
This is all described in the Spring Security reference documentation.