Summary
This is not a bug on our side, but I believe it's worth noticing it. I created an issue in the com.nimbusds:oauth2-oidc-sdk repo:
https://bitbucket.org/connect2id/oauth-2.0-sdk-with-openid-connect-extensions/issues/278
When I set up a Resource Server to use opaque tokens / introspection endpoint, it uses a NimbusOpaqueTokenIntrospector instance.
The NimbusOpaqueTokenIntrospector#convertClaimsSet step first creates the claims by parsing the response using the toJSONObject method.
If the response has a “scope” field with an array of _Strings, it gets mapped to a JSONArray then.
Then, it calls TokenIntrospectionSuccessResponse#getScope method, which parses the "scope" param value as a String
If we have a “scope” param with a JSONArray instance value, then the parsing process fails (since it’s not a String), and the 'scope' claim receives a null value.
This happens, for example, if we use the Spring Security OAuth2 Authorization Server to issue and check tokens.
Actual Behavior
scope param doesn't get mapped to OAuth2AuthenticatedPrincipal claim when it is sent as a JSON array.
Expected Behavior
scope param should get mapped to OAuth2AuthenticatedPrincipal claim, wither if it is sent as a String or as an array of Strings in the response.
Configuration
Authorization Server using spring-security-oauth2-autoconfigure v2.2.0.RELEASE, using Spring Boot
Resource server Spring Security v2.2.0.RELEASE, using Spring Boot, and based mainly on this configuration
Version
v2.2.0.RELEASE
Comment From: jzheaux
Thanks for the report, @rozagerardo.
It may come down to what the Introspection spec says, which is that scope is expected to be a space-delimited string. We'll see how the Nimbus team responds to your request.
One thing that could be done to simplify the customization of parsing the response would be to change NimbusOpaqueTokenIntrospector to internally use an HttpMessageConverter, like how it's done in DefaultClientCredentialsTokenResponseClient along with several other clients in Spring Security.
Then, you could wire a custom RestOperations:
RestTemplate rest = new RestTemplate(Arrays.asList(myResponseConverter()));
rest.getInterceptors().add(new BasicAuthenticationInterceptor(clientId, clientSecret));
return new NimbusOpaqueTokenIntrospector(uri, rest);
I suppose let's see how the NImbus team responds first since upgrading a dependency would obviously be a nice solution.
Comment From: rozagerardo
Cool, you're right @jzheaux , I got carried away and forgot to verify what is actually defined in the specs.
I guess it would be better that the Spring Security OAuth Authorization Server complies with the specifications; I just realized we even have an issue for that (#1558).
But, of course, I believe a change in the API at this point is probably unlikely to happen at this point, what do you think? Otherwise, I could try to work on that in my free time.
Anyway, thank you very much for the help and the suggestion, I'll give it a shot :)
Comment From: rozagerardo
An update here, the issue for the enhancement in the Nimbus repo team got rejected. As a side note until ( / if) the issue gets fixed in the Spring Security OAuth library, another possible solution for this is following the one mentioned in the docs to 'Extract Authorities Manually', implementing our own Introspector:
@Component
public class CustomAuthoritiesOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
private OpaqueTokenIntrospector delegate;
public CustomAuthoritiesOpaqueTokenIntrospector(
@Value("${spring.security.oauth2.resourceserver.opaque-token.introspection-uri}") String introspectionUri,
@Value("${spring.security.oauth2.resourceserver.opaque-token.client-id}") String clientId,
@Value("${spring.security.oauth2.resourceserver.opaque-token.client-secret}") String clientSecret) {
delegate = new NimbusOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret);
}
@Override
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(scope -> "SCOPE_" + scope)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
}
Comment From: jzheaux
@rozagerardo I like your solution much better than the one I was suggesting. :)
I've added a test that confirms that a scope that doesn't conform to the spec doesn't break NimbusOpaqueTokenIntrospector.
Other than that, it seems like there is nothing to do at this point.
Comment From: rozagerardo
Sounds good @jzheaux thank you very much for your support here! Have a nice week!