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.