Summary

I think it may make sense to provide first class support (or since we haven't hit GA change our current support) for opaque token support with dynamic credentials. Currently, there is no first class API for supporting rotating the client credentials.

I wonder if it makes sense to use the ClientRegistrationRepository which could be used for looking up both the client key/secret but also the endpoint that is validated against. This would also make it simple for configuration via the issuer uri.

Comment From: jzheaux

I believe this would introduce an optional dependency on oauth2-client - it would at least be optional based on configuring for opaque tokens.

I'm imagining that this would replace the existing constructors - is there value in keeping the (clientId, clientSecret) constructor, though?

Comment From: jzheaux

It's not yet clear the means by which dynamic credentials ought to be provided.

An obvious first candidate is using ClientRegistration and ClientRegistrationRepository since these hold client ids and secrets already, and these are in oauth2-client. ClientRegistration turns out to not be such a great fit, though, since it is focused on OAuth 2.0 grant flows. A grant flow is just one of numerous ways a client can identify itself to an introspection endpoint, and most times it wouldn't make sense to specify a grant type for client authentication.

There is an additional problem with selecting ClientRegistrationRepository and that is that, when a ClientRegistrationRepository is exposed by the application, Spring Boot will wire the application with filters for negotiating the authorization code grant flow. Exposing a ClientRegistrationRepository means that the application is identifying itself as an OAuth 2.0 Client Application, so this again is not what a resource server is when it's making introspection requests.

Another candidate is a new contract. For example, we could introduce a contract called ClientCredentials. This immediately begs the question, though, about the numerous other modes for client authentication and how will those be represented. The challenge with this is that, being so early-on with introspection, it isn't clear how that contract will evolve.

It's also not yet clear where something like this would live since it should not go into oauth2-client insofar as that dependency is focused on identifying that application as an OAuth 2.0 Client Application. There is, of course, a natural hesitancy to place it in oauth2-core as it may not be genuinely cross-cutting.

The user can already add dynamic support themselves, too, since OAuth2TokenIntrospectionClient accepts a RestOperations as a constructor parameter. Quite simply, an application can do:

@Bean 
public OAuth2TokenIntrospectionClient introspectionClient() {
    RestTemplate rest = new RestTemplate();
    rest.getInterceptors().add(this::myBasicAuthorizationInterceptor);
    return new NimbusOAuth2TokenIntrospectionClient("https://idp.example.com/introspect", rest);
}

These earlier unclear items are ones that will clarify as users use the existing more flexible API (RestOperations). I believe we have time to wait and see the ways that are most important to the community before committing to a specific direction for this ticket.

Comment From: jgrandja

@jzheaux If we decide to wait on this, I would still rather remove the constructor

NimbusOAuth2TokenIntrospectionClient(String introspectionUri, String clientId, String clientSecret)

and instead use

NimbusOAuth2TokenIntrospectionClient(String introspectionUri, RestOperations restOperations)

by supplying it with a configured restOperations that uses a BasicAuthenticationInterceptor

What do you think?

Comment From: jzheaux

I don't have a very strong opinion here, though we have seen the benefit in the past of having two configurations: an easy one and an open one (I'm making up these terms).

For example, in the case of NimbusJwtDecoder, there is the constructor that takes the algorithm and the JWK Set URL (easy), and then there is the constructor that takes a JWTProcessor (open).

This is a pattern common to the Resource Server DSL as well, where the application can specify a jwkSetUrl(url) or a jwtDecoder(decoder), even though there are numerous other ways to verify the signature. introspectionClientCredentials(id, secret) and introspectionClient(client) are also similar in this regard.

Like I said, I don't have a very strong opinion, but just so it's easier to find in the future, would you mind adding to this ticket your reason for wanting to remove it?

Comment From: rwinch

I think it makes sense to have something that is simple for users to leverage using the client id and client secret. Perhaps it would be better served as a builder or static factory method to avoid complications with other strategies that leverage two Strings as credentials.

Comment From: jzheaux

@rwinch, you've also indirectly pointed out that the JWK Set URL construction is a builder, not a constructor, so there may also be some power in exposing this in a similar way (builder or factory, as you said) for consistency.

Comment From: jgrandja

@jzheaux Is this ticket still valid or should we consider closing?

Comment From: jzheaux

Applications can do this by configuring the RestOperations with an interceptor that looks up the client credentials.

For example, if an application is using ClientRegistrationRepository, then it can do the following:

@Bean 
OpaqueTokenIntrospector introspector(ClientRegistrationRepository registrations) {
    RestTemplate rest = new RestTemplate();
    rest.getInterceptors().add((request, body, execution) -> {
        ClientRegistration registration = registrations.findByRegistrationId("id");
        HttpHeaders headers = request.getHeaders();
        if (!headers.containsKey(HttpHeaders.AUTHORIZATION)) {
            String clientId = registration.getClientId();
            String secret = registration.getClientSecret();
            String encoded = HttpHeaders.encodeBasicAuth(clientId, secret, UTF_8);
            headers.setBasicAuth(encoded);
        }
        return execution.execute(request, body);
    });
    return new NimbusOpaqueTokenIntrospector(introspectionUri, rest);
}

There may be some value in further simplifying this. As new client authentication strategies emerge in oauth2-client a helpful API may come from it that can achieve this better than the alternatives already described.