Describe the bug
ServletOAuth2AuthorizedClientExchangeFilterFunction with AuthenticatedPrincipalOAuth2AuthorizedClientRepository never removes an invalid token if an origin princapal is authenticated. The problem is that the ServletOAuth2AuthorizedClientExchangeFilterFunction creates a new Authentication if a token is invalid and a resource server returns 401. This new Authentication is created in AuthorizationFailureForwarder and it is not authenticated. And because there is a check for authentication in AuthenticatedPrincipalOAuth2AuthorizedClientRepository and origin token was authenticated and it is stored for example in InMemoryOAuth2AuthorizedClientService but the new one is not authenticated then the AuthenticatedPrincipalOAuth2AuthorizedClientRepository tries to remove the token from the default different HttpSessionOAuth2AuthorizedClientRepository.
See the methods implementations: https://github.com/spring-projects/spring-security/blob/7ef3f619242816683a72b35a1f8b4fb4f32d5203/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java#L618-L632
https://github.com/spring-projects/spring-security/blob/857830f6958c229650e6a00d376af38c8bcddbad/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/AuthenticatedPrincipalOAuth2AuthorizedClientRepository.java#L98-L112
To Reproduce Use an Authentication with and set authentication true. And set it into SecurityContext. For example:
class KeycloakAuthentication extends AbstractAuthenticationToken {
public KeycloakAuthentication() {
super(null);
setAuthenticated(true);
}
@Override
public Object getCredentials() {
return "";
}
@Override
public Object getPrincipal() {
return "keycloak";
}
}
Use WebClient configured with ServletOAuth2AuthorizedClientExchangeFilterFunction where authorizedClientRepository is AuthenticatedPrincipalOAuth2AuthorizedClientRepository
final ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = new ServletOAuth2AuthorizedClientExchangeFilterFunction(
clientRegistrationRepository, authorizedClientRepository);
oauth2Client.setDefaultClientRegistrationId(clientName);
WebClient webClient = WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build();
Call it the first time with a success response from a resource server. Call it the second time with 401 from a resource server. And invalid OAuth2 token is still there and you have to wait until expiration.
Expected behavior
OAuth2 token is removed even if an Authentication is authenticated.
Sample I will try to create it.
Comment From: jgrandja
@Saljack Thanks for the report. However, I don't think I'm following your use case. It would be most efficient if you can provide a minimal sample (or test) that reproduces the issue.
Comment From: Saljack
Ok I finally create a sample here: https://github.com/Saljack/spring-security-9477
See a SpringSecurity9477ApplicationTests where I try simulate a resource server which returns 401 and then a token should be removed but it will never be removed. The Best way how to see it is debug it and put a break point into the method removeAuthorizedClient in AuthenticatedPrincipalOAuth2AuthorizedClientRepository
https://github.com/spring-projects/spring-security/blob/857830f6958c229650e6a00d376af38c8bcddbad/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/AuthenticatedPrincipalOAuth2AuthorizedClientRepository.java#L98-L112
Comment From: jgrandja
@Saljack Thanks for creating the sample.
The issue here is a misconfiguration of the WebClient @Bean and associated ServletOAuth2AuthorizedClientExchangeFilterFunction.
Replace your existing WebClient @Bean with the following:
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientService authorizedClientService) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build();
AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager =
new AuthorizedClientServiceOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientService);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
@Bean
public WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
final ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth2Client.setDefaultClientRegistrationId("test");
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build();
}
Given that your sample is using a client_credentials client, you need to configure a AuthorizedClientServiceOAuth2AuthorizedClientManager instead of a DefaultOAuth2AuthorizedClientManager.
Please review the reference for OAuth2AuthorizedClientManager.
Take note of:
The
DefaultOAuth2AuthorizedClientManageris designed to be used within the context of aHttpServletRequest. When operating outside of aHttpServletRequestcontext, useAuthorizedClientServiceOAuth2AuthorizedClientManagerinstead.
After you apply the above change, you will need to fix your test in SpringSecurity9477ApplicationTests.
The test has the following:
// Second request and resource server returns 401 Unauthorized then the token
// should be removed
webClient
.get()
.uri("/")
.headers(headers -> headers.setBasicAuth("user", "password"))
.exchange()
.expectStatus()
.is5xxServerError();
loadAuthorizedClient = oauth2AuthorizedClientService.loadAuthorizedClient("test", "user");
assertNull(loadAuthorizedClient, "Token is not removed");
Calling the Resource Server using an invalid token (e.g. expired) should return a 401 NOT a 500. Also, you shouldn't pass .headers(headers -> headers.setBasicAuth("user", "password")). Please update the test to:
// Second request and resource server returns 401 Unauthorized then the token
// should be removed
webClient
.get()
....
.is4xxClientError()
NOTE: The clientResponseHandler in ServletOAuth2AuthorizedClientExchangeFilterFunction will only be applied on a successful response, e.g. 401. It will not be applied on a 500 server error. You will need to simulate the mock server to return a 401.
I'm going to close this based on application misconfiguration.