Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Docs: WebClient OAuth2 Setup for Reactive Applications might be wrong #8444

Closed
fabian-froehlich opened this issue Apr 27, 2020 · 9 comments
Closed
Assignees
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) status: invalid An issue that we don't feel is valid

Comments

@fabian-froehlich
Copy link

fabian-froehlich commented Apr 27, 2020

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.

@fabian-froehlich fabian-froehlich added status: waiting-for-triage An issue we've not yet triaged type: bug A general bug labels Apr 27, 2020
@Avec112
Copy link

Avec112 commented Apr 28, 2020

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.

@jgrandja
Copy link
Contributor

jgrandja commented Apr 29, 2020

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

@jgrandja jgrandja added in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) status: invalid An issue that we don't feel is valid and removed status: waiting-for-triage An issue we've not yet triaged type: bug A general bug labels Apr 29, 2020
@fabian-froehlich
Copy link
Author

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

@jgrandja
Copy link
Contributor

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

@fabian-froehlich
Copy link
Author

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

@jgrandja
Copy link
Contributor

jgrandja commented Apr 30, 2020

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

@eugene-kuntsevich
Copy link

eugene-kuntsevich commented Sep 2, 2021

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

@andrecampanini
Copy link

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.

@sjohnr
Copy link
Member

sjohnr commented Feb 16, 2022

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) status: invalid An issue that we don't feel is valid
Projects
None yet
Development

No branches or pull requests

6 participants