1
0
mirror of synced 2026-08-05 01:36:56 +00:00

Add RestClient interceptor

Closes gh-13588
This commit is contained in:
Steve Riesenberg
2024-04-30 17:48:17 -05:00
parent b294816600
commit e3c19ba86c
4 changed files with 1432 additions and 71 deletions
@@ -0,0 +1,381 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.web.client;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.oauth2.client.ClientAuthorizationException;
import org.springframework.security.oauth2.client.OAuth2AuthorizationFailureHandler;
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.RemoveAuthorizedClientOAuth2AuthorizationFailureHandler;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestClientResponseException;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* Provides an easy mechanism for using an {@link OAuth2AuthorizedClient} to make OAuth
* 2.0 requests by including the {@link OAuth2AuthorizedClient#getAccessToken() access
* token} as a bearer token.
*
* <p>
* Example usage:
*
* <pre>
* OAuth2ClientHttpRequestInterceptor requestInterceptor =
* new OAuth2ClientHttpRequestInterceptor(authorizedClientManager);
* RestClient restClient = RestClient.builder()
* .requestInterceptor(requestInterceptor)
* .build();
* String response = restClient.get()
* .uri(uri)
* .retrieve()
* .body(String.class);
* </pre>
*
* <h3>Authentication and Authorization Failures</h3>
*
* <p>
* This interceptor has the ability to forward authentication (HTTP 401 Unauthorized) and
* authorization (HTTP 403 Forbidden) failures from an OAuth 2.0 Resource Server to an
* {@link OAuth2AuthorizationFailureHandler}. A
* {@link RemoveAuthorizedClientOAuth2AuthorizationFailureHandler} can be used to remove
* the cached {@link OAuth2AuthorizedClient}, so that future requests will result in a new
* token being retrieved from an Authorization Server, and sent to the Resource Server.
*
* <p>
* Use either {@link #authorizationFailureHandler(OAuth2AuthorizedClientRepository)} or
* {@link #authorizationFailureHandler(OAuth2AuthorizedClientService)} to create a
* {@link RemoveAuthorizedClientOAuth2AuthorizationFailureHandler} which can be provided
* to {@link #setAuthorizationFailureHandler(OAuth2AuthorizationFailureHandler)}.
*
* <p>
* For example:
*
* <pre>
* OAuth2AuthorizationFailureHandler authorizationFailureHandler =
* OAuth2ClientHttpRequestInterceptor.authorizationFailureHandler(authorizedClientRepository);
* requestInterceptor.setAuthorizationFailureHandler(authorizationFailureHandler);
* </pre>
*
* @author Steve Riesenberg
* @since 6.4
* @see OAuth2AuthorizedClientManager
* @see OAuth2AuthorizedClientProvider
* @see OAuth2AuthorizedClient
* @see OAuth2AuthorizationFailureHandler
*/
public final class OAuth2ClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
// @formatter:off
private static final Map<HttpStatusCode, String> OAUTH2_ERROR_CODES = Map.of(
HttpStatus.UNAUTHORIZED, OAuth2ErrorCodes.INVALID_TOKEN,
HttpStatus.FORBIDDEN, OAuth2ErrorCodes.INSUFFICIENT_SCOPE
);
// @formatter:on
private static final Authentication ANONYMOUS_AUTHENTICATION = new AnonymousAuthenticationToken("anonymous",
"anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
private final OAuth2AuthorizedClientManager authorizedClientManager;
private final ClientRegistrationIdResolver clientRegistrationIdResolver;
// @formatter:off
private OAuth2AuthorizationFailureHandler authorizationFailureHandler =
(clientRegistrationId, principal, attributes) -> { };
// @formatter:on
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
.getContextHolderStrategy();
/**
* Constructs a {@code OAuth2ClientHttpRequestInterceptor} using the provided
* parameters.
* @param authorizedClientManager the {@link OAuth2AuthorizedClientManager} which
* manages the authorized client(s)
*/
public OAuth2ClientHttpRequestInterceptor(OAuth2AuthorizedClientManager authorizedClientManager) {
this(authorizedClientManager, new RequestAttributeClientRegistrationIdResolver());
}
/**
* Constructs a {@code OAuth2ClientHttpRequestInterceptor} using the provided
* parameters.
* @param authorizedClientManager the {@link OAuth2AuthorizedClientManager} which
* manages the authorized client(s)
* @param clientRegistrationIdResolver the strategy for resolving a
* {@code clientRegistrationId} from the intercepted request
*/
public OAuth2ClientHttpRequestInterceptor(OAuth2AuthorizedClientManager authorizedClientManager,
ClientRegistrationIdResolver clientRegistrationIdResolver) {
Assert.notNull(authorizedClientManager, "authorizedClientManager cannot be null");
Assert.notNull(clientRegistrationIdResolver, "clientRegistrationIdResolver cannot be null");
this.authorizedClientManager = authorizedClientManager;
this.clientRegistrationIdResolver = clientRegistrationIdResolver;
}
/**
* Sets the {@link OAuth2AuthorizationFailureHandler} that handles authentication and
* authorization failures when communicating to the OAuth 2.0 Resource Server.
*
* <p>
* For example, a {@link RemoveAuthorizedClientOAuth2AuthorizationFailureHandler} is
* typically used to remove the cached {@link OAuth2AuthorizedClient}, so that the
* same token is no longer used in future requests to the Resource Server.
* @param authorizationFailureHandler the {@link OAuth2AuthorizationFailureHandler}
* that handles authentication and authorization failures
* @see #authorizationFailureHandler(OAuth2AuthorizedClientRepository)
* @see #authorizationFailureHandler(OAuth2AuthorizedClientService)
*/
public void setAuthorizationFailureHandler(OAuth2AuthorizationFailureHandler authorizationFailureHandler) {
Assert.notNull(authorizationFailureHandler, "authorizationFailureHandler cannot be null");
this.authorizationFailureHandler = authorizationFailureHandler;
}
/**
* Provides an {@link OAuth2AuthorizationFailureHandler} that handles authentication
* and authorization failures when communicating to the OAuth 2.0 Resource Server
* using a {@link OAuth2AuthorizedClientRepository}.
*
* <p>
* When this method is used, authentication (HTTP 401) and authorization (HTTP 403)
* failures returned from an OAuth 2.0 Resource Server will be forwarded to a
* {@link RemoveAuthorizedClientOAuth2AuthorizationFailureHandler}, which will
* potentially remove the {@link OAuth2AuthorizedClient} from the given
* {@link OAuth2AuthorizedClientRepository}, depending on the OAuth 2.0 error code
* returned. Authentication failures returned from an OAuth 2.0 Resource Server
* typically indicate that the token is invalid, and should not be used in future
* requests. Removing the authorized client from the repository will ensure that the
* existing token will not be sent for future requests to the Resource Server, and a
* new token is retrieved from the Authorization Server and used for future requests
* to the Resource Server.
* @param authorizedClientRepository the repository of authorized clients
* @see #setAuthorizationFailureHandler(OAuth2AuthorizationFailureHandler)
*/
public static OAuth2AuthorizationFailureHandler authorizationFailureHandler(
OAuth2AuthorizedClientRepository authorizedClientRepository) {
Assert.notNull(authorizedClientRepository, "authorizedClientRepository cannot be null");
return new RemoveAuthorizedClientOAuth2AuthorizationFailureHandler(
(clientRegistrationId, principal, attributes) -> {
HttpServletRequest request = (HttpServletRequest) attributes
.get(HttpServletRequest.class.getName());
HttpServletResponse response = (HttpServletResponse) attributes
.get(HttpServletResponse.class.getName());
authorizedClientRepository.removeAuthorizedClient(clientRegistrationId, principal, request,
response);
});
}
/**
* Provides an {@link OAuth2AuthorizationFailureHandler} that handles authentication
* and authorization failures when communicating to the OAuth 2.0 Resource Server
* using a {@link OAuth2AuthorizedClientService}.
*
* <p>
* When this method is used, authentication (HTTP 401) and authorization (HTTP 403)
* failures returned from an OAuth 2.0 Resource Server will be forwarded to a
* {@link RemoveAuthorizedClientOAuth2AuthorizationFailureHandler}, which will
* potentially remove the {@link OAuth2AuthorizedClient} from the given
* {@link OAuth2AuthorizedClientService}, depending on the OAuth 2.0 error code
* returned. Authentication failures returned from an OAuth 2.0 Resource Server
* typically indicate that the token is invalid, and should not be used in future
* requests. Removing the authorized client from the repository will ensure that the
* existing token will not be sent for future requests to the Resource Server, and a
* new token is retrieved from the Authorization Server and used for future requests
* to the Resource Server.
* @param authorizedClientService the service used to manage authorized clients
* @see #setAuthorizationFailureHandler(OAuth2AuthorizationFailureHandler)
*/
public static OAuth2AuthorizationFailureHandler authorizationFailureHandler(
OAuth2AuthorizedClientService authorizedClientService) {
Assert.notNull(authorizedClientService, "authorizedClientService cannot be null");
return new RemoveAuthorizedClientOAuth2AuthorizationFailureHandler(
(clientRegistrationId, principal, attributes) -> authorizedClientService
.removeAuthorizedClient(clientRegistrationId, principal.getName()));
}
/**
* Sets the {@link SecurityContextHolderStrategy} to use. The default action is to use
* the {@link SecurityContextHolderStrategy} stored in {@link SecurityContextHolder}.
* @param securityContextHolderStrategy the {@link SecurityContextHolderStrategy} to
* use
*/
public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
this.securityContextHolderStrategy = securityContextHolderStrategy;
}
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
Authentication principal = this.securityContextHolderStrategy.getContext().getAuthentication();
if (principal == null) {
principal = ANONYMOUS_AUTHENTICATION;
}
authorizeClient(request, principal);
try {
ClientHttpResponse response = execution.execute(request, body);
handleAuthorizationFailure(request, principal, response.getHeaders(), response.getStatusCode());
return response;
}
catch (RestClientResponseException ex) {
handleAuthorizationFailure(request, principal, ex.getResponseHeaders(), ex.getStatusCode());
throw ex;
}
catch (OAuth2AuthorizationException ex) {
handleAuthorizationFailure(ex, principal);
throw ex;
}
}
private void authorizeClient(HttpRequest request, Authentication principal) {
String clientRegistrationId = this.clientRegistrationIdResolver.resolve(request);
if (clientRegistrationId == null) {
return;
}
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(clientRegistrationId)
.principal(principal)
.build();
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
if (authorizedClient != null) {
request.getHeaders().setBearerAuth(authorizedClient.getAccessToken().getTokenValue());
}
}
private void handleAuthorizationFailure(HttpRequest request, Authentication principal, HttpHeaders headers,
HttpStatusCode httpStatus) {
OAuth2Error error = resolveOAuth2ErrorIfPossible(headers, httpStatus);
if (error == null) {
return;
}
String clientRegistrationId = this.clientRegistrationIdResolver.resolve(request);
if (clientRegistrationId == null) {
return;
}
ClientAuthorizationException authorizationException = new ClientAuthorizationException(error,
clientRegistrationId);
handleAuthorizationFailure(authorizationException, principal);
}
private static OAuth2Error resolveOAuth2ErrorIfPossible(HttpHeaders headers, HttpStatusCode httpStatus) {
String wwwAuthenticateHeader = headers.getFirst(HttpHeaders.WWW_AUTHENTICATE);
if (wwwAuthenticateHeader != null) {
Map<String, String> parameters = parseWwwAuthenticateHeader(wwwAuthenticateHeader);
if (parameters.containsKey(OAuth2ParameterNames.ERROR)) {
return new OAuth2Error(parameters.get(OAuth2ParameterNames.ERROR),
parameters.get(OAuth2ParameterNames.ERROR_DESCRIPTION),
parameters.get(OAuth2ParameterNames.ERROR_URI));
}
}
String errorCode = OAUTH2_ERROR_CODES.get(httpStatus);
if (errorCode != null) {
return new OAuth2Error(errorCode, null, "https://tools.ietf.org/html/rfc6750#section-3.1");
}
return null;
}
private static Map<String, String> parseWwwAuthenticateHeader(String wwwAuthenticateHeader) {
if (!StringUtils.hasLength(wwwAuthenticateHeader)
|| !StringUtils.startsWithIgnoreCase(wwwAuthenticateHeader, "bearer")) {
return Map.of();
}
String headerValue = wwwAuthenticateHeader.substring("bearer".length()).stripLeading();
Map<String, String> parameters = new HashMap<>();
for (String kvPair : StringUtils.delimitedListToStringArray(headerValue, ",")) {
String[] kv = StringUtils.split(kvPair, "=");
if (kv == null || kv.length <= 1) {
continue;
}
parameters.put(kv[0].trim(), kv[1].trim().replace("\"", ""));
}
return parameters;
}
private void handleAuthorizationFailure(OAuth2AuthorizationException authorizationException,
Authentication principal) {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
Map<String, Object> attributes = new HashMap<>();
if (requestAttributes != null) {
attributes.put(HttpServletRequest.class.getName(), requestAttributes.getRequest());
if (requestAttributes.getResponse() != null) {
attributes.put(HttpServletResponse.class.getName(), requestAttributes.getResponse());
}
}
this.authorizationFailureHandler.onAuthorizationFailure(authorizationException, principal, attributes);
}
/**
* A strategy for resolving a {@code clientRegistrationId} from an intercepted
* request.
*/
@FunctionalInterface
public interface ClientRegistrationIdResolver {
/**
* Resolve the {@code clientRegistrationId} from the current request, which is
* used to obtain an {@link OAuth2AuthorizedClient}.
* @param request the intercepted request, containing HTTP method, URI, headers,
* and request attributes
* @return the {@code clientRegistrationId} to be used for resolving an
* {@link OAuth2AuthorizedClient}.
*/
@Nullable
String resolve(HttpRequest request);
}
}
@@ -0,0 +1,60 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.web.client;
import java.util.Map;
import java.util.function.Consumer;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.util.Assert;
/**
* A strategy for resolving a {@code clientRegistrationId} from an intercepted request
* using {@link ClientHttpRequest#getAttributes() attributes}.
*
* @author Steve Riesenberg
* @see OAuth2ClientHttpRequestInterceptor
*/
public final class RequestAttributeClientRegistrationIdResolver
implements OAuth2ClientHttpRequestInterceptor.ClientRegistrationIdResolver {
private static final String CLIENT_REGISTRATION_ID_ATTR_NAME = RequestAttributeClientRegistrationIdResolver.class
.getName()
.concat(".clientRegistrationId");
@Override
public String resolve(HttpRequest request) {
return (String) request.getAttributes().get(CLIENT_REGISTRATION_ID_ATTR_NAME);
}
/**
* Modifies the {@link ClientHttpRequest#getAttributes() attributes} to include the
* {@link ClientRegistration#getRegistrationId() clientRegistrationId} to be used to
* look up the {@link OAuth2AuthorizedClient}.
* @param clientRegistrationId the {@link ClientRegistration#getRegistrationId()
* clientRegistrationId} to be used to look up the {@link OAuth2AuthorizedClient}
* @return the {@link Consumer} to populate the attributes
*/
public static Consumer<Map<String, Object>> clientRegistrationId(String clientRegistrationId) {
Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty");
return (attributes) -> attributes.put(CLIENT_REGISTRATION_ID_ATTR_NAME, clientRegistrationId);
}
}
@@ -0,0 +1,725 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.web.function.client;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.oauth2.client.ClientAuthorizationException;
import org.springframework.security.oauth2.client.OAuth2AuthorizationFailureHandler;
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.client.OAuth2ClientHttpRequestInterceptor;
import org.springframework.security.oauth2.client.web.client.RequestAttributeClientRegistrationIdResolver;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.TestOAuth2AccessTokens;
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.test.web.client.RequestMatcher;
import org.springframework.test.web.client.ResponseCreator;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.RestClient;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.entry;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.header;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.headerDoesNotExist;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
/**
* Tests for {@link OAuth2ClientHttpRequestInterceptor}.
*
* @author Steve Riesenberg
*/
@ExtendWith(MockitoExtension.class)
public class OAuth2ClientHttpRequestInterceptorTests {
private static final String REQUEST_URI = "/resources";
private static final String ERROR_DESCRIPTION = "The request requires higher privileges than provided by the access token.";
private static final String ERROR_URI = "https://tools.ietf.org/html/rfc6750#section-3.1";
@Mock
private OAuth2AuthorizedClientManager authorizedClientManager;
@Mock
private OAuth2AuthorizationFailureHandler authorizationFailureHandler;
@Mock
private OAuth2AuthorizedClientRepository authorizedClientRepository;
@Mock
private SecurityContextHolderStrategy securityContextHolderStrategy;
@Mock
private OAuth2AuthorizedClientService authorizedClientService;
@Mock
private OAuth2ClientHttpRequestInterceptor.ClientRegistrationIdResolver clientRegistrationIdResolver;
@Captor
private ArgumentCaptor<OAuth2AuthorizeRequest> authorizeRequestCaptor;
@Captor
private ArgumentCaptor<OAuth2AuthorizationException> authorizationExceptionCaptor;
@Captor
private ArgumentCaptor<Authentication> authenticationCaptor;
@Captor
private ArgumentCaptor<Map<String, Object>> attributesCaptor;
private ClientRegistration clientRegistration;
private OAuth2AuthorizedClient authorizedClient;
private OAuth2AuthenticationToken principal;
private OAuth2ClientHttpRequestInterceptor requestInterceptor;
private MockRestServiceServer server;
private RestClient restClient;
@BeforeEach
public void setUp() {
this.clientRegistration = TestClientRegistrations.clientRegistration().build();
OAuth2AccessToken accessToken = TestOAuth2AccessTokens.scopes("read", "write");
this.authorizedClient = new OAuth2AuthorizedClient(this.clientRegistration, "user", accessToken);
List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("OAUTH2_USER");
Map<String, Object> attributes = Map.of(StandardClaimNames.SUB, "user");
OAuth2User user = new DefaultOAuth2User(authorities, attributes, StandardClaimNames.SUB);
this.principal = new OAuth2AuthenticationToken(user, authorities, "login-client");
this.requestInterceptor = new OAuth2ClientHttpRequestInterceptor(this.authorizedClientManager);
}
@AfterEach
public void tearDown() {
SecurityContextHolder.clearContext();
RequestContextHolder.resetRequestAttributes();
}
@Test
public void constructorWhenAuthorizedClientManagerIsNullThenThrowsIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> new OAuth2ClientHttpRequestInterceptor(null))
.withMessage("authorizedClientManager cannot be null");
}
@Test
public void constructorWhenClientRegistrationIdResolverIsNullThenThrowsIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new OAuth2ClientHttpRequestInterceptor(this.authorizedClientManager, null))
.withMessage("clientRegistrationIdResolver cannot be null");
}
@Test
public void setAuthorizationFailureHandlerWhenNullThenThrowsIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.requestInterceptor.setAuthorizationFailureHandler(null))
.withMessage("authorizationFailureHandler cannot be null");
}
@Test
public void authorizationFailureHandlerWhenAuthorizedClientRepositoryIsNullThenThrowsIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> OAuth2ClientHttpRequestInterceptor
.authorizationFailureHandler((OAuth2AuthorizedClientRepository) null))
.withMessage("authorizedClientRepository cannot be null");
}
@Test
public void authorizationFailureHandlerWhenAuthorizedClientServiceIsNullThenThrowsIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> OAuth2ClientHttpRequestInterceptor
.authorizationFailureHandler((OAuth2AuthorizedClientService) null))
.withMessage("authorizedClientService cannot be null");
}
@Test
public void setSecurityContextHolderStrategyWhenNullThenThrowsIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.requestInterceptor.setSecurityContextHolderStrategy(null))
.withMessage("securityContextHolderStrategy cannot be null");
}
@Test
public void interceptWhenAnonymousThenAuthorizationHeaderNotSet() {
this.requestInterceptor.setAuthorizationFailureHandler(this.authorizationFailureHandler);
bindToRestClient(withRequestInterceptor());
this.server.expect(requestTo(REQUEST_URI))
.andExpect(headerDoesNotExist(HttpHeaders.AUTHORIZATION))
.andRespond(withApplicationJson());
performRequest(withDefaults());
this.server.verify();
verifyNoInteractions(this.authorizedClientManager, this.authorizationFailureHandler);
}
@Test
public void interceptWhenAnonymousAndAuthorizedThenAuthorizationHeaderSet() {
this.requestInterceptor.setAuthorizationFailureHandler(this.authorizationFailureHandler);
given(this.authorizedClientManager.authorize(any(OAuth2AuthorizeRequest.class)))
.willReturn(this.authorizedClient);
bindToRestClient(withRequestInterceptor());
this.server.expect(requestTo(REQUEST_URI))
.andExpect(hasAuthorizationHeader(this.authorizedClient.getAccessToken()))
.andRespond(withApplicationJson());
performRequest(withClientRegistrationId());
this.server.verify();
verify(this.authorizedClientManager).authorize(this.authorizeRequestCaptor.capture());
verifyNoMoreInteractions(this.authorizedClientManager);
verifyNoInteractions(this.authorizationFailureHandler);
OAuth2AuthorizeRequest authorizeRequest = this.authorizeRequestCaptor.getValue();
assertThat(authorizeRequest.getClientRegistrationId()).isEqualTo(this.clientRegistration.getRegistrationId());
assertThat(authorizeRequest.getPrincipal()).isInstanceOf(AnonymousAuthenticationToken.class);
}
@Test
public void interceptWhenAnonymousAndNotAuthorizedThenAuthorizationHeaderNotSet() {
this.requestInterceptor.setAuthorizationFailureHandler(this.authorizationFailureHandler);
given(this.authorizedClientManager.authorize(any(OAuth2AuthorizeRequest.class))).willReturn(null);
bindToRestClient(withRequestInterceptor());
this.server.expect(requestTo(REQUEST_URI))
.andExpect(headerDoesNotExist(HttpHeaders.AUTHORIZATION))
.andRespond(withApplicationJson());
performRequest(withClientRegistrationId());
this.server.verify();
verify(this.authorizedClientManager).authorize(this.authorizeRequestCaptor.capture());
verifyNoMoreInteractions(this.authorizedClientManager);
verifyNoInteractions(this.authorizationFailureHandler);
OAuth2AuthorizeRequest authorizeRequest = this.authorizeRequestCaptor.getValue();
assertThat(authorizeRequest.getClientRegistrationId()).isEqualTo(this.clientRegistration.getRegistrationId());
assertThat(authorizeRequest.getPrincipal()).isInstanceOf(AnonymousAuthenticationToken.class);
}
@Test
public void interceptWhenAuthenticatedAndAuthorizedThenAuthorizationHeaderSet() {
this.requestInterceptor.setAuthorizationFailureHandler(this.authorizationFailureHandler);
given(this.authorizedClientManager.authorize(any(OAuth2AuthorizeRequest.class)))
.willReturn(this.authorizedClient);
bindToRestClient(withRequestInterceptor());
this.server.expect(requestTo(REQUEST_URI))
.andExpect(hasAuthorizationHeader(this.authorizedClient.getAccessToken()))
.andRespond(withApplicationJson());
SecurityContext securityContext = new SecurityContextImpl();
securityContext.setAuthentication(this.principal);
SecurityContextHolder.setContext(securityContext);
performRequest(withClientRegistrationId());
this.server.verify();
verify(this.authorizedClientManager).authorize(this.authorizeRequestCaptor.capture());
verifyNoMoreInteractions(this.authorizedClientManager);
verifyNoInteractions(this.authorizationFailureHandler);
OAuth2AuthorizeRequest authorizeRequest = this.authorizeRequestCaptor.getValue();
assertThat(authorizeRequest.getClientRegistrationId()).isEqualTo(this.clientRegistration.getRegistrationId());
assertThat(authorizeRequest.getPrincipal()).isEqualTo(this.principal);
}
@Test
public void interceptWhenAuthenticatedAndNotAuthorizedThenAuthorizationHeaderNotSet() {
this.requestInterceptor.setAuthorizationFailureHandler(this.authorizationFailureHandler);
given(this.authorizedClientManager.authorize(any(OAuth2AuthorizeRequest.class))).willReturn(null);
bindToRestClient(withRequestInterceptor());
this.server.expect(requestTo(REQUEST_URI))
.andExpect(headerDoesNotExist(HttpHeaders.AUTHORIZATION))
.andRespond(withApplicationJson());
SecurityContext securityContext = new SecurityContextImpl();
securityContext.setAuthentication(this.principal);
SecurityContextHolder.setContext(securityContext);
performRequest(withClientRegistrationId());
this.server.verify();
verify(this.authorizedClientManager).authorize(this.authorizeRequestCaptor.capture());
verifyNoMoreInteractions(this.authorizedClientManager);
verifyNoInteractions(this.authorizationFailureHandler);
OAuth2AuthorizeRequest authorizeRequest = this.authorizeRequestCaptor.getValue();
assertThat(authorizeRequest.getClientRegistrationId()).isEqualTo(this.clientRegistration.getRegistrationId());
assertThat(authorizeRequest.getPrincipal()).isInstanceOf(OAuth2AuthenticationToken.class);
}
@Test
public void interceptWhenAnonymousAndUnauthorizedThenDoesNotCallAuthorizationFailureHandler() {
this.requestInterceptor.setAuthorizationFailureHandler(this.authorizationFailureHandler);
bindToRestClient(withRequestInterceptor());
this.server.expect(requestTo(REQUEST_URI))
.andExpect(headerDoesNotExist(HttpHeaders.AUTHORIZATION))
.andRespond(withWwwAuthenticateHeader(HttpStatus.UNAUTHORIZED));
assertThatExceptionOfType(HttpClientErrorException.class).isThrownBy(() -> performRequest(withDefaults()))
.satisfies((ex) -> assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED));
this.server.verify();
verifyNoInteractions(this.authorizedClientManager, this.authorizationFailureHandler);
}
@Test
public void interceptWhenAnonymousAndOAuth2ErrorInWwwAuthenticateHeaderThenCallsAuthorizationFailureHandlerWithInsufficientScopeError() {
this.requestInterceptor.setAuthorizationFailureHandler(this.authorizationFailureHandler);
given(this.authorizedClientManager.authorize(any(OAuth2AuthorizeRequest.class)))
.willReturn(this.authorizedClient);
bindToRestClient(withRequestInterceptor());
this.server.expect(requestTo(REQUEST_URI))
.andExpect(hasAuthorizationHeader(this.authorizedClient.getAccessToken()))
.andRespond(withWwwAuthenticateHeader(HttpStatus.OK));
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request, response));
performRequest(withClientRegistrationId());
this.server.verify();
verify(this.authorizedClientManager).authorize(any(OAuth2AuthorizeRequest.class));
verify(this.authorizationFailureHandler).onAuthorizationFailure(this.authorizationExceptionCaptor.capture(),
this.authenticationCaptor.capture(), this.attributesCaptor.capture());
verifyNoMoreInteractions(this.authorizedClientManager, this.authorizationFailureHandler);
assertThat(this.authorizationExceptionCaptor.getValue()).isInstanceOfSatisfying(
ClientAuthorizationException.class,
hasOAuth2Error(OAuth2ErrorCodes.INSUFFICIENT_SCOPE, ERROR_DESCRIPTION));
assertThat(this.authenticationCaptor.getValue()).isInstanceOf(AnonymousAuthenticationToken.class);
assertThat(this.attributesCaptor.getValue()).containsExactly(entry(HttpServletRequest.class.getName(), request),
entry(HttpServletResponse.class.getName(), response));
}
@Test
public void interceptWhenAuthenticatedAndOAuth2ErrorInWwwAuthenticateHeaderThenCallsAuthorizationFailureHandlerWithInsufficientScopeError() {
this.requestInterceptor.setAuthorizationFailureHandler(this.authorizationFailureHandler);
given(this.authorizedClientManager.authorize(any(OAuth2AuthorizeRequest.class)))
.willReturn(this.authorizedClient);
bindToRestClient(withRequestInterceptor());
this.server.expect(requestTo(REQUEST_URI))
.andExpect(hasAuthorizationHeader(this.authorizedClient.getAccessToken()))
.andRespond(withWwwAuthenticateHeader(HttpStatus.OK));
SecurityContext securityContext = new SecurityContextImpl();
securityContext.setAuthentication(this.principal);
SecurityContextHolder.setContext(securityContext);
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request, response));
performRequest(withClientRegistrationId());
this.server.verify();
verify(this.authorizedClientManager).authorize(any(OAuth2AuthorizeRequest.class));
verify(this.authorizationFailureHandler).onAuthorizationFailure(this.authorizationExceptionCaptor.capture(),
this.authenticationCaptor.capture(), this.attributesCaptor.capture());
verifyNoMoreInteractions(this.authorizedClientManager, this.authorizationFailureHandler);
assertThat(this.authorizationExceptionCaptor.getValue()).isInstanceOfSatisfying(
ClientAuthorizationException.class,
hasOAuth2Error(OAuth2ErrorCodes.INSUFFICIENT_SCOPE, ERROR_DESCRIPTION));
assertThat(this.authenticationCaptor.getValue()).isEqualTo(this.principal);
assertThat(this.attributesCaptor.getValue()).containsExactly(entry(HttpServletRequest.class.getName(), request),
entry(HttpServletResponse.class.getName(), response));
}
@Test
public void interceptWhenUnauthorizedAndOAuth2ErrorInWwwAuthenticateHeaderThenCallsAuthorizationFailureHandlerWithInsufficientScopeError() {
this.requestInterceptor.setAuthorizationFailureHandler(this.authorizationFailureHandler);
given(this.authorizedClientManager.authorize(any(OAuth2AuthorizeRequest.class)))
.willReturn(this.authorizedClient);
bindToRestClient(withRequestInterceptor());
this.server.expect(requestTo(REQUEST_URI))
.andExpect(hasAuthorizationHeader(this.authorizedClient.getAccessToken()))
.andRespond(withWwwAuthenticateHeader(HttpStatus.UNAUTHORIZED));
SecurityContext securityContext = new SecurityContextImpl();
securityContext.setAuthentication(this.principal);
SecurityContextHolder.setContext(securityContext);
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request, response));
assertThatExceptionOfType(HttpClientErrorException.class)
.isThrownBy(() -> performRequest(withClientRegistrationId()))
.satisfies((ex) -> assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED));
this.server.verify();
verify(this.authorizedClientManager).authorize(any(OAuth2AuthorizeRequest.class));
verify(this.authorizationFailureHandler).onAuthorizationFailure(this.authorizationExceptionCaptor.capture(),
this.authenticationCaptor.capture(), this.attributesCaptor.capture());
verifyNoMoreInteractions(this.authorizedClientManager, this.authorizationFailureHandler);
assertThat(this.authorizationExceptionCaptor.getValue()).isInstanceOfSatisfying(
ClientAuthorizationException.class,
hasOAuth2Error(OAuth2ErrorCodes.INSUFFICIENT_SCOPE, ERROR_DESCRIPTION));
assertThat(this.authenticationCaptor.getValue()).isEqualTo(this.principal);
assertThat(this.attributesCaptor.getValue()).containsExactly(entry(HttpServletRequest.class.getName(), request),
entry(HttpServletResponse.class.getName(), response));
}
@Test
public void interceptWhenForbiddenAndOAuth2ErrorInWwwAuthenticateHeaderThenCallsAuthorizationFailureHandlerWithInsufficientScopeError() {
this.requestInterceptor.setAuthorizationFailureHandler(this.authorizationFailureHandler);
given(this.authorizedClientManager.authorize(any(OAuth2AuthorizeRequest.class)))
.willReturn(this.authorizedClient);
bindToRestClient(withRequestInterceptor());
this.server.expect(requestTo(REQUEST_URI))
.andExpect(hasAuthorizationHeader(this.authorizedClient.getAccessToken()))
.andRespond(withWwwAuthenticateHeader(HttpStatus.FORBIDDEN));
SecurityContext securityContext = new SecurityContextImpl();
securityContext.setAuthentication(this.principal);
SecurityContextHolder.setContext(securityContext);
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request, response));
assertThatExceptionOfType(HttpClientErrorException.class)
.isThrownBy(() -> performRequest(withClientRegistrationId()))
.satisfies((ex) -> assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN));
this.server.verify();
verify(this.authorizedClientManager).authorize(any(OAuth2AuthorizeRequest.class));
verify(this.authorizationFailureHandler).onAuthorizationFailure(this.authorizationExceptionCaptor.capture(),
this.authenticationCaptor.capture(), this.attributesCaptor.capture());
verifyNoMoreInteractions(this.authorizedClientManager, this.authorizationFailureHandler);
assertThat(this.authorizationExceptionCaptor.getValue()).isInstanceOfSatisfying(
ClientAuthorizationException.class,
hasOAuth2Error(OAuth2ErrorCodes.INSUFFICIENT_SCOPE, ERROR_DESCRIPTION));
assertThat(this.authenticationCaptor.getValue()).isEqualTo(this.principal);
assertThat(this.attributesCaptor.getValue()).containsExactly(entry(HttpServletRequest.class.getName(), request),
entry(HttpServletResponse.class.getName(), response));
}
@Test
public void interceptWhenUnauthorizedThenCallsAuthorizationFailureHandlerWithInvalidTokenError() {
this.requestInterceptor.setAuthorizationFailureHandler(this.authorizationFailureHandler);
given(this.authorizedClientManager.authorize(any(OAuth2AuthorizeRequest.class)))
.willReturn(this.authorizedClient);
bindToRestClient(withRequestInterceptor());
this.server.expect(requestTo(REQUEST_URI))
.andExpect(hasAuthorizationHeader(this.authorizedClient.getAccessToken()))
.andRespond(withStatus(HttpStatus.UNAUTHORIZED));
SecurityContext securityContext = new SecurityContextImpl();
securityContext.setAuthentication(this.principal);
SecurityContextHolder.setContext(securityContext);
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request, response));
assertThatExceptionOfType(HttpClientErrorException.class)
.isThrownBy(() -> performRequest(withClientRegistrationId()))
.satisfies((ex) -> assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED));
this.server.verify();
verify(this.authorizedClientManager).authorize(any(OAuth2AuthorizeRequest.class));
verify(this.authorizationFailureHandler).onAuthorizationFailure(this.authorizationExceptionCaptor.capture(),
this.authenticationCaptor.capture(), this.attributesCaptor.capture());
verifyNoMoreInteractions(this.authorizedClientManager, this.authorizationFailureHandler);
assertThat(this.authorizationExceptionCaptor.getValue()).isInstanceOfSatisfying(
ClientAuthorizationException.class, hasOAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, null));
assertThat(this.authenticationCaptor.getValue()).isEqualTo(this.principal);
assertThat(this.attributesCaptor.getValue()).containsExactly(entry(HttpServletRequest.class.getName(), request),
entry(HttpServletResponse.class.getName(), response));
}
@Test
public void interceptWhenForbiddenThenCallsAuthorizationFailureHandlerWithInsufficientScopeError() {
this.requestInterceptor.setAuthorizationFailureHandler(this.authorizationFailureHandler);
given(this.authorizedClientManager.authorize(any(OAuth2AuthorizeRequest.class)))
.willReturn(this.authorizedClient);
bindToRestClient(withRequestInterceptor());
this.server.expect(requestTo(REQUEST_URI))
.andExpect(hasAuthorizationHeader(this.authorizedClient.getAccessToken()))
.andRespond(withStatus(HttpStatus.FORBIDDEN));
SecurityContext securityContext = new SecurityContextImpl();
securityContext.setAuthentication(this.principal);
SecurityContextHolder.setContext(securityContext);
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request, response));
assertThatExceptionOfType(HttpClientErrorException.class)
.isThrownBy(() -> performRequest(withClientRegistrationId()))
.satisfies((ex) -> assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN));
this.server.verify();
verify(this.authorizedClientManager).authorize(any(OAuth2AuthorizeRequest.class));
verify(this.authorizationFailureHandler).onAuthorizationFailure(this.authorizationExceptionCaptor.capture(),
this.authenticationCaptor.capture(), this.attributesCaptor.capture());
verifyNoMoreInteractions(this.authorizedClientManager, this.authorizationFailureHandler);
assertThat(this.authorizationExceptionCaptor.getValue()).isInstanceOfSatisfying(
ClientAuthorizationException.class, hasOAuth2Error(OAuth2ErrorCodes.INSUFFICIENT_SCOPE, null));
assertThat(this.authenticationCaptor.getValue()).isEqualTo(this.principal);
assertThat(this.attributesCaptor.getValue()).containsExactly(entry(HttpServletRequest.class.getName(), request),
entry(HttpServletResponse.class.getName(), response));
}
@Test
public void interceptWhenInternalServerErrorThenDoesNotCallAuthorizationFailureHandler() {
this.requestInterceptor.setAuthorizationFailureHandler(this.authorizationFailureHandler);
given(this.authorizedClientManager.authorize(any(OAuth2AuthorizeRequest.class)))
.willReturn(this.authorizedClient);
bindToRestClient(withRequestInterceptor());
this.server.expect(requestTo(REQUEST_URI))
.andExpect(hasAuthorizationHeader(this.authorizedClient.getAccessToken()))
.andRespond(withStatus(HttpStatus.INTERNAL_SERVER_ERROR));
assertThatExceptionOfType(HttpServerErrorException.class)
.isThrownBy(() -> performRequest(withClientRegistrationId()))
.satisfies((ex) -> assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR));
this.server.verify();
verify(this.authorizedClientManager).authorize(any(OAuth2AuthorizeRequest.class));
verifyNoMoreInteractions(this.authorizedClientManager);
verifyNoInteractions(this.authorizationFailureHandler);
}
@Test
public void interceptWhenAuthorizationExceptionThenCallsAuthorizationFailureHandlerWithException() {
this.requestInterceptor.setAuthorizationFailureHandler(this.authorizationFailureHandler);
given(this.authorizedClientManager.authorize(any(OAuth2AuthorizeRequest.class)))
.willReturn(this.authorizedClient);
bindToRestClient(withRequestInterceptor());
OAuth2AuthorizationException authorizationException = new OAuth2AuthorizationException(
new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN));
this.server.expect(requestTo(REQUEST_URI))
.andExpect(hasAuthorizationHeader(this.authorizedClient.getAccessToken()))
.andRespond(withException(authorizationException));
SecurityContext securityContext = new SecurityContextImpl();
securityContext.setAuthentication(this.principal);
SecurityContextHolder.setContext(securityContext);
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request, response));
assertThatExceptionOfType(OAuth2AuthorizationException.class)
.isThrownBy(() -> performRequest(withClientRegistrationId()))
.isEqualTo(authorizationException);
this.server.verify();
verify(this.authorizedClientManager).authorize(any(OAuth2AuthorizeRequest.class));
verify(this.authorizationFailureHandler).onAuthorizationFailure(this.authorizationExceptionCaptor.capture(),
this.authenticationCaptor.capture(), this.attributesCaptor.capture());
verifyNoMoreInteractions(this.authorizedClientManager, this.authorizationFailureHandler);
assertThat(this.authorizationExceptionCaptor.getValue()).isEqualTo(authorizationException);
assertThat(this.authenticationCaptor.getValue()).isEqualTo(this.principal);
assertThat(this.attributesCaptor.getValue()).containsExactly(entry(HttpServletRequest.class.getName(), request),
entry(HttpServletResponse.class.getName(), response));
}
@Test
public void interceptWhenUnauthorizedAndAuthorizationFailureHandlerSetWithAuthorizedClientRepositoryThenAuthorizedClientRemoved() {
this.requestInterceptor.setAuthorizationFailureHandler(
OAuth2ClientHttpRequestInterceptor.authorizationFailureHandler(this.authorizedClientRepository));
given(this.authorizedClientManager.authorize(any(OAuth2AuthorizeRequest.class)))
.willReturn(this.authorizedClient);
bindToRestClient(withRequestInterceptor());
this.server.expect(requestTo(REQUEST_URI))
.andExpect(hasAuthorizationHeader(this.authorizedClient.getAccessToken()))
.andRespond(withStatus(HttpStatus.UNAUTHORIZED));
SecurityContext securityContext = new SecurityContextImpl();
securityContext.setAuthentication(this.principal);
SecurityContextHolder.setContext(securityContext);
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request, response));
assertThatExceptionOfType(HttpClientErrorException.class)
.isThrownBy(() -> performRequest(withClientRegistrationId()))
.satisfies((ex) -> assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED));
this.server.verify();
verify(this.authorizedClientManager).authorize(any(OAuth2AuthorizeRequest.class));
verify(this.authorizedClientRepository).removeAuthorizedClient(this.clientRegistration.getRegistrationId(),
this.principal, request, response);
verifyNoMoreInteractions(this.authorizedClientManager, this.authorizedClientRepository);
}
@Test
public void interceptWhenUnauthorizedAndAuthorizationFailureHandlerSetWithAuthorizedClientServiceThenAuthorizedClientRemoved() {
this.requestInterceptor.setAuthorizationFailureHandler(
OAuth2ClientHttpRequestInterceptor.authorizationFailureHandler(this.authorizedClientService));
given(this.authorizedClientManager.authorize(any(OAuth2AuthorizeRequest.class)))
.willReturn(this.authorizedClient);
bindToRestClient(withRequestInterceptor());
this.server.expect(requestTo(REQUEST_URI))
.andExpect(hasAuthorizationHeader(this.authorizedClient.getAccessToken()))
.andRespond(withStatus(HttpStatus.UNAUTHORIZED));
SecurityContext securityContext = new SecurityContextImpl();
securityContext.setAuthentication(this.principal);
SecurityContextHolder.setContext(securityContext);
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request, response));
assertThatExceptionOfType(HttpClientErrorException.class)
.isThrownBy(() -> performRequest(withClientRegistrationId()))
.satisfies((ex) -> assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED));
this.server.verify();
verify(this.authorizedClientManager).authorize(any(OAuth2AuthorizeRequest.class));
verify(this.authorizedClientService).removeAuthorizedClient(this.clientRegistration.getRegistrationId(),
this.principal.getName());
verifyNoMoreInteractions(this.authorizedClientManager, this.authorizedClientService);
}
@Test
public void interceptWhenClientRegistrationIdResolverSetThenUsed() {
this.requestInterceptor = new OAuth2ClientHttpRequestInterceptor(this.authorizedClientManager,
this.clientRegistrationIdResolver);
this.requestInterceptor.setAuthorizationFailureHandler(this.authorizationFailureHandler);
given(this.authorizedClientManager.authorize(any(OAuth2AuthorizeRequest.class)))
.willReturn(this.authorizedClient);
String clientRegistrationId = "test-client";
given(this.clientRegistrationIdResolver.resolve(any(HttpRequest.class))).willReturn(clientRegistrationId);
bindToRestClient(withRequestInterceptor());
this.server.expect(requestTo(REQUEST_URI))
.andExpect(hasAuthorizationHeader(this.authorizedClient.getAccessToken()))
.andRespond(withApplicationJson());
SecurityContext securityContext = new SecurityContextImpl();
securityContext.setAuthentication(this.principal);
SecurityContextHolder.setContext(securityContext);
performRequest(withDefaults());
this.server.verify();
verify(this.authorizedClientManager).authorize(this.authorizeRequestCaptor.capture());
verify(this.clientRegistrationIdResolver).resolve(any(HttpRequest.class));
verifyNoMoreInteractions(this.clientRegistrationIdResolver, this.authorizedClientManager);
verifyNoInteractions(this.authorizationFailureHandler);
OAuth2AuthorizeRequest authorizeRequest = this.authorizeRequestCaptor.getValue();
assertThat(authorizeRequest.getClientRegistrationId()).isEqualTo(clientRegistrationId);
assertThat(authorizeRequest.getPrincipal()).isEqualTo(this.principal);
}
@Test
public void interceptWhenCustomSecurityContextHolderStrategySetThenUsed() {
this.requestInterceptor.setSecurityContextHolderStrategy(this.securityContextHolderStrategy);
given(this.authorizedClientManager.authorize(any(OAuth2AuthorizeRequest.class)))
.willReturn(this.authorizedClient);
bindToRestClient(withRequestInterceptor());
this.server.expect(requestTo(REQUEST_URI))
.andExpect(hasAuthorizationHeader(this.authorizedClient.getAccessToken()))
.andRespond(withApplicationJson());
SecurityContext securityContext = new SecurityContextImpl();
securityContext.setAuthentication(this.principal);
given(this.securityContextHolderStrategy.getContext()).willReturn(securityContext);
performRequest(withClientRegistrationId());
this.server.verify();
verify(this.authorizedClientManager).authorize(this.authorizeRequestCaptor.capture());
verify(this.securityContextHolderStrategy).getContext();
verifyNoMoreInteractions(this.authorizedClientManager);
OAuth2AuthorizeRequest authorizeRequest = this.authorizeRequestCaptor.getValue();
assertThat(authorizeRequest.getClientRegistrationId()).isEqualTo(this.clientRegistration.getRegistrationId());
assertThat(authorizeRequest.getPrincipal()).isEqualTo(this.principal);
}
private void bindToRestClient(Consumer<RestClient.Builder> customizer) {
RestClient.Builder builder = RestClient.builder();
customizer.accept(builder);
this.server = MockRestServiceServer.bindTo(builder).build();
this.restClient = builder.build();
}
private Consumer<RestClient.Builder> withRequestInterceptor() {
return (builder) -> builder.requestInterceptor(this.requestInterceptor);
}
private static RequestMatcher hasAuthorizationHeader(OAuth2AccessToken accessToken) {
String tokenType = accessToken.getTokenType().getValue();
String tokenValue = accessToken.getTokenValue();
return header(HttpHeaders.AUTHORIZATION, "%s %s".formatted(tokenType, tokenValue));
}
private static ResponseCreator withApplicationJson() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return withSuccess().headers(headers).body("{}");
}
private static ResponseCreator withWwwAuthenticateHeader(HttpStatus httpStatus) {
String wwwAuthenticateHeader = "Bearer error=\"insufficient_scope\", "
+ "error_description=\"The request requires higher privileges than provided by the access token.\", "
+ "error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\"";
HttpHeaders headers = new HttpHeaders();
headers.set(HttpHeaders.WWW_AUTHENTICATE, wwwAuthenticateHeader);
return withStatus(httpStatus).headers(headers);
}
private static ResponseCreator withException(OAuth2AuthorizationException ex) {
return (request) -> {
throw ex;
};
}
private void performRequest(Consumer<RestClient.RequestHeadersSpec<?>> customizer) {
RestClient.RequestHeadersSpec<?> spec = this.restClient.get().uri(REQUEST_URI);
customizer.accept(spec);
spec.retrieve().toBodilessEntity();
}
private static Consumer<RestClient.RequestHeadersSpec<?>> withDefaults() {
return (spec) -> {
};
}
private Consumer<RestClient.RequestHeadersSpec<?>> withClientRegistrationId() {
return (spec) -> spec.attributes(RequestAttributeClientRegistrationIdResolver
.clientRegistrationId(this.clientRegistration.getRegistrationId()));
}
private Consumer<ClientAuthorizationException> hasOAuth2Error(String errorCode, String errorDescription) {
return (ex) -> {
assertThat(ex.getClientRegistrationId()).isEqualTo(this.clientRegistration.getRegistrationId());
assertThat(ex.getError().getErrorCode()).isEqualTo(errorCode);
assertThat(ex.getError().getDescription()).isEqualTo(errorDescription);
assertThat(ex.getError().getUri()).isEqualTo(ERROR_URI);
assertThat(ex).hasNoCause();
assertThat(ex).hasMessageContaining(errorCode);
};
}
}