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.