7

Using Spring Boot 2.5, I am trying to configure a webClient at the builder level that, when it gets a 401, will remove the current token and then try again to call the resource (so the webclient, realizing there's no token anymore, will fetch a new one before actually calling the resource).

This is what I have so far :

  @Bean
  WebClient servletWebClient(ClientRegistrationRepository  clientRegistrations,
                      OAuth2AuthorizedClientRepository authorizedClients) {

    //this constructor  will configure internally a RemoveAuthorizedClientOAuth2AuthorizationFailureHandler,
    // and onAuthorizationFailure will be called on it when we get a 401
    var oauth = new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrations, authorizedClients);

    oauth.setDefaultClientRegistrationId("keycloak");

    return WebClient.builder()
        .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
        .apply(oauth.oauth2Configuration())
        .build();
  }

I can see that when using this webClient, RemoveAuthorizedClientOAuth2AuthorizationFailureHandler comes into picture and removeAuthorizedClient is called. If I make a second call through this webClient, it automatically fetchs a new token before making the actual call.

So now, I want to configure the retry part, and I was thinking an additional filter would do the trick :

  ExchangeFilterFunction retryOn401Function() {

    return (request, next) -> next.exchange(request)
        .flatMap((Function<ClientResponse, Mono<ClientResponse>>) clientResponse -> {
          if (clientResponse.statusCode().value() == 401) {
            log.warn("got an unauthorized status when calling service - will retry once");
            return next.exchange(request).retry(1L);
          } else {
            return Mono.just(clientResponse);
          }
        });
  }

and now the webClient is built with it :

    return WebClient.builder()
        .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
        .apply(oauth.oauth2Configuration())
         // filter added
        .filter(retryOn401Function())
        .build();

From what I can observe, the retry happens before removeAuthorizedClient is called. so the exact same call is retried, with the same token - which obviously fails again.

I've seen a couple of similar questions, but the solutions feel "hack-ish" as it usually requires manipulating the headers myself in one way or another :

Surely, there must be a better "Spring" way to achieve this, right ? Is there a way for removeAuthorizedClient to be called before the retry ?

Thanks !

Vincent F
  • 6,523
  • 7
  • 37
  • 79
  • The "older one" link has the correct solution. You can't just retry the request, as it will reuse the request without any updates to the auth headers, so you first have to take the initial request and update the auth header, then retry the new updated request. – Shadow Man Sep 06 '22 at 18:09

1 Answers1

0

first you should use filter for retrying, then filter that make token removing.

    return WebClient.builder()
    .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
    .filter(retryOn401Filter())
    .filter(oauth)
    .build();

Sorry forget token removing logic, it may looks like this:

private ServerOAuth2AuthorizedClientExchangeFilterFunction getAuthorizationFilter(
        CustomClientCredentialsReactiveOAuth2AuthorizedClientProvider provider,
        Properties properties
) {
    ReactiveClientRegistrationRepository clientRegistration = clientRegistration(properties);
    ReactiveOAuth2AuthorizedClientService authorizedClientService = authorizedClient(clientRegistration);
    AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager manager =
            new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistration, authorizedClientService);
    manager.setAuthorizedClientProvider(provider);
    ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(manager);

    //removes access token in case of 401/403 http error codes
    RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler removeAuthorizedClientReactiveOAuth2AuthorizationFailureHandler =
            new RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler(
                    (clientRegistrationId, principal, attributes) ->
                            authorizedClientService.removeAuthorizedClient(clientRegistrationId, principal.getName())
            );
    oauth.setAuthorizationFailureHandler(removeAuthorizedClientReactiveOAuth2AuthorizationFailureHandler);
    oauth.setDefaultOAuth2AuthorizedClient(true);
    oauth.setDefaultClientRegistrationId("pyrus");
    return oauth;
}
JustK K
  • 179
  • 1
  • 3