In the reference doc there is an example for a WebClient with OAuth2 Setup for Reactive Applications: https://docs.spring.io/spring-security/site/docs/current/reference/html5/#webclient-setup

    @Bean
    WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations, ServerOAuth2AuthorizedClientRepository authorizedClients) {
        ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
                new ServerOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrations, authorizedClients);
        oauth.setDefaultClientRegistrationId("keycloak");
        return WebClient.builder()
                .filter(oauth)
                .build();
    }

But in my szenario it leads to an exception:

java.lang.IllegalArgumentException: serverWebExchange cannot be null
    at org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager.lambda$authorize$4(DefaultReactiveOAuth2AuthorizedClientManager.java:131) ~[spring-security-oauth2-client-5.3.1.RELEASE.jar:5.3.1.RELEASE]
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
    |_ checkpoint ⇢ Request to GET https://abc.de/service/api/endpoint?x=0&y=0&z=0 [DefaultWebClient]
Stack trace:
        at org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager.lambda$authorize$4(DefaultReactiveOAuth2AuthorizedClientManager.java:131) ~[spring-security-oauth2-client-5.3.1.RELEASE.jar:5.3.1.RELEASE]
        at reactor.core.publisher.MonoErrorSupplied.subscribe(MonoErrorSupplied.java:70) ~[reactor-core-3.3.4.RELEASE.jar:3.3.4.RELEASE]
        at reactor.core.publisher.Mono.subscribe(Mono.java:4210) ~[reactor-core-
...

However, switching the ServerOAuth2AuthorizedClientRepository to a ReactiveOAuth2AuthorizedClientService makes the code run.

    @Bean
    WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations, ReactiveOAuth2AuthorizedClientService authorizedClientService) {
        ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistrations, authorizedClientService));
        oauth.setDefaultClientRegistrationId("keycloak");
        return WebClient.builder()
                .filter(oauth)
                .build();
    }
spring-security-config:5.3.1.RELEASE
spring-security-oauth2-client:5.3.1.RELEASE
spring-boot-starter-parent:2.2.6.RELEASE
spring-boot-starter-webflux:2.2.6.RELEASE

Is that an issue or am I handling something wrong? I am not sure if there is a correlation but, the working code example does not retrieve a new token, when Mono.retryWhen(...) is used.

Comment From: Avec112

I experience the same thing. When doing av webClient.get() outside Servlet context i get "servletRequest cannot be null".

If I do the whole call inside a @Controller or @RestController it works fine.

Comment From: jgrandja

@fabian-froehlich @Avec112 The issue here is that the OAuth 2.0 Client Reactive documentation is out-of-date and missing quite a bit of content compared to the Servlet sections.

Take a look at the OAuth2AuthorizedClientManager / OAuth2AuthorizedClientProvider (Servlet) docs:

The DefaultOAuth2AuthorizedClientManager is designed to be used within the context of a HttpServletRequest. When operating outside of a HttpServletRequest context, use AuthorizedClientServiceOAuth2AuthorizedClientManager instead.

Since 5.2, it's recommended to use the OAuth2AuthorizedClientManager constructor.

However, switching the ServerOAuth2AuthorizedClientRepository to a ReactiveOAuth2AuthorizedClientService makes the code run.

This makes sense, however, I would recommend using the ReactiveOAuth2AuthorizedClientManager constructor and pass in AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager.

We have #8174 logged to get the Reactive docs in sync with the Servlet docs.

I'll close this issue as answered. If something is still not clear let me know and we'll address it.

Comment From: fabian-froehlich

Hi @jgrandja, thanks for your reply.

If I am understanding you correct, then your recommendet way is what I wrote in my initial post as a running example, right? And it seems that I am outside of a HttpServletRequest. If I need to change the code in order to work as expected, could you give an example?

Could you give me an insight, if any possible error here, results in my finding, that Mon.retryWhen(..) does not handle a correct token refresh, when retrys are triggered?

Kind regards, Fabian Fröhlich

Comment From: jgrandja

@fabian-froehlich

There are plenty of examples in the reference documentation so please take a look there. Again, the reactive docs are out of date so check out the Servlet docs (the only difference between Servlet and Reactive are the class names).

Comment From: fabian-froehlich

Hi @jgrandja and sorry for your trouble. If the only difference is the class name then there might be an issue because the following config still results in an java.lang.IllegalArgumentException: serverWebExchange cannot be null when following OAuth2AuthorizedClientManager / OAuth2AuthorizedClientProvider and exchanging the classes.

    @Bean
    public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
            ReactiveClientRegistrationRepository clientRegistrationRepository,
            ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {

        ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
                ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
                        .authorizationCode()
                        .refreshToken()
                        .clientCredentials()
                        .password()
                        .build();

        DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager =
                new DefaultReactiveOAuth2AuthorizedClientManager(
                        clientRegistrationRepository, authorizedClientRepository);

        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

        return authorizedClientManager;
    }
    @Bean
    WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) {
        ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
        oauth.setDefaultClientRegistrationId("keycloak");
        return WebClient.builder()
                .filter(oauth)
                .build();
    }

Comment From: jgrandja

@fabian-froehlich

DefaultReactiveOAuth2AuthorizedClientManager is intended to be used within a request context.

Given that you're seeing serverWebExchange cannot be null, you must be operating outside of a request context, which in case you should use AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager instead.

NOTE: Change the ServerOAuth2AuthorizedClientRepository parameter to ReactiveOAuth2AuthorizedClientService.

Comment From: eugene-kuntsevich

Added WebClientConfiguration class like described above but when I'm trying to run app I see in log: Consider defining a bean of type 'org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository' in your configuration.

How could it be solved?

Here code of my class:

import io.netty.channel.ChannelOption;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.oauth2.client.AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProviderBuilder;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.http.client.HttpClient;

@Configuration
@EnableWebFlux
public class WebClientConfiguration {

  @Primary
  @Bean
  public WebClient webClient(ServiceProperties properties,
      AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager auth2AuthorizedClientManager) {

    ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
        new ServerOAuth2AuthorizedClientExchangeFilterFunction(auth2AuthorizedClientManager);

    oauth2Client.setDefaultClientRegistrationId("keycloak");
    HttpClient httpClient = HttpClient.create()
        .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10 * 1000)
        .doOnConnected(connection -> {
          connection.addHandlerLast(new ReadTimeoutHandler(2 * 60 * 1000L, MILLISECONDS));
          connection.addHandlerLast(new WriteTimeoutHandler(2 * 60 * 1000L, MILLISECONDS));
        });

    return WebClient.builder()
        .baseUrl(properties.getUrl())
        .filter(oauth2Client)
        .clientConnector(new ReactorClientHttpConnector(httpClient))
        .build();
  }

  @Bean
  public AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager(
      ReactiveClientRegistrationRepository clientRegistrationRepository,
      ReactiveOAuth2AuthorizedClientService reactiveOAuth2AuthorizedClientService) {

    ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
        ReactiveOAuth2AuthorizedClientProviderBuilder.builder().clientCredentials().build();

    AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager =
        new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
            clientRegistrationRepository, reactiveOAuth2AuthorizedClientService);

    authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

    return authorizedClientManager;
  }
}

SOLVED. Need to add ReactiveClientRegistrationRepository bean:

  @Bean
  ReactiveClientRegistrationRepository clientRegistrations(
      @Value("${spring.security.oauth2.client.provider.keycloak.token-uri}") String token_uri,
      @Value("${spring.security.oauth2.client.registration.keycloak.client-id}") String client_id,
      @Value("${spring.security.oauth2.client.registration.keycloak.client-secret}") String client_secret,
      @Value("${spring.security.oauth2.client.registration.keycloak.authorization-grant-type}") String authorizationGrantType

  ) {
    ClientRegistration registration = ClientRegistration
        .withRegistrationId("keycloak")
        .tokenUri(token_uri)
        .clientId(client_id)
        .clientSecret(client_secret)
        .authorizationGrantType(new AuthorizationGrantType(authorizationGrantType))
        .build();
    return new InMemoryReactiveClientRegistrationRepository(registration);
  }

and change AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager bean:

  @Bean
  public AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager(
      ReactiveClientRegistrationRepository clientRegistrationRepository) {

    InMemoryReactiveOAuth2AuthorizedClientService clientService =
        new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrationRepository);

    ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
        ReactiveOAuth2AuthorizedClientProviderBuilder.builder().clientCredentials().build();

    AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager =
        new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
            clientRegistrationRepository, clientService);

    authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

    return authorizedClientManager;
  }

Comment From: andrecampanini

eugene-kuntsevich

Hello Eugene,

I configured exactly the same beans as you (like AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager) but when I execute my code is always an instance of DefaultReactiveOAuth2AuthorizedClientManager that is being executed.

Comment From: sjohnr

@andrecampanini, just a note that since the last comment on this thread, we have updated the reactive section of the documentation. See the section on ReactiveOAuth2AuthorizedClientManager. It may be worth reading through the entire chapter in context. If you have anything that looks like a bug, feel free to file a new issue with a minimal sample that reproduces the issue.