4 changed files with 1432 additions and 71 deletions
@ -0,0 +1,381 @@
@@ -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 @@
@@ -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 @@
@@ -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); |
||||
}; |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue