Summary
From my service I am performing http calls to an external service OAuth2 protected. The flow that we are using for the communication is "client_credentials". I am using not the RestTemplate http client but the WebClient. The WebClient is configured like this:
@Bean
WebClient webClient(ReactiveClientRegistrationRepository reactiveClientRegistrationRepository,
ServerOAuth2AuthorizedClientRepository serverOAuth2AuthorizedClientRepository) {
ClientCredentialsReactiveOAuth2AuthorizedClientProvider provider =
new ClientCredentialsReactiveOAuth2AuthorizedClientProvider();
provider.setAccessTokenResponseClient(new WebClientReactiveClientCredentialsTokenResponseClient());
DefaultReactiveOAuth2AuthorizedClientManager manager =
new DefaultReactiveOAuth2AuthorizedClientManager(reactiveClientRegistrationRepository,
serverOAuth2AuthorizedClientRepository);
manager.setAuthorizedClientProvider(provider);
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2 =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(manager);
oauth2.setDefaultClientRegistrationId(CLIENT_REGISTRATION_ID);
WebClient client = WebClient.builder()
.baseUrl(externalApiSettings.getUri())
.filter(oauth2)
.build();
return client;
}
Actual Behavior
When I perform the call like this:
@Component
@Log4j2
@RequiredArgsConstructor
class Client {
private final WebClient client;
@EventListener(ApplicationReadyEvent.class)
public void ready() {
this.client
.get()
.uri(uriBuilder -> uriBuilder
.path("/api/v3/{tenant}/{entity}")
.queryParam("view", "flat")
.queryParam("version", "1")
.queryParam("state", "update")
.queryParam("fromSeq", "58000")
.queryParam("toSeq", "58100")
.build("nl", "contactchannels"))
.retrieve()
.bodyToFlux(String.class)
.subscribe(entity -> log.info("Entity: " + entity));
}
}
, I get IllegalArgumentException:
java.lang.IllegalArgumentException: serverWebExchange cannot be null
, and debugging I can see that getTokenResponse method from WebClientReactiveClientCredentialsTokenResponseClient is never reached. Before that, I was building the webclient using the non-reactive ClientRegistrationRepository and OAuth2AuthorizedClientRepository. I am sure that I am missing something but I don't know what.
Expected Behavior
Configuration
logging:
level:
org.springframework.web.reactive.function.client.WebClient: DEBUG
external:
uri: https://external_url
spring:
application:
name: cdm_client
http:
log-request-details: true
security:
oauth2:
client:
provider:
idam:
token-uri: "https://idam_token_hostname/authorize/api/oauth2/access_token"
registration:
idam:
client-id: CLIENT_ID
client-secret: CLIENT_SECRET
authorization-grant-type: client_credentials
Version
Sample
https://github.com/cipriandumitrel/oauth2-client
Comment From: cipriandumitrel
The reason for which I used the ServerOAuth2AuthorizedClientExchangeFilterFunction constructor that receives an authorized client manager (and not the one that receives a ReactiveClientRegistrationRepository and a ServerOAuth2AuthorizedClientRepository) is that I needed to implement my own ReactiveOAuth2AccessTokenResponseClient, in order to customize the form parameters list used for the get token request by adding a new parameter called REALM_ID. I found out that the IllegalArgumentException that I was receiving is because in the client manager implementation that i was using (DefaultReactiveOAuth2AuthorizedClientManager), the loadAuthorizedClient method takes into consideration the ServerWebExchange, which was null. That's why I needed to use an authentication manager that does not take into consideration the ServerWebExchange, something like ServerOAuth2AuthorizedClientExchangeFilterFunction.UnAuthenticatedReactiveOAuth2AuthorizedClientManager. That implied to instantiate the ServerOAuth2AuthorizedClientExchangeFilterFunction with the other constructor like this:
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(
clientRegistrations, new UnAuthenticatedServerOAuth2AuthorizedClientRepository());
, but this leaded to the impossibility to pass a custom access token response client to the underlying authorized client provider created like this:
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken()
.clientCredentials()
.password()
.build();
I found the following workaround to obtain the behaviour that I want, but I am really not proud of it, and maybe there's a happier and more elegant solution out there. The workaround looks like this:
- Created my own client manager type that pretty much resembles the UnAuthenticatedReactiveOAuth2AuthorizedClientManager ;
- Attach to it a new DelegatingReactiveOAuth2AuthorizedClientProvider that contains the new ClientCredentialsReactiveOAuth2AuthorizedClientProvider equiped with my custom ReactiveOAuth2AccessTokenResponseClient
- Pass the new client manager to the exchange function.
The code looks like this:
@Bean
public WebClient webClientSecurityCustomizer(
ReactiveClientRegistrationRepository clientRegistrations) {
AnonymousReactiveOAuth2AuthorizedClientManager manager =
new AnonymousReactiveOAuth2AuthorizedClientManager(clientRegistrations,
new UnAuthenticatedServerOAuth2AuthorizedClientRepository());
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken()
.clientCredentials(clientCredentialsGrantBuilder ->
clientCredentialsGrantBuilder.accessTokenResponseClient(new IdamReactiveOAuth2AccessTokenResponseClient()))
.password()
.build();
manager.setAuthorizedClientProvider(authorizedClientProvider);
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2 =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(manager);
return WebClient.builder()
.baseUrl(externalApiSettings.getUri())
.filter(oauth2)
.build();
}
Comment From: jgrandja
@cipriandumitrel Instead of creating AnonymousReactiveOAuth2AuthorizedClientManager, you can use AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager (since 5.2.2) or AuthorizedClientServiceOAuth2AuthorizedClientManager (since 5.2.0). These managers are instended to be used outside of a request context. The reference documentation was recently updated in 5.2.3.BUILD-SNAPSHOT. Please see the section OAuth2AuthorizedClientManager / OAuth2AuthorizedClientProvider:
The DefaultOAuth2AuthorizedClientManager is designed to be used within the context of a HttpServletRequest. When operating outside of a HttpServletRequest context, use AuthorizedClientServiceOAuth2AuthorizedClientManager instead.
I'm going to close this issue as the solution above will work for you.
Comment From: cipriandumitrel
Hi, @jgrandja ,
Indeed, the solution works for me. I'm sorry that I haven't read the documentation thoroughly. I haven't stumbled across this implementation of ReactiveOAuth2AuthorizedClientManager in the codebase.
Thanks a lot for your help.