parent
b294816600
commit
e3c19ba86c
|
@ -398,13 +398,14 @@ See xref:getting-spring-security.adoc[] for additional options when not using Sp
|
|||
|
||||
Consider the following use cases for OAuth2 Client:
|
||||
|
||||
* <<oauth2-client-log-users-in,I want to log users in using OAuth 2.0 or OpenID Connect 1.0>>
|
||||
* <<oauth2-client-access-protected-resources,I want to obtain an access token for users in order to access a third-party API>>
|
||||
* <<oauth2-client-access-protected-resources-current-user,I want to do both>> (log users in _and_ access a third-party API)
|
||||
* <<oauth2-client-enable-extension-grant-type,I want to enable an extension grant type>>
|
||||
* <<oauth2-client-customize-existing-grant-type,I want to customize an existing grant type>>
|
||||
* <<oauth2-client-customize-request-parameters,I want to customize token request parameters>>
|
||||
* <<oauth2-client-customize-rest-operations,I want to customize the `RestOperations` used by OAuth2 Client components>>
|
||||
* I want to <<oauth2-client-log-users-in,log users in using OAuth 2.0 or OpenID Connect 1.0>>
|
||||
* I want to <<oauth2-client-access-protected-resources,use `RestClient` to obtain an access token for users in order to access a third-party API>>
|
||||
* I want to <<oauth2-client-access-protected-resources-current-user,do both>> (log users in _and_ access a third-party API)
|
||||
* I want to <<oauth2-client-access-protected-resources-webclient,use `WebClient` to obtain an access token for users in order to access a third-party API>>
|
||||
* I want to <<oauth2-client-enable-extension-grant-type,enable an extension grant type>>
|
||||
* I want to <<oauth2-client-customize-existing-grant-type,customize an existing grant type>>
|
||||
* I want to <<oauth2-client-customize-request-parameters,customize token request parameters>>
|
||||
* I want to <<oauth2-client-customize-rest-operations,customize the `RestOperations` used by OAuth2 Client components>>
|
||||
|
||||
[[oauth2-client-log-users-in]]
|
||||
=== Log Users In with OAuth2
|
||||
|
@ -584,38 +585,11 @@ Spring Security provides implementations of `OAuth2AuthorizedClientManager` for
|
|||
Spring Security registers a default `OAuth2AuthorizedClientManager` bean for you when one does not exist.
|
||||
====
|
||||
|
||||
The easiest way to use an `OAuth2AuthorizedClientManager` is via an `ExchangeFilterFunction` that intercepts requests through a `WebClient`.
|
||||
To use `WebClient`, you will need to add the `spring-webflux` dependency along with a reactive client implementation:
|
||||
The easiest way to use an `OAuth2AuthorizedClientManager` is via a `ClientHttpRequestInterceptor` that intercepts requests through a `RestClient`, which is already available when `spring-web` is on the classpath.
|
||||
|
||||
.Add Spring WebFlux Dependency
|
||||
[tabs]
|
||||
======
|
||||
Gradle::
|
||||
+
|
||||
[source,gradle,role="primary"]
|
||||
----
|
||||
implementation 'org.springframework:spring-webflux'
|
||||
implementation 'io.projectreactor.netty:reactor-netty'
|
||||
----
|
||||
The following example uses the default `OAuth2AuthorizedClientManager` to configure a `RestClient` capable of accessing protected resources by placing `Bearer` tokens in the `Authorization` header of each request:
|
||||
|
||||
Maven::
|
||||
+
|
||||
[source,maven,role="secondary"]
|
||||
----
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-webflux</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor.netty</groupId>
|
||||
<artifactId>reactor-netty</artifactId>
|
||||
</dependency>
|
||||
----
|
||||
======
|
||||
|
||||
The following example uses the default `OAuth2AuthorizedClientManager` to configure a `WebClient` capable of accessing protected resources by placing `Bearer` tokens in the `Authorization` header of each request:
|
||||
|
||||
.Configure `WebClient` with `ExchangeFilterFunction`
|
||||
.Configure `RestClient` with `ClientHttpRequestInterceptor`
|
||||
[tabs]
|
||||
=====
|
||||
Java::
|
||||
|
@ -623,14 +597,14 @@ Java::
|
|||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
public class WebClientConfig {
|
||||
public class RestClientConfig {
|
||||
|
||||
@Bean
|
||||
public WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
|
||||
ServletOAuth2AuthorizedClientExchangeFilterFunction filter =
|
||||
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
|
||||
return WebClient.builder()
|
||||
.apply(filter.oauth2Configuration())
|
||||
public RestClient restClient(OAuth2AuthorizedClientManager authorizedClientManager) {
|
||||
OAuth2ClientHttpRequestInterceptor requestInterceptor =
|
||||
new OAuth2ClientHttpRequestInterceptor(authorizedClientManager);
|
||||
return RestClient.builder()
|
||||
.requestInterceptor(requestInterceptor)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
@ -642,13 +616,13 @@ Kotlin::
|
|||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Configuration
|
||||
class WebClientConfig {
|
||||
class RestClientConfig {
|
||||
|
||||
@Bean
|
||||
fun webClient(authorizedClientManager: OAuth2AuthorizedClientManager): WebClient {
|
||||
val filter = ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager)
|
||||
return WebClient.builder()
|
||||
.apply(filter.oauth2Configuration())
|
||||
fun restClient(authorizedClientManager: OAuth2AuthorizedClientManager): RestClient {
|
||||
val requestInterceptor = OAuth2ClientHttpRequestInterceptor(authorizedClientManager)
|
||||
return RestClient.builder()
|
||||
.requestInterceptor(requestInterceptor)
|
||||
.build()
|
||||
}
|
||||
|
||||
|
@ -656,35 +630,35 @@ class WebClientConfig {
|
|||
----
|
||||
=====
|
||||
|
||||
This configured `WebClient` can be used as in the following example:
|
||||
This configured `RestClient` can be used as in the following example:
|
||||
|
||||
[[oauth2-client-accessing-protected-resources-example]]
|
||||
.Use `WebClient` to Access Protected Resources
|
||||
.Use `RestClient` to Access Protected Resources
|
||||
[tabs]
|
||||
=====
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
import static org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.clientRegistrationId;
|
||||
import static org.springframework.security.oauth2.client.web.client.RequestAttributeClientRegistrationIdResolver.clientRegistrationId;
|
||||
|
||||
@RestController
|
||||
public class MessagesController {
|
||||
|
||||
private final WebClient webClient;
|
||||
private final RestClient restClient;
|
||||
|
||||
public MessagesController(WebClient webClient) {
|
||||
this.webClient = webClient;
|
||||
public MessagesController(RestClient restClient) {
|
||||
this.restClient = restClient;
|
||||
}
|
||||
|
||||
@GetMapping("/messages")
|
||||
public ResponseEntity<List<Message>> messages() {
|
||||
return this.webClient.get()
|
||||
Message[] messages = this.restClient.get()
|
||||
.uri("http://localhost:8090/messages")
|
||||
.attributes(clientRegistrationId("my-oauth2-client"))
|
||||
.retrieve()
|
||||
.toEntityList(Message.class)
|
||||
.block();
|
||||
.body(Message[].class);
|
||||
return ResponseEntity.ok(Arrays.asList(messages));
|
||||
}
|
||||
|
||||
public record Message(String message) {
|
||||
|
@ -697,19 +671,21 @@ Kotlin::
|
|||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.clientRegistrationId
|
||||
import org.springframework.security.oauth2.client.web.client.RequestAttributeClientRegistrationIdResolver.clientRegistrationId
|
||||
import org.springframework.web.client.body
|
||||
|
||||
@RestController
|
||||
class MessagesController(private val webClient: WebClient) {
|
||||
class MessagesController(private val restClient: RestClient) {
|
||||
|
||||
@GetMapping("/messages")
|
||||
fun messages(): ResponseEntity<List<Message>> {
|
||||
return webClient.get()
|
||||
val messages = restClient.get()
|
||||
.uri("http://localhost:8090/messages")
|
||||
.attributes(clientRegistrationId("my-oauth2-client"))
|
||||
.retrieve()
|
||||
.toEntityList<Message>()
|
||||
.block()!!
|
||||
.body<Array<Message>>()!!
|
||||
.toList()
|
||||
return ResponseEntity.ok(messages)
|
||||
}
|
||||
|
||||
data class Message(val message: String)
|
||||
|
@ -815,7 +791,227 @@ Spring Security provides implementations of `OAuth2AuthorizedClientManager` for
|
|||
Spring Security registers a default `OAuth2AuthorizedClientManager` bean for you when one does not exist.
|
||||
====
|
||||
|
||||
The easiest way to use an `OAuth2AuthorizedClientManager` is via an `ExchangeFilterFunction` that intercepts requests through a `WebClient`.
|
||||
The easiest way to use an `OAuth2AuthorizedClientManager` is via a `ClientHttpRequestInterceptor` that intercepts requests through a `RestClient`, which is already available when `spring-web` is on the classpath.
|
||||
|
||||
The following example uses the default `OAuth2AuthorizedClientManager` to configure a `RestClient` capable of accessing protected resources by placing `Bearer` tokens in the `Authorization` header of each request:
|
||||
|
||||
.Configure `RestClient` with `ClientHttpRequestInterceptor`
|
||||
[tabs]
|
||||
=====
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
public class RestClientConfig {
|
||||
|
||||
@Bean
|
||||
public RestClient restClient(OAuth2AuthorizedClientManager authorizedClientManager) {
|
||||
OAuth2ClientHttpRequestInterceptor requestInterceptor =
|
||||
new OAuth2ClientHttpRequestInterceptor(authorizedClientManager, clientRegistrationIdResolver());
|
||||
return RestClient.builder()
|
||||
.requestInterceptor(requestInterceptor)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static ClientRegistrationIdResolver clientRegistrationIdResolver() {
|
||||
return (request) -> {
|
||||
Authentication authentication = SecurityContextHolder.getAuthentication();
|
||||
return (authentication instanceof OAuth2AuthenticationToken principal)
|
||||
? principal.getAuthorizedClientRegistrationId()
|
||||
: null;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Configuration
|
||||
class RestClientConfig {
|
||||
|
||||
@Bean
|
||||
fun restClient(authorizedClientManager: OAuth2AuthorizedClientManager): RestClient {
|
||||
val requestInterceptor = OAuth2ClientHttpRequestInterceptor(authorizedClientManager, clientRegistrationIdResolver())
|
||||
return RestClient.builder()
|
||||
.requestInterceptor(requestInterceptor)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun clientRegistrationIdResolver(): OAuth2ClientHttpRequestInterceptor.ClientRegistrationIdResolver {
|
||||
return OAuth2ClientHttpRequestInterceptor.ClientRegistrationIdResolver { request ->
|
||||
val authentication = SecurityContextHolder.getContext().authentication
|
||||
if (authentication is OAuth2AuthenticationToken) {
|
||||
authentication.authorizedClientRegistrationId
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
=====
|
||||
|
||||
This configured `RestClient` can be used as in the following example:
|
||||
|
||||
[[oauth2-client-accessing-protected-resources-current-user-example]]
|
||||
.Use `RestClient` to Access Protected Resources (Current User)
|
||||
[tabs]
|
||||
=====
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@RestController
|
||||
public class MessagesController {
|
||||
|
||||
private final RestClient restClient;
|
||||
|
||||
public MessagesController(RestClient restClient) {
|
||||
this.restClient = restClient;
|
||||
}
|
||||
|
||||
@GetMapping("/messages")
|
||||
public ResponseEntity<List<Message>> messages() {
|
||||
Message[] messages = this.restClient.get()
|
||||
.uri("http://localhost:8090/messages")
|
||||
.retrieve()
|
||||
.body(Message[].class);
|
||||
return ResponseEntity.ok(Arrays.asList(messages));
|
||||
}
|
||||
|
||||
public record Message(String message) {
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
import org.springframework.web.client.body
|
||||
|
||||
@RestController
|
||||
class MessagesController(private val restClient: RestClient) {
|
||||
|
||||
@GetMapping("/messages")
|
||||
fun messages(): ResponseEntity<List<Message>> {
|
||||
val messages = restClient.get()
|
||||
.uri("http://localhost:8090/messages")
|
||||
.retrieve()
|
||||
.body<Array<Message>>()!!
|
||||
.toList()
|
||||
return ResponseEntity.ok(messages)
|
||||
}
|
||||
|
||||
data class Message(val message: String)
|
||||
|
||||
}
|
||||
----
|
||||
=====
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
Unlike the <<oauth2-client-accessing-protected-resources-example,previous example>>, notice that we do not need to tell Spring Security about the `clientRegistrationId` we'd like to use.
|
||||
This is because it can be derived from the currently logged in user.
|
||||
====
|
||||
|
||||
[[oauth2-client-access-protected-resources-webclient]]
|
||||
=== Access Protected Resources with `WebClient`
|
||||
|
||||
Making requests to a third party API that is protected by OAuth2 is a core use case of OAuth2 Client.
|
||||
This is accomplished by authorizing a client (represented by the `OAuth2AuthorizedClient` class in Spring Security) and accessing protected resources by placing a `Bearer` token in the `Authorization` header of an outbound request.
|
||||
|
||||
The following example configures the application to act as an OAuth2 Client capable of requesting protected resources from a third party API:
|
||||
|
||||
.Configure OAuth2 Client
|
||||
[tabs]
|
||||
=====
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
// ...
|
||||
.oauth2Client(Customizer.withDefaults());
|
||||
return http.build();
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
import org.springframework.security.config.annotation.web.invoke
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
http {
|
||||
// ...
|
||||
oauth2Client { }
|
||||
}
|
||||
|
||||
return http.build()
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
=====
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
The above example does not provide a way to log users in.
|
||||
You can use any other login mechanism (such as `formLogin()`).
|
||||
See the <<oauth2-client-access-protected-resources-current-user,previous section>> for an example combining `oauth2Client()` with `oauth2Login()`.
|
||||
====
|
||||
|
||||
In addition to the above configuration, the application requires at least one `ClientRegistration` to be configured through the use of a `ClientRegistrationRepository` bean.
|
||||
The following example configures an `InMemoryClientRegistrationRepository` bean using Spring Boot configuration properties:
|
||||
|
||||
[source,yaml]
|
||||
----
|
||||
spring:
|
||||
security:
|
||||
oauth2:
|
||||
client:
|
||||
registration:
|
||||
my-oauth2-client:
|
||||
provider: my-auth-server
|
||||
client-id: my-client-id
|
||||
client-secret: my-client-secret
|
||||
authorization-grant-type: authorization_code
|
||||
scope: message.read,message.write
|
||||
provider:
|
||||
my-auth-server:
|
||||
issuer-uri: https://my-auth-server.com
|
||||
----
|
||||
|
||||
In addition to configuring Spring Security to support OAuth2 Client features, you will also need to decide how you will be accessing protected resources and configure your application accordingly.
|
||||
Spring Security provides implementations of `OAuth2AuthorizedClientManager` for obtaining access tokens that can be used to access protected resources.
|
||||
|
||||
[TIP]
|
||||
====
|
||||
Spring Security registers a default `OAuth2AuthorizedClientManager` bean for you when one does not exist.
|
||||
====
|
||||
|
||||
Another way to use an `OAuth2AuthorizedClientManager` is via an `ExchangeFilterFunction` that intercepts requests through a `WebClient`.
|
||||
To use `WebClient`, you will need to add the `spring-webflux` dependency along with a reactive client implementation:
|
||||
|
||||
.Add Spring WebFlux Dependency
|
||||
|
@ -889,14 +1085,15 @@ class WebClientConfig {
|
|||
|
||||
This configured `WebClient` can be used as in the following example:
|
||||
|
||||
[[oauth2-client-accessing-protected-resources-current-user-example]]
|
||||
.Use `WebClient` to Access Protected Resources (Current User)
|
||||
.Use `WebClient` to Access Protected Resources
|
||||
[tabs]
|
||||
=====
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
import static org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.clientRegistrationId;
|
||||
|
||||
@RestController
|
||||
public class MessagesController {
|
||||
|
||||
|
@ -910,6 +1107,7 @@ public class MessagesController {
|
|||
public ResponseEntity<List<Message>> messages() {
|
||||
return this.webClient.get()
|
||||
.uri("http://localhost:8090/messages")
|
||||
.attributes(clientRegistrationId("my-oauth2-client"))
|
||||
.retrieve()
|
||||
.toEntityList(Message.class)
|
||||
.block();
|
||||
|
@ -925,6 +1123,8 @@ Kotlin::
|
|||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.clientRegistrationId
|
||||
|
||||
@RestController
|
||||
class MessagesController(private val webClient: WebClient) {
|
||||
|
||||
|
@ -932,6 +1132,7 @@ class MessagesController(private val webClient: WebClient) {
|
|||
fun messages(): ResponseEntity<List<Message>> {
|
||||
return webClient.get()
|
||||
.uri("http://localhost:8090/messages")
|
||||
.attributes(clientRegistrationId("my-oauth2-client"))
|
||||
.retrieve()
|
||||
.toEntityList<Message>()
|
||||
.block()!!
|
||||
|
@ -943,12 +1144,6 @@ class MessagesController(private val webClient: WebClient) {
|
|||
----
|
||||
=====
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
Unlike the <<oauth2-client-accessing-protected-resources-example,previous example>>, notice that we do not need to tell Spring Security about the `clientRegistrationId` we'd like to use.
|
||||
This is because it can be derived from the currently logged in user.
|
||||
====
|
||||
|
||||
[[oauth2-client-enable-extension-grant-type]]
|
||||
=== Enable an Extension Grant Type
|
||||
|
||||
|
|
|
@ -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