Use AuthorizedClientServiceOAuth2AuthorizedClientManager for client_credentials
I currently have two client registrations with a rest client each:
spring:
security:
oauth2:
client:
provider:
keycloak:
issuer-uri: ${keycloak-configuration.issuer-uri}
registration:
keycloak-authorization-code:
provider: keycloak
authorization-grant-type: authorization_code
client-id: ${keycloak-configuration.client-id}
client-secret: ${keycloak-configuration.client-secret}
scope:
- openid
keycloak-client-credentials:
provider: keycloak
authorization-grant-type: client_credentials
client-id: ${keycloak-configuration.client-id}
client-secret: ${keycloak-configuration.client-secret}
com:
c4-soft:
springaddons:
rest:
client:
foo-client:
base-url: ${foo-service.base-url}
authorization:
oauth2:
oauth2-registration-id: keycloak-authorization-code
bar-client:
base-url: ${bar-service.base-url}
authorization:
oauth2:
oauth2-registration-id: keycloak-client-credentials
expose-builder: true
For my bar-client to work, I have to use the Builder to change the OAuth2AuthorizedClientManager from DefaultOAuth2AuthorizedClientManager to AuthorizedClientServiceOAuth2AuthorizedClientManager (it's used in a background thread). But then I lose the ClientRegistrationIdResolver you create in the RestClientBuilderFactoryBean, so the two options I currently see are:
@Bean
fun barClient(
barClientBuilder: RestClient.Builder,
clientRegistrationRepository: ClientRegistrationRepository,
authorizedClientService: OAuth2AuthorizedClientService,
oauth2AuthorizedClientProvider: OAuth2AuthorizedClientProvider,
): RestClient = barClientBuilder.requestInterceptors {
it.clear()
it.add(
OAuth2ClientHttpRequestInterceptor(
AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientService).apply {
setAuthorizedClientProvider(oauth2AuthorizedClientProvider)
}
).apply {
// option 1
setClientRegistrationIdResolver {
"keycloak-client-credentials"
}
})
}.requestInitializer { request ->
// option 2
RequestAttributeClientRegistrationIdResolver.clientRegistrationId("keycloak-client-credentials").accept(request.attributes)
}.build()
- Overwrite the
ClientRegistrationIdResolverin theOAuth2ClientHttpRequestInterceptorwith a custom one. - Add a
ClientHttpRequestInitializerto let the defaultRequestAttributeClientRegistrationIdResolverget the registration ID from the request attributes.
With both options, I have to reconfigure the registration ID, even though it's already configured in the application properties. Is there a better option I'm missing?
Either way, if the AuthorizedClientServiceOAuth2AuthorizedClientManager was the default for client_credentials clients, it would work out of the box.
spring-addons-starter-rest does not instantiate a DefaultOAuth2AuthorizedClientManager. It uses whatever OAuth2AuthorizedClientManager @Bean is in the context.
spring-addons-starter-oidc instantiates a DefaultOAuth2AuthorizedClientManager only if no other OAuth2AuthorizedClientManager @Bean is defined.
So, exposing your AuthorizedClientServiceOAuth2AuthorizedClientManager instance as a @Bean should be enough in any case (with or without spring-addons-starter-oidc).
Yes, the DefaultOAuth2AuthorizedClientManager comes from spring-addons-starter-oidc.
I thought I needed the DefaultOAuth2AuthorizedClientManager for the foo-client (and the AuthorizedClientServiceOAuth2AuthorizedClientManager for the bar-client). Then what's the point of the DefaultOAuth2AuthorizedClientManager at all when AuthorizedClientServiceOAuth2AuthorizedClientManager can handle both cases?
The documentation says:
(When operating within the context of a
HttpServletRequest, useDefaultOAuth2AuthorizedClientManagerinstead.)
What if I wanted to have different client managers, be it just to follow the "official guide"?
@xehpuk, please accept my apologies for two things:
- the delay: I was very busy on other subjects recently and couldn't dig deeper into your problem
- I hadn't realized that your use case requires different authorized client managers for the two registrations because one is used with the context of the HttpServletRequest, but not the other.
May I ask why the keycloak-client-credentials is used in a separate thread? (why is the response not important for answering the request triggering its usage?)
In any case, I can't think of a mean to satisfy this use case without using named beans: registering two OAuth2AuthorizedClientManager beans with different names, and using qualifiers when injecting the authorized client manager to the RestClient configurer. This could break backward compatibility for quite some existing apps: we would have to be very careful with the name of the bean when registering a custom OAuth2AuthorizedClientManager, and would need a new property to specify for each RestClient the name of the OAuth2AuthorizedClientManager to use.
So, I fear that the best option for you is to expose the RestClient builder and override the authorized client manager, like you do already. However, there could be some options to reduce the technical debt by injecting the SpringAddonsRestProperties and retrieving the registration ID to use from there. I'll try to prototype around that by the end of the week.
The other client just informs another service that something happened. No user authentication or response needed in this case.
If you don't need the response, you could consider using the WebClient and fire-and-forget the child request from the HttpServletRequest thread (subcribe() to it). There is an addons property to expose a WebClient (or its builder) instead of the default RestClient in servlets.
If you're using generated @HttpClient proxies, the factory works equally with WebClient and RestClient.
@xehpuk did you give a try to the WebClient and fire-and-forget the child request from the HttpServletRequest thread?