Spring-security: Docs: WebClient OAuth2 Setup for Reactive Applications might be wrong

Created on 27 Apr 2020  Â·  6Comments  Â·  Source: spring-projects/spring-security

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.

oauth2 invalid

Most helpful comment

@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.

All 6 comments

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.

@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.

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

@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).

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();
    }

@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.

Was this page helpful?
0 / 5 - 0 ratings