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); |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
Loading…
Reference in new issue