Search code examples
spring-security

How to associate an Oauth identity that is not the default user to a WebClient?


I am building a service that will allow a user to associate multiple oauth identities to their account and then retrieve information based on any / all of the identities.

I am storing the oauth identities in Postgres using Spring's provided R2dbcReactiveOAuth2AuthorizedClientService. My current challenge is to associate the saved oauth identity to a WebClient so the information is based on that Oauth identity.

Based on the JavaDoc for ServerOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient, I can pass the OAuth2AuthorizedClient and it will use that identity for the WebClient.retrieve().

Modifies the ClientRequest.attributes() to include the OAuth2AuthorizedClient to be used for providing the Bearer Token. Example usage:

 WebClient webClient = WebClient.builder()
     .filter(new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager))
     .build();
  Mono<String> response = webClient
     .get()
     .uri(uri)
     .attributes(oauth2AuthorizedClient(authorizedClient))
     // ...
     .retrieve()
     .bodyToMono(String.class);

Based on debugging, my current code successfully loads the oauth identity from the database and adds it as an attribute to the WebClient. When the WebClient retrieves, I get the error IllegalArgumentException: serverWebExchange cannot be null. The other questions on SO that refer to this error indicate that it happens when you mix servlet and reactive calls. However, I only have WebFlux as a maven dependency, so I'm pretty sure that is not happening here.

Any suggestions on how to resolve / proceed?

My product service

    public class ProductService {
        private final ReactiveOAuth2AuthorizedClientService oAuth2AuthorizedClientService;
        private final ReactiveClientRegistrationRepository clientRegistrations;
        private static final String baseUri = "https://myapp.net/product";

        public ProductService(ReactiveOAuth2AuthorizedClientService oAuth2AuthorizedClientService,
                ReactiveClientRegistrationRepository clientRegistrations) {
            this.oAuth2AuthorizedClientService = oAuth2AuthorizedClientService;
            this.clientRegistrations = clientRegistrations;
        }

        public Mono<String> getNotifications(String productName, String userName) {
            String dataUri = "/{id}/notifications";
            Mono<OAuth2AuthorizedClient> userOauth = oAuth2AuthorizedClientService.loadAuthorizedClient("xxx", userName);
            Mono<Long> productId = this.lookupProductId(productName);

            return Mono.zip(productId, userOauth).checkpoint().flatMap(tuple2 ->
                    this.getUserWebClient(userName).get()
                            .uri(uriBuilder -> uriBuilder
                                    .path(dataUri)
                                    .queryParam("datasource", "development")
                                    .build(tuple2.getT1().toString()))
                            .attributes(ServerOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient(tuple2.getT2()))
                            .retrieve()
                            .bodyToMono(String.class));
        }

        private WebClient getUserWebClient() {
            var authorizedClients = new AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository(oAuth2AuthorizedClientService);
            var oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(
                    clientRegistrations, authorizedClients);
            return WebClient.builder()
                    .baseUrl(baseUri)
                    .filter(oauth)
                    .build();
        }

        public Mono<Long> lookupProductId(String name) {
            // business logic to lookup product based on name
        }
    }

Web Security configuration to use Postgres repository instead of the default In-Memory bean

    @Bean
    public ReactiveOAuth2AuthorizedClientService dbOauth2AuthorizedClientService(DatabaseClient databaseClient,
            ReactiveClientRegistrationRepository clientRegistrationRepository) {
        return new R2dbcReactiveOAuth2AuthorizedClientService(databaseClient, clientRegistrationRepository);
    }

Solution

  • After a nice long summer holiday, I found and re-read the docs at https://docs.spring.io/spring-security/reference/reactive/oauth2/client/core.html. This lead me to understand that the error is due to not having a ResponseContext since it is in a @Service component. For the time being, I moved it to a @GetMapping in a @Controller.

    The docs point out that a AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager can be used in a @Service component. I will try to update this answer with the code for that once I have it complete.