Describe the bug
when configuring WebClient using ServerOAuth2AuthorizedClientExchangeFilterFunction with AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager, if the resource server returns 403, the OAuth2AuthorizedClient doesn't get removed.
To Reproduce
@Bean
public WebClient oauth2WebClient(
final WebClient.Builder webClientBuilder,
final ReactiveClientRegistrationRepository registrationRepository,
final ReactiveOAuth2AuthorizedClientService authorizedClientService,
final String clientRegistrationId) {
final ServerOAuth2AuthorizedClientExchangeFilterFunction secureExchangeFilterFunction =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(
new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
registrationRepository, authorizedClientService));
secureExchangeFilterFunction.setAuthorizationFailureHandler(
new RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler(
(clientRegistrationId, principal, attributes) ->
authorizedClientService.removeAuthorizedClient(
clientRegistrationId, principal.getName())
));
secureExchangeFilterFunction.setDefaultClientRegistrationId(clientRegistrationId);
return webClientBuilder.clone().filter(secureExchangeFilterFunction).build();
}
when using the above oauth2WebClient to make a call to a resource server, the resource server returns 403.
A subsequence call to the resource server uses the old access token.
Expected behavior
when using the above oauth2WebClient to make a call to a resource server, the resource server returns 403.
A subsequence call to the resource server should retrieve a new access token from the authorization server.
Initial thoughts
RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler is initialized with DEFAULT_REMOVE_AUTHORIZED_CLIENT_ERROR_CODES which contains only INVALID_TOKEN and INVALID_GRANT. However, with 403 returned by the resource server, ServerOAuth2AuthorizedClientExchangeFilterFunction#AuthorizationFailureForwarder maps 403 to INSUFFICIENT_SCOPE. This causes condition hasRemovalErrorCode() in RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler#onAuthorizationFailure not satisfy hence Oauth2AuthorizedClient doesn't get removed.
https://github.com/spring-projects/spring-security/blob/1ff5eb6b57d2ba55c82a90c2150aa760c36ec237/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler.java#L113-L120
Work Around
@Bean
public WebClient oauth2WebClient(
final WebClient.Builder webClientBuilder,
final ReactiveClientRegistrationRepository registrationRepository,
final ReactiveOAuth2AuthorizedClientService authorizedClientService,
final String clientRegistrationId) {
final ServerOAuth2AuthorizedClientExchangeFilterFunction secureExchangeFilterFunction =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(
new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
registrationRepository, authorizedClientService));
Set<String> removeAuthorizedClientErrorCodes =
new HashSet<>(DEFAULT_REMOVE_AUTHORIZED_CLIENT_ERROR_CODES);
removeAuthorizedClientErrorCodes.add(INSUFFICIENT_SCOPE); // 403
secureExchangeFilterFunction.setAuthorizationFailureHandler(
new RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler(
(clientRegistrationId, principal, attributes) ->
authorizedClientService.removeAuthorizedClient(
clientRegistrationId, principal.getName()),
removeAuthorizedClientErrorCodes
));
secureExchangeFilterFunction.setDefaultClientRegistrationId(clientRegistrationId);
return webClientBuilder.clone().filter(secureExchangeFilterFunction).build();
}
Comment From: jzheaux
Hi, @trung. I don't find it quite straightforward to have the filter function query for another token if the first is valid, just with insufficient privileges. In most cases, I think that would be an unending loop.
For example, if your client has a token with a scope of message:read and presents it to the resource server. If it responds with a 403 and then you request a new access token, isn't it most likely that the new access token's scope will still be message:read?
Please elaborate if I'm missing something. Otherwise, I'd encourage you to contribute this use case to #11783 to potentially simplify your configuration.
Comment From: trung
Hi @jzheaux, the configuration is for machine-to-machine communication. If there were missing privileges, we would update the client scopes in our authorization server. In that case, the old token should be invalidated instantly without the need of waiting for token to expire or restarting the server.
Let me take a look at the ticket and see if this could be configurable.