The default principal created for an OAuth2 resource server opaque token does not include the token scopes in the authorities. This makes tags such as @PreAuthorize("hasAuthority('SCOPE_myscope')") always fail.

To Reproduce: Set up an OAuth2 resource server authenticating based on opaque tokens:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  http.oauth2ResourceServer(resourceServer -> resourceServer
    .opaqueToken(token -> token
        .introspectionClientCredentials(clientName,clientSecret)
        .introspectionUri(authorizationServerIntrospectionUri)
    )
  );
  return http.build();
}

Set up a controller that logs the scopes and the authorities:

@GetMapping("/api/test")
public OAuth2IntrospectionAuthenticatedPrincipal test(@AuthenticationPrincipal OAuth2IntrospectionAuthenticatedPrincipal principal) {
  logger.info("Principal scopes are: {}",principal.getScopes());
  logger.info("Principal authorities are: {}",principal.getAuthorities);
  return principal;
}

The result is that no matter what the scopes are, the authorities are empty. It is expected according to the documentation that all scopes be added as authorities in the format of SCOPE_myscope.

From the source code, it appears that the issue is in the extraction of authorities from scopes in the default authentication converter: https://github.com/spring-projects/spring-security/blob/main/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/SpringOpaqueTokenIntrospector.java#L254-L269

scopes is never an instance of ArrayListFromString so this check if (!(scopes instanceof ArrayListFromString)) is failing, causing it always to return an empty list.

Comment From: jzheaux

Thanks for the report, @jdachman. This is a duplicate of https://github.com/spring-projects/spring-security/issues/15165 and will go out in the next milestone release.