I am implementing a resource server in Spring and need to configure a WebClient in order to access downstream data from other APIs. That WebClient needs to negotiate a client credentials OAuth2 flow. Fairly typical stuff. What I find confusing is the following:
When configuring a WebClient for this purpose, I first define an appropriate ReactiveOAuth2AuthorizedClientManager. Then I define an appropriate ExchangeFilterFunction, initializing it with my client manager. According to Spring's docs (and confirming by looking at the source code) when initializing the filter function in this manner, I still need to explicitly set an authorizationFailureHandler for it even though the client manager has effectively the same authorizationFailureHandler automatically configured.
Am I missing something about why this was done? Maybe this is a question for the Spring Security guys.
Here is my code:
@Bean
public WebClient webClientForSomeDownstreamAPI(
ReactiveClientRegistrationRepository clientRegistrationRepository,
ReactiveOAuth2AuthorizedClientService authorizedClientService) {
AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager =
new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository,
authorizedClientService);
authorizedClientManager.setAuthorizedClientProvider(
ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build());
ReactiveOAuth2AuthorizationFailureHandler authorizationFailureHandler =
new RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler(
(clientRegistrationId, principal, attributes) ->
authorizedClientService.removeAuthorizedClient(
clientRegistrationId, principal.getName()));
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
//for some reason the filter function and the client manager both need a failure handler
oauth.setAuthorizationFailureHandler(authorizationFailureHandler);
oauth.setDefaultClientRegistrationId("redacted");
return WebClient.builder()
.filter(oauth)
.clientConnector(createClientConnectorWithLogging())
.baseUrl("redacted")
.build();
}
Spring's AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager (geez what a mouthful) configures a failure handler automatically, using a reference to the passed-in authorizedClientService as seen in the below code snippet (taken from the source code).
public AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
ReactiveClientRegistrationRepository clientRegistrationRepository,
ReactiveOAuth2AuthorizedClientService authorizedClientService) {
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
Assert.notNull(authorizedClientService, "authorizedClientService cannot be null");
this.clientRegistrationRepository = clientRegistrationRepository;
this.authorizedClientService = authorizedClientService;
this.authorizationSuccessHandler = (authorizedClient, principal, attributes) -> authorizedClientService
.saveAuthorizedClient(authorizedClient, principal);
this.authorizationFailureHandler = new RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler(
(clientRegistrationId, principal, attributes) -> this.authorizedClientService
.removeAuthorizedClient(clientRegistrationId, principal.getName()));
}
But the ServerOAuth2AuthorizedClientExchangeFilterFunction constructor I am using (that takes a client manager as an argument) needs me to subsequently set the failure handler explicitly. See the following snippet from the JavaDoc:
/**
* When this constructor is used, authentication (HTTP 401) and authorization (HTTP
* 403) failures returned from a OAuth 2.0 Resource Server will <em>NOT</em> be
* forwarded to a {@link ReactiveOAuth2AuthorizationFailureHandler}. Therefore, future
* requests to the Resource Server will most likely use the same (most likely invalid)
* token, resulting in the same errors returned from the Resource Server. It is
* recommended to configure a
* {@link RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler} via
* {@link #setAuthorizationFailureHandler(ReactiveOAuth2AuthorizationFailureHandler)}
* so that authentication and authorization failures returned from a Resource Server
* will result in removing the authorized client, so that a new token is retrieved for
* future requests.
*/
What makes this interesting is that the other filter function constructor does create an authorizationFailureHandler and it's darn near the same as what the client manager automatically creates. See here:
public ServerOAuth2AuthorizedClientExchangeFilterFunction(
ReactiveClientRegistrationRepository clientRegistrationRepository,
ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
ReactiveOAuth2AuthorizationFailureHandler authorizationFailureHandler = new RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler(
(clientRegistrationId, principal, attributes) -> authorizedClientRepository.removeAuthorizedClient(
clientRegistrationId, principal,
(ServerWebExchange) attributes.get(ServerWebExchange.class.getName())));
this.authorizedClientManager = createDefaultAuthorizedClientManager(clientRegistrationRepository,
authorizedClientRepository, authorizationFailureHandler);
this.clientResponseHandler = new AuthorizationFailureForwarder(authorizationFailureHandler);
this.defaultAuthorizedClientManager = true;
}
I understand that the client manager does not expose its authorizationFailureHandler by way of a getter so the filter function constructor I am invoking cannot just refer to it. But what's the deal with all of that? It doesn't feel right to have two failure handlers doing the same thing to the same authorizedClientService so I must be missing something.
I am using Spring Boot 2.4.1 with Spring Security 5.4.2.