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();
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.