Kaynağa Gözat

Add RestClient interceptor

Closes gh-13588
Steve Riesenberg 1 yıl önce
ebeveyn
işleme
e3c19ba86c

+ 266 - 71
docs/modules/ROOT/pages/servlet/oauth2/index.adoc

@@ -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
 

+ 381 - 0
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/OAuth2ClientHttpRequestInterceptor.java

@@ -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);
+
+	}
+
+}

+ 60 - 0
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/client/RequestAttributeClientRegistrationIdResolver.java

@@ -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);
+	}
+
+}

+ 725 - 0
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/function/client/OAuth2ClientHttpRequestInterceptorTests.java

@@ -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);
+		};
+	}
+
+}