Summary:

this is not a bug for sping, but for : https://bitbucket.org/connect2id/oauth-2.0-sdk-with-openid-connect-extensions/src/master/

hi, I use this maven package to combine spring oauth2 with opaque token,

the spring security oauth2 return json response with scope property as json array, but TokenIntrospectionSuccessResponse getScope() method read this property as string, which lead to scope and authorities to be null,

Actual Behavior:

the properties: scope and authorities from NimbusReactiveOpaqueTokenIntrospector is null

Expected Behavior:

the properties: scope and authorities from NimbusReactiveOpaqueTokenIntrospector is normal, here should be: scope: all, authorities: ROLE_USER

Configuration

Version

spring security oauth version: spring-boot-starter-oauth2-resource-server: 2.2.3.RELEASE spring-boot-starter-security: 2.2.3.RELEASE spring-cloud-starter-gateway: 2.2.3.RELEASE oauth2-oidc-sdk: 8.28.1

Sample

    #https://bitbucket.org/connect2id/oauth-2.0-sdk-with-openid-connect-extensions/src/7da4c0b7f9ad10ea08e4ed970d527f7ec37d67f0/src/main/java/com/nimbusds/oauth2/sdk/TokenIntrospectionSuccessResponse.java#lines-385

    public Scope getScope() {

        try {
            return Scope.parse(JSONObjectUtils.getString(params, "scope"));
        } catch (ParseException e) {
            return null;
        }
    }

    #https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/common/OAuth2AccessToken.java#L73
    #spring return scope as array:
    Set<String> getScope();

1

Comment From: jzheaux

Thanks for reaching out, @imwower. I believe the introspection RFC states that scope is a string:

A JSON string containing a space-separated list of scopes associated with this token, in the format described in Section 3.3 of OAuth 2.0 [RFC6749].

So, the best solution is likely to change the authorization server so that it follows the spec.

If that's not possible, you can instead configure RestTemplate to adapt your response:

@Bean
OpaqueTokenIntrospector introspector() {
    FormHttpMessageConverter form = new FormHttpMessageConverter();
    ScopeArrayHttpMessageConverter scopeArray = new ScopeArrayHttpMessageConverter();
    List<HttpMessageConverter<?>> converters = Arrays.asList(form, scopeArray);
    RestTemplate rest = new RestTemplate(converters);
    rest.getInterceptors().add(new BasicAuthenticationInterceptor(clientId, clientSecret));
    return new NimbusOpaqueTokenIntrospector(introspectionUri, rest);
}

private static class ScopeArrayHttpMessageConverter extends StringHttpMessageConverter {
    private final ObjectMapper mapper = new ObjectMapper();
    private final HttpMessageConverter<Object> jackson = new MappingJackson2HttpMessageConverter(mapper);

    @Override
    public String readInternal(Class<? extends String> clazz, HttpInputMessage inputMessage)
            throws IOException, HttpMessageNotReadableException {
        Map<String, Object> claims = (Map<String, Object>) jackson.read(Map.class, inputMessage);
        claims.computeIfPresent("scope", (key, value) -> ((Collection<String>) value)
                .stream().map(Objects::toString).collect(Collectors.joining(" ")));
        return mapper.writeValueAsString(claims);
    }
}

I'm going to close this issue as answered, but feel free to reopen if you feel there's more to discuss.