Explorar o código

Add support for OAuth 2.0 Token Exchange Grant

Issue gh-60
Steve Riesenberg hai 1 ano
pai
achega
25a785de49
Modificáronse 16 ficheiros con 1129 adicións e 12 borrados
  1. 2 2
      docs/modules/ROOT/pages/protocol-endpoints.adoc
  2. 8 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/aot/hint/OAuth2AuthorizationServerBeanRegistrationAotProcessor.java
  3. 53 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ActorAuthenticationToken.java
  4. 69 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2CompositeAuthenticationToken.java
  5. 314 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenExchangeAuthenticationProvider.java
  6. 164 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenExchangeAuthenticationToken.java
  7. 46 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/DelegatingOAuth2TokenCustomizer.java
  8. 30 2
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ConfigurerUtils.java
  9. 7 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenEndpointConfigurer.java
  10. 85 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenExchangeTokenCustomizers.java
  11. 44 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2ActorAuthenticationTokenMixin.java
  12. 6 1
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2AuthorizationServerJackson2Module.java
  13. 48 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2CompositeAuthenticationTokenMixin.java
  14. 3 1
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilter.java
  15. 241 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2TokenExchangeAuthenticationConverter.java
  16. 9 6
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientCredentialsGrantTests.java

+ 2 - 2
docs/modules/ROOT/pages/protocol-endpoints.adoc

@@ -261,8 +261,8 @@ The supported https://datatracker.ietf.org/doc/html/rfc6749#section-1.3[authoriz
 
 
 `OAuth2TokenEndpointFilter` is configured with the following defaults:
 `OAuth2TokenEndpointFilter` is configured with the following defaults:
 
 
-* `*AuthenticationConverter*` -- A `DelegatingAuthenticationConverter` composed of `OAuth2AuthorizationCodeAuthenticationConverter`, `OAuth2RefreshTokenAuthenticationConverter`, `OAuth2ClientCredentialsAuthenticationConverter`, and `OAuth2DeviceCodeAuthenticationConverter`.
-* `*AuthenticationManager*` -- An `AuthenticationManager` composed of `OAuth2AuthorizationCodeAuthenticationProvider`, `OAuth2RefreshTokenAuthenticationProvider`, `OAuth2ClientCredentialsAuthenticationProvider`, and `OAuth2DeviceCodeAuthenticationProvider`.
+* `*AuthenticationConverter*` -- A `DelegatingAuthenticationConverter` composed of `OAuth2AuthorizationCodeAuthenticationConverter`, `OAuth2RefreshTokenAuthenticationConverter`, `OAuth2ClientCredentialsAuthenticationConverter`, `OAuth2DeviceCodeAuthenticationConverter`, and `OAuth2TokenExchangeAuthenticationConverter`.
+* `*AuthenticationManager*` -- An `AuthenticationManager` composed of `OAuth2AuthorizationCodeAuthenticationProvider`, `OAuth2RefreshTokenAuthenticationProvider`, `OAuth2ClientCredentialsAuthenticationProvider`, `OAuth2DeviceCodeAuthenticationProvider`, and `OAuth2TokenExchangeAuthenticationProvider`.
 * `*AuthenticationSuccessHandler*` -- An `OAuth2AccessTokenResponseAuthenticationSuccessHandler`.
 * `*AuthenticationSuccessHandler*` -- An `OAuth2AccessTokenResponseAuthenticationSuccessHandler`.
 * `*AuthenticationFailureHandler*` -- An `OAuth2ErrorAuthenticationFailureHandler`.
 * `*AuthenticationFailureHandler*` -- An `OAuth2ErrorAuthenticationFailureHandler`.
 
 

+ 8 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/aot/hint/OAuth2AuthorizationServerBeanRegistrationAotProcessor.java

@@ -43,6 +43,8 @@ import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
 import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
 import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
 import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
 import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
 import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
 import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ActorAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2CompositeAuthenticationToken;
 import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module;
 import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module;
 import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
 import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
 import org.springframework.security.web.authentication.WebAuthenticationDetails;
 import org.springframework.security.web.authentication.WebAuthenticationDetails;
@@ -109,7 +111,9 @@ class OAuth2AuthorizationServerBeanRegistrationAotProcessor implements BeanRegis
 							TypeReference.of(OidcIdToken.class),
 							TypeReference.of(OidcIdToken.class),
 							TypeReference.of(AbstractOAuth2Token.class),
 							TypeReference.of(AbstractOAuth2Token.class),
 							TypeReference.of(OidcUserInfo.class),
 							TypeReference.of(OidcUserInfo.class),
+							TypeReference.of(OAuth2ActorAuthenticationToken.class),
 							TypeReference.of(OAuth2AuthorizationRequest.class),
 							TypeReference.of(OAuth2AuthorizationRequest.class),
+							TypeReference.of(OAuth2CompositeAuthenticationToken.class),
 							TypeReference.of(AuthorizationGrantType.class),
 							TypeReference.of(AuthorizationGrantType.class),
 							TypeReference.of(OAuth2AuthorizationResponseType.class),
 							TypeReference.of(OAuth2AuthorizationResponseType.class),
 							TypeReference.of(OAuth2TokenFormat.class)
 							TypeReference.of(OAuth2TokenFormat.class)
@@ -150,8 +154,12 @@ class OAuth2AuthorizationServerBeanRegistrationAotProcessor implements BeanRegis
 					loadClass("org.springframework.security.jackson2.UserMixin"));
 					loadClass("org.springframework.security.jackson2.UserMixin"));
 			this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
 			this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
 					loadClass("org.springframework.security.jackson2.SimpleGrantedAuthorityMixin"));
 					loadClass("org.springframework.security.jackson2.SimpleGrantedAuthorityMixin"));
+			this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
+					loadClass("org.springframework.security.oauth2.server.authorization.jackson2.OAuth2ActorAuthenticationTokenMixin"));
 			this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
 			this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
 					loadClass("org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationRequestMixin"));
 					loadClass("org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationRequestMixin"));
+			this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
+					loadClass("org.springframework.security.oauth2.server.authorization.jackson2.OAuth2CompositeAuthenticationTokenMixin"));
 			this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
 			this.reflectionHintsRegistrar.registerReflectionHints(hints.reflection(),
 					loadClass("org.springframework.security.oauth2.server.authorization.jackson2.OAuth2TokenFormatMixin"));
 					loadClass("org.springframework.security.oauth2.server.authorization.jackson2.OAuth2TokenFormatMixin"));
 
 

+ 53 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ActorAuthenticationToken.java

@@ -0,0 +1,53 @@
+/*
+ * Copyright 2020-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.server.authorization.authentication;
+
+import java.io.Serializable;
+import java.util.Collections;
+
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link Authentication} implementation used for the OAuth 2.0 Token Exchange Grant
+ * to represent an actor in a composite token (e.g. the "delegation" use case).
+ *
+ * @author Steve Riesenberg
+ * @since 1.3
+ * @see OAuth2CompositeAuthenticationToken
+ */
+public class OAuth2ActorAuthenticationToken extends AbstractAuthenticationToken implements Serializable {
+
+	private final String name;
+
+	public OAuth2ActorAuthenticationToken(String name) {
+		super(Collections.emptyList());
+		Assert.hasText(name, "name cannot be empty");
+		this.name = name;
+	}
+
+	@Override
+	public Object getPrincipal() {
+		return this.name;
+	}
+
+	@Override
+	public Object getCredentials() {
+		return null;
+	}
+}

+ 69 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2CompositeAuthenticationToken.java

@@ -0,0 +1,69 @@
+/*
+ * Copyright 2020-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.server.authorization.authentication;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link Authentication} implementation used for the OAuth 2.0 Token Exchange Grant
+ * to represent the principal in a composite token (e.g. the "delegation" use case).
+ *
+ * @author Steve Riesenberg
+ * @since 1.3
+ * @see OAuth2TokenExchangeAuthenticationToken
+ */
+public class OAuth2CompositeAuthenticationToken extends AbstractAuthenticationToken implements Serializable {
+
+	private final Authentication subject;
+
+	private final List<Authentication> actors;
+
+	public OAuth2CompositeAuthenticationToken(Authentication subject, List<Authentication> actors) {
+		super(subject != null ? subject.getAuthorities() : null);
+		Assert.notNull(subject, "subject cannot be null");
+		Assert.notNull(actors, "actors cannot be null");
+		this.subject = subject;
+		this.actors = Collections.unmodifiableList(new ArrayList<>(actors));
+		setDetails(subject.getDetails());
+		setAuthenticated(subject.isAuthenticated());
+	}
+
+	@Override
+	public Object getPrincipal() {
+		return this.subject.getPrincipal();
+	}
+
+	@Override
+	public Object getCredentials() {
+		return null;
+	}
+
+	public Authentication getSubject() {
+		return this.subject;
+	}
+
+	public List<Authentication> getActors() {
+		return this.actors;
+	}
+}

+ 314 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenExchangeAuthenticationProvider.java

@@ -0,0 +1,314 @@
+/*
+ * Copyright 2020-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.server.authorization.authentication;
+
+import java.security.Principal;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.ClaimAccessor;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.core.OAuth2Token;
+import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
+import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
+import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+import static org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient;
+
+/**
+ * An {@link AuthenticationProvider} implementation for the OAuth 2.0 Token Exchange Grant.
+ *
+ * @author Steve Riesenberg
+ * @since 1.3
+ * @see OAuth2TokenExchangeAuthenticationToken
+ * @see OAuth2AccessTokenAuthenticationToken
+ * @see OAuth2AuthorizationService
+ * @see OAuth2TokenGenerator
+ * @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc8693#section-1">Section 1 Introduction</a>
+ * @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc8693#section-2.1">Section 2.1 Request</a>
+ */
+public final class OAuth2TokenExchangeAuthenticationProvider implements AuthenticationProvider {
+
+	private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
+
+	private static final AuthorizationGrantType TOKEN_EXCHANGE = new AuthorizationGrantType(
+			"urn:ietf:params:oauth:grant-type:token-exchange");
+
+	private static final String JWT_TOKEN_TYPE_VALUE = "urn:ietf:params:oauth:token-type:jwt";
+
+	private static final String MAY_ACT = "may_act";
+
+	private static final String ISSUED_TOKEN_TYPE = "issued_token_type";
+
+	private final Log logger = LogFactory.getLog(getClass());
+
+	private final OAuth2AuthorizationService authorizationService;
+
+	private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
+
+	/**
+	 * Constructs an {@code OAuth2TokenExchangeAuthenticationProvider} using the provided parameters.
+	 *
+	 * @param authorizationService the authorization service
+	 * @param tokenGenerator the token generator
+	 */
+	public OAuth2TokenExchangeAuthenticationProvider(OAuth2AuthorizationService authorizationService,
+			OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {
+		Assert.notNull(authorizationService, "authorizationService cannot be null");
+		Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
+		this.authorizationService = authorizationService;
+		this.tokenGenerator = tokenGenerator;
+	}
+
+	@Override
+	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+		OAuth2TokenExchangeAuthenticationToken tokenExchangeAuthentication =
+			(OAuth2TokenExchangeAuthenticationToken) authentication;
+
+		OAuth2ClientAuthenticationToken clientPrincipal =
+			getAuthenticatedClientElseThrowInvalidClient(tokenExchangeAuthentication);
+		RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Retrieved registered client");
+		}
+
+		if (!registeredClient.getAuthorizationGrantTypes().contains(TOKEN_EXCHANGE)) {
+			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
+		}
+
+		if (JWT_TOKEN_TYPE_VALUE.equals(tokenExchangeAuthentication.getRequestedTokenType()) &&
+				!OAuth2TokenFormat.SELF_CONTAINED.equals(registeredClient.getTokenSettings().getAccessTokenFormat())) {
+			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
+		}
+
+		OAuth2Authorization subjectAuthorization = this.authorizationService.findByToken(
+			tokenExchangeAuthentication.getSubjectToken(), OAuth2TokenType.ACCESS_TOKEN);
+		if (subjectAuthorization == null) {
+			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Retrieved authorization with subject token");
+		}
+
+		OAuth2Authorization.Token<OAuth2Token> subjectToken = subjectAuthorization.getToken(
+				tokenExchangeAuthentication.getSubjectToken());
+		if (!subjectToken.isActive()) {
+			// As per https://tools.ietf.org/html/rfc6749#section-5.2
+			// invalid_grant: The provided authorization grant (e.g., authorization code,
+			// resource owner credentials) or refresh token is invalid, expired, revoked [...].
+			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
+		}
+
+		if (JWT_TOKEN_TYPE_VALUE.equals(tokenExchangeAuthentication.getSubjectTokenType()) &&
+				!Jwt.class.isAssignableFrom(subjectToken.getToken().getClass())) {
+			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
+		}
+
+		if (subjectAuthorization.getAttribute(Principal.class.getName()) == null) {
+			// As per https://datatracker.ietf.org/doc/html/rfc8693#section-1.1,
+			// we require a principal to be available via the subject_token for
+			// impersonation or delegation use cases.
+			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
+		}
+
+		// As per https://datatracker.ietf.org/doc/html/rfc8693#section-4.4,
+		// The may_act claim makes a statement that one party is authorized to
+		// become the actor and act on behalf of another party.
+		String authorizedActorSubject = null;
+		if (subjectToken.getClaims() != null &&
+				subjectToken.getClaims().containsKey(MAY_ACT) &&
+				subjectToken.getClaims().get(MAY_ACT) instanceof Map<?, ?> mayAct) {
+			authorizedActorSubject = (String) mayAct.get(StandardClaimNames.SUB);
+		}
+
+		OAuth2Authorization actorAuthorization = null;
+		if (StringUtils.hasText(tokenExchangeAuthentication.getActorToken())) {
+			actorAuthorization = this.authorizationService.findByToken(
+					tokenExchangeAuthentication.getActorToken(), OAuth2TokenType.ACCESS_TOKEN);
+			if (actorAuthorization == null) {
+				throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
+			}
+
+			if (this.logger.isTraceEnabled()) {
+				this.logger.trace("Retrieved authorization with actor token");
+			}
+
+			OAuth2Authorization.Token<OAuth2Token> actorToken = actorAuthorization.getToken(
+					tokenExchangeAuthentication.getActorToken());
+			if (!actorToken.isActive()) {
+				// As per https://tools.ietf.org/html/rfc6749#section-5.2
+				// invalid_grant: The provided authorization grant (e.g., authorization code,
+				// resource owner credentials) or refresh token is invalid, expired, revoked [...].
+				throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
+			}
+
+			if (JWT_TOKEN_TYPE_VALUE.equals(tokenExchangeAuthentication.getActorTokenType()) &&
+					!Jwt.class.isAssignableFrom(actorToken.getToken().getClass())) {
+				throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
+			}
+
+			if (StringUtils.hasText(authorizedActorSubject) &&
+					!authorizedActorSubject.equals(actorAuthorization.getPrincipalName())) {
+				throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
+			}
+		} else if (StringUtils.hasText(authorizedActorSubject) &&
+				!authorizedActorSubject.equals(clientPrincipal.getName())) {
+			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
+		}
+
+		Set<String> authorizedScopes = Collections.emptySet();
+		if (!CollectionUtils.isEmpty(tokenExchangeAuthentication.getScopes())) {
+			authorizedScopes = validateRequestedScopes(registeredClient, tokenExchangeAuthentication.getScopes());
+		} else if (!CollectionUtils.isEmpty(subjectAuthorization.getAuthorizedScopes())) {
+			authorizedScopes = validateRequestedScopes(registeredClient, subjectAuthorization.getAuthorizedScopes());
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Validated token request parameters");
+		}
+
+		Authentication principal = getPrincipal(subjectAuthorization, actorAuthorization);
+
+		// @formatter:off
+		DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
+				.registeredClient(registeredClient)
+				.authorization(subjectAuthorization)
+				.principal(principal)
+				.authorizationServerContext(AuthorizationServerContextHolder.getContext())
+				.authorizedScopes(authorizedScopes)
+				.tokenType(OAuth2TokenType.ACCESS_TOKEN)
+				.authorizationGrantType(TOKEN_EXCHANGE)
+				.authorizationGrant(tokenExchangeAuthentication);
+		// @formatter:on
+
+		// ----- Access token -----
+		OAuth2TokenContext tokenContext = tokenContextBuilder.build();
+		OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
+		if (generatedAccessToken == null) {
+			OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
+					"The token generator failed to generate the access token.", ERROR_URI);
+			throw new OAuth2AuthenticationException(error);
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Generated access token");
+		}
+
+		OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
+				generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
+				generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
+
+		// @formatter:off
+		OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
+				.principalName(subjectAuthorization.getPrincipalName())
+				.authorizationGrantType(TOKEN_EXCHANGE)
+				.authorizedScopes(authorizedScopes)
+				.attribute(Principal.class.getName(), principal);
+		// @formatter:on
+
+		if (generatedAccessToken instanceof ClaimAccessor) {
+			authorizationBuilder.token(accessToken, (metadata) -> {
+				metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims());
+				metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, false);
+			});
+		} else {
+			authorizationBuilder.accessToken(accessToken);
+		}
+
+		OAuth2Authorization authorization = authorizationBuilder.build();
+		this.authorizationService.save(authorization);
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Saved authorization");
+		}
+
+		Map<String, Object> additionalParameters = new HashMap<>();
+		additionalParameters.put(ISSUED_TOKEN_TYPE, tokenExchangeAuthentication.getRequestedTokenType());
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Authenticated token request");
+		}
+
+		return new OAuth2AccessTokenAuthenticationToken(
+				registeredClient, clientPrincipal, accessToken, null, additionalParameters);
+	}
+
+	private static Set<String> validateRequestedScopes(RegisteredClient registeredClient, Set<String> requestedScopes) {
+		for (String requestedScope : requestedScopes) {
+			if (!registeredClient.getScopes().contains(requestedScope)) {
+				throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE);
+			}
+		}
+
+		return new LinkedHashSet<>(requestedScopes);
+	}
+
+	private static Authentication getPrincipal(OAuth2Authorization subjectAuthorization, OAuth2Authorization actorAuthorization) {
+		Authentication subjectPrincipal = subjectAuthorization.getAttribute(Principal.class.getName());
+
+		List<Authentication> actorPrincipals = new LinkedList<>();
+		if (actorAuthorization != null) {
+			actorPrincipals.add(new OAuth2ActorAuthenticationToken(actorAuthorization.getPrincipalName()));
+		}
+
+		if (subjectPrincipal instanceof OAuth2CompositeAuthenticationToken compositeAuthenticationToken) {
+			// As per https://datatracker.ietf.org/doc/html/rfc8693#section-4.1,
+			// the act claim can be used to represent a chain of delegation,
+			// so we unwrap the original subject and any previous actor(s).
+			subjectPrincipal = compositeAuthenticationToken.getSubject();
+			actorPrincipals.addAll(compositeAuthenticationToken.getActors());
+			// TODO: Should we allow delegation-to-impersonation where previous
+			//  actors exist but no actor_token exists on this request?
+		}
+
+		return CollectionUtils.isEmpty(actorPrincipals) ? subjectPrincipal :
+				new OAuth2CompositeAuthenticationToken(subjectPrincipal, actorPrincipals);
+	}
+
+	@Override
+	public boolean supports(Class<?> authentication) {
+		return OAuth2TokenExchangeAuthenticationToken.class.isAssignableFrom(authentication);
+	}
+
+}

+ 164 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2TokenExchangeAuthenticationToken.java

@@ -0,0 +1,164 @@
+/*
+ * Copyright 2020-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.server.authorization.authentication;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link Authentication} implementation used for the OAuth 2.0 Token Exchange Grant.
+ *
+ * @author Steve Riesenberg
+ * @since 1.3
+ * @see OAuth2AuthorizationGrantAuthenticationToken
+ * @see OAuth2TokenExchangeAuthenticationProvider
+ */
+public class OAuth2TokenExchangeAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
+
+	private static final AuthorizationGrantType TOKEN_EXCHANGE = new AuthorizationGrantType(
+			"urn:ietf:params:oauth:grant-type:token-exchange");
+
+	private final List<String> resources;
+
+	private final List<String> audiences;
+
+	private final String requestedTokenType;
+
+	private final String subjectToken;
+
+	private final String subjectTokenType;
+
+	private final String actorToken;
+
+	private final String actorTokenType;
+
+	private final Set<String> scopes;
+
+	/**
+	 * Constructs an {@code OAuth2TokenExchangeAuthenticationToken} using the provided parameters.
+	 *
+	 * @param resources a list of resource URIs
+	 * @param audiences a list audience values
+	 * @param scopes the requested scope(s)
+	 * @param requestedTokenType the requested token type
+	 * @param subjectToken the subject token
+	 * @param subjectTokenType the subject token type
+	 * @param actorToken the actor token
+	 * @param actorTokenType the actor token type
+	 * @param clientPrincipal the authenticated client principal
+	 * @param additionalParameters the additional parameters
+	 */
+	public OAuth2TokenExchangeAuthenticationToken(List<String> resources, List<String> audiences,
+			@Nullable Set<String> scopes, @Nullable String requestedTokenType, String subjectToken,
+			String subjectTokenType, @Nullable String actorToken, @Nullable String actorTokenType,
+			Authentication clientPrincipal, @Nullable Map<String, Object> additionalParameters) {
+		super(TOKEN_EXCHANGE, clientPrincipal, additionalParameters);
+		Assert.notNull(resources, "resources cannot be null");
+		Assert.notNull(audiences, "audiences cannot be null");
+		Assert.hasText(requestedTokenType, "requestedTokenType cannot be empty");
+		Assert.hasText(subjectToken, "subjectToken cannot be empty");
+		Assert.hasText(subjectTokenType, "subjectTokenType cannot be empty");
+		this.resources = resources;
+		this.audiences = audiences;
+		this.requestedTokenType = requestedTokenType;
+		this.subjectToken = subjectToken;
+		this.subjectTokenType = subjectTokenType;
+		this.actorToken = actorToken;
+		this.actorTokenType = actorTokenType;
+		this.scopes = Collections.unmodifiableSet(
+				scopes != null ? new HashSet<>(scopes) : Collections.emptySet());
+	}
+
+	/**
+	 * Returns the list of resource URIs.
+	 *
+	 * @return the list of resource URIs
+	 */
+	public List<String> getResources() {
+		return this.resources;
+	}
+
+	/**
+	 * Returns the list of audience values.
+	 *
+	 * @return the list of audience values
+	 */
+	public List<String> getAudiences() {
+		return this.audiences;
+	}
+
+	/**
+	 * Returns the requested scope(s).
+	 *
+	 * @return the requested scope(s), or an empty {@code Set} if not available
+	 */
+	public Set<String> getScopes() {
+		return this.scopes;
+	}
+
+	/**
+	 * Returns the requested token type.
+	 *
+	 * @return the requested token type
+	 */
+	public String getRequestedTokenType() {
+		return this.requestedTokenType;
+	}
+
+	/**
+	 * Returns the subject token.
+	 *
+	 * @return the subject token
+	 */
+	public String getSubjectToken() {
+		return this.subjectToken;
+	}
+
+	/**
+	 * Returns the subject token type.
+	 *
+	 * @return the subject token type
+	 */
+	public String getSubjectTokenType() {
+		return this.subjectTokenType;
+	}
+
+	/**
+	 * Returns the actor token.
+	 *
+	 * @return the actor token
+	 */
+	public String getActorToken() {
+		return this.actorToken;
+	}
+
+	/**
+	 * Returns the actor token type.
+	 *
+	 * @return the actor token type
+	 */
+	public String getActorTokenType() {
+		return this.actorTokenType;
+	}
+}

+ 46 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/DelegatingOAuth2TokenCustomizer.java

@@ -0,0 +1,46 @@
+/*
+ * Copyright 2020-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.server.authorization.config.annotation.web.configurers;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
+import org.springframework.util.Assert;
+
+/**
+ * @author Steve Riesenberg
+ * @since 1.3
+ */
+final class DelegatingOAuth2TokenCustomizer<T extends OAuth2TokenContext> implements OAuth2TokenCustomizer<T> {
+
+	private final List<OAuth2TokenCustomizer<T>> tokenCustomizers;
+
+	DelegatingOAuth2TokenCustomizer(List<OAuth2TokenCustomizer<T>> tokenCustomizers) {
+		Assert.notEmpty(tokenCustomizers, "tokenCustomizers cannot be empty");
+		this.tokenCustomizers = Collections.unmodifiableList(tokenCustomizers);
+	}
+
+	@Override
+	public void customize(T context) {
+		for (OAuth2TokenCustomizer<T> tokenCustomizer : this.tokenCustomizers) {
+			tokenCustomizer.customize(context);
+		}
+	}
+
+}

+ 30 - 2
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ConfigurerUtils.java

@@ -15,6 +15,8 @@
  */
  */
 package org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers;
 package org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers;
 
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Map;
 import java.util.Map;
 
 
 import com.nimbusds.jose.jwk.source.JWKSource;
 import com.nimbusds.jose.jwk.source.JWKSource;
@@ -170,13 +172,39 @@ final class OAuth2ConfigurerUtils {
 	}
 	}
 
 
 	private static OAuth2TokenCustomizer<JwtEncodingContext> getJwtCustomizer(HttpSecurity httpSecurity) {
 	private static OAuth2TokenCustomizer<JwtEncodingContext> getJwtCustomizer(HttpSecurity httpSecurity) {
+		OAuth2TokenCustomizer<JwtEncodingContext> defaultTokenCustomizer = OAuth2TokenExchangeTokenCustomizers.jwt();
 		ResolvableType type = ResolvableType.forClassWithGenerics(OAuth2TokenCustomizer.class, JwtEncodingContext.class);
 		ResolvableType type = ResolvableType.forClassWithGenerics(OAuth2TokenCustomizer.class, JwtEncodingContext.class);
-		return getOptionalBean(httpSecurity, type);
+		OAuth2TokenCustomizer<JwtEncodingContext> userTokenCustomizer = getOptionalBean(httpSecurity, type);
+
+		OAuth2TokenCustomizer<JwtEncodingContext> tokenCustomizer;
+		if (userTokenCustomizer != null) {
+			List<OAuth2TokenCustomizer<JwtEncodingContext>> tokenCustomizers = new ArrayList<>();
+			tokenCustomizers.add(defaultTokenCustomizer);
+			tokenCustomizers.add(userTokenCustomizer);
+			tokenCustomizer = new DelegatingOAuth2TokenCustomizer<>(tokenCustomizers);
+		} else {
+			tokenCustomizer = defaultTokenCustomizer;
+		}
+
+		return tokenCustomizer;
 	}
 	}
 
 
 	private static OAuth2TokenCustomizer<OAuth2TokenClaimsContext> getAccessTokenCustomizer(HttpSecurity httpSecurity) {
 	private static OAuth2TokenCustomizer<OAuth2TokenClaimsContext> getAccessTokenCustomizer(HttpSecurity httpSecurity) {
+		OAuth2TokenCustomizer<OAuth2TokenClaimsContext> defaultTokenCustomizer = OAuth2TokenExchangeTokenCustomizers.accessToken();
 		ResolvableType type = ResolvableType.forClassWithGenerics(OAuth2TokenCustomizer.class, OAuth2TokenClaimsContext.class);
 		ResolvableType type = ResolvableType.forClassWithGenerics(OAuth2TokenCustomizer.class, OAuth2TokenClaimsContext.class);
-		return getOptionalBean(httpSecurity, type);
+		OAuth2TokenCustomizer<OAuth2TokenClaimsContext> userTokenCustomizer = getOptionalBean(httpSecurity, type);
+
+		OAuth2TokenCustomizer<OAuth2TokenClaimsContext> tokenCustomizer;
+		if (userTokenCustomizer != null) {
+			List<OAuth2TokenCustomizer<OAuth2TokenClaimsContext>> tokenCustomizers = new ArrayList<>();
+			tokenCustomizers.add(defaultTokenCustomizer);
+			tokenCustomizers.add(userTokenCustomizer);
+			tokenCustomizer = new DelegatingOAuth2TokenCustomizer<>(tokenCustomizers);
+		} else {
+			tokenCustomizer = defaultTokenCustomizer;
+		}
+
+		return tokenCustomizer;
 	}
 	}
 
 
 	static AuthorizationServerSettings getAuthorizationServerSettings(HttpSecurity httpSecurity) {
 	static AuthorizationServerSettings getAuthorizationServerSettings(HttpSecurity httpSecurity) {

+ 7 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenEndpointConfigurer.java

@@ -38,6 +38,7 @@ import org.springframework.security.oauth2.server.authorization.authentication.O
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationProvider;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationProvider;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceCodeAuthenticationProvider;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceCodeAuthenticationProvider;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2RefreshTokenAuthenticationProvider;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2RefreshTokenAuthenticationProvider;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeAuthenticationProvider;
 import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
 import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
 import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
 import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
 import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
 import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
@@ -46,6 +47,7 @@ import org.springframework.security.oauth2.server.authorization.web.authenticati
 import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ClientCredentialsAuthenticationConverter;
 import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ClientCredentialsAuthenticationConverter;
 import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2DeviceCodeAuthenticationConverter;
 import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2DeviceCodeAuthenticationConverter;
 import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2RefreshTokenAuthenticationConverter;
 import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2RefreshTokenAuthenticationConverter;
+import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2TokenExchangeAuthenticationConverter;
 import org.springframework.security.web.access.intercept.AuthorizationFilter;
 import org.springframework.security.web.access.intercept.AuthorizationFilter;
 import org.springframework.security.web.authentication.AuthenticationConverter;
 import org.springframework.security.web.authentication.AuthenticationConverter;
 import org.springframework.security.web.authentication.AuthenticationFailureHandler;
 import org.springframework.security.web.authentication.AuthenticationFailureHandler;
@@ -213,6 +215,7 @@ public final class OAuth2TokenEndpointConfigurer extends AbstractOAuth2Configure
 		authenticationConverters.add(new OAuth2RefreshTokenAuthenticationConverter());
 		authenticationConverters.add(new OAuth2RefreshTokenAuthenticationConverter());
 		authenticationConverters.add(new OAuth2ClientCredentialsAuthenticationConverter());
 		authenticationConverters.add(new OAuth2ClientCredentialsAuthenticationConverter());
 		authenticationConverters.add(new OAuth2DeviceCodeAuthenticationConverter());
 		authenticationConverters.add(new OAuth2DeviceCodeAuthenticationConverter());
+		authenticationConverters.add(new OAuth2TokenExchangeAuthenticationConverter());
 
 
 		return authenticationConverters;
 		return authenticationConverters;
 	}
 	}
@@ -243,6 +246,10 @@ public final class OAuth2TokenEndpointConfigurer extends AbstractOAuth2Configure
 				new OAuth2DeviceCodeAuthenticationProvider(authorizationService, tokenGenerator);
 				new OAuth2DeviceCodeAuthenticationProvider(authorizationService, tokenGenerator);
 		authenticationProviders.add(deviceCodeAuthenticationProvider);
 		authenticationProviders.add(deviceCodeAuthenticationProvider);
 
 
+		OAuth2TokenExchangeAuthenticationProvider tokenExchangeAuthenticationProvider =
+				new OAuth2TokenExchangeAuthenticationProvider(authorizationService, tokenGenerator);
+		authenticationProviders.add(tokenExchangeAuthenticationProvider);
+
 		return authenticationProviders;
 		return authenticationProviders;
 	}
 	}
 
 

+ 85 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2TokenExchangeTokenCustomizers.java

@@ -0,0 +1,85 @@
+/*
+ * Copyright 2020-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.server.authorization.config.annotation.web.configurers;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2CompositeAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimNames;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsContext;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * @author Steve Riesenberg
+ * @since 1.3
+ */
+final class OAuth2TokenExchangeTokenCustomizers {
+
+	private static final AuthorizationGrantType TOKEN_EXCHANGE = new AuthorizationGrantType(
+			"urn:ietf:params:oauth:grant-type:token-exchange");
+
+	private OAuth2TokenExchangeTokenCustomizers() {
+	}
+
+	static OAuth2TokenCustomizer<JwtEncodingContext> jwt() {
+		return (context) -> context.getClaims().claims((claims) -> customize(context, claims));
+	}
+
+	static OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessToken() {
+		return (context) -> context.getClaims().claims((claims) -> customize(context, claims));
+	}
+
+	private static void customize(OAuth2TokenContext context, Map<String, Object> claims) {
+		if (!TOKEN_EXCHANGE.equals(context.getAuthorizationGrantType())) {
+			return;
+		}
+
+		if (context.getAuthorizationGrant() instanceof OAuth2TokenExchangeAuthenticationToken tokenExchangeAuthentication) {
+			// Customize the token claims when audience is present in the request
+			List<String> audience = tokenExchangeAuthentication.getAudiences();
+			if (!CollectionUtils.isEmpty(audience)) {
+				claims.put(OAuth2TokenClaimNames.AUD, audience);
+			}
+		}
+
+		// As per https://datatracker.ietf.org/doc/html/rfc8693#section-4.1,
+		// we handle a composite principal with an actor by adding an "act"
+		// claim with a "sub" claim of the actor.
+		//
+		// If more than one actor is present, we create a chain of delegation by
+		// nesting "act" claims.
+		if (context.getPrincipal() instanceof OAuth2CompositeAuthenticationToken compositeAuthenticationToken) {
+			Map<String, Object> currentClaims = claims;
+			for (Authentication actorPrincipal : compositeAuthenticationToken.getActors()) {
+				Map<String, Object> actClaim = new HashMap<>();
+				actClaim.put("sub", actorPrincipal.getName());
+				currentClaims.put("act", Collections.unmodifiableMap(actClaim));
+				currentClaims = actClaim;
+			}
+		}
+	}
+
+}

+ 44 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2ActorAuthenticationTokenMixin.java

@@ -0,0 +1,44 @@
+/*
+ * Copyright 2020-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.server.authorization.jackson2;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ActorAuthenticationToken;
+
+/**
+ * This mixin class is used to serialize/deserialize {@link OAuth2ActorAuthenticationToken}.
+ *
+ * @author Steve Riesenberg
+ * @since 1.3
+ * @see OAuth2ActorAuthenticationToken
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
+@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
+		isGetterVisibility = JsonAutoDetect.Visibility.NONE, creatorVisibility = JsonAutoDetect.Visibility.NONE)
+@JsonIgnoreProperties(ignoreUnknown = true)
+abstract class OAuth2ActorAuthenticationTokenMixin {
+
+	@JsonCreator
+	OAuth2ActorAuthenticationTokenMixin(@JsonProperty("name") String name) {
+	}
+
+}

+ 6 - 1
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2AuthorizationServerJackson2Module.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2020-2022 the original author or authors.
+ * Copyright 2020-2024 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -27,6 +27,8 @@ import org.springframework.security.jackson2.SecurityJackson2Modules;
 import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
 import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
 import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
 import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
 import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
 import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ActorAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2CompositeAuthenticationToken;
 import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
 import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
 
 
 /**
 /**
@@ -37,6 +39,7 @@ import org.springframework.security.oauth2.server.authorization.settings.OAuth2T
  * <li>{@link UnmodifiableMapMixin}</li>
  * <li>{@link UnmodifiableMapMixin}</li>
  * <li>{@link HashSetMixin}</li>
  * <li>{@link HashSetMixin}</li>
  * <li>{@link OAuth2AuthorizationRequestMixin}</li>
  * <li>{@link OAuth2AuthorizationRequestMixin}</li>
+ * <li>{@link OAuth2CompositeAuthenticationTokenMixin}</li>
  * <li>{@link DurationMixin}</li>
  * <li>{@link DurationMixin}</li>
  * <li>{@link JwsAlgorithmMixin}</li>
  * <li>{@link JwsAlgorithmMixin}</li>
  * <li>{@link OAuth2TokenFormatMixin}</li>
  * <li>{@link OAuth2TokenFormatMixin}</li>
@@ -77,7 +80,9 @@ public class OAuth2AuthorizationServerJackson2Module extends SimpleModule {
 				UnmodifiableMapMixin.class);
 				UnmodifiableMapMixin.class);
 		context.setMixInAnnotations(HashSet.class, HashSetMixin.class);
 		context.setMixInAnnotations(HashSet.class, HashSetMixin.class);
 		context.setMixInAnnotations(LinkedHashSet.class, HashSetMixin.class);
 		context.setMixInAnnotations(LinkedHashSet.class, HashSetMixin.class);
+		context.setMixInAnnotations(OAuth2ActorAuthenticationToken.class, OAuth2ActorAuthenticationTokenMixin.class);
 		context.setMixInAnnotations(OAuth2AuthorizationRequest.class, OAuth2AuthorizationRequestMixin.class);
 		context.setMixInAnnotations(OAuth2AuthorizationRequest.class, OAuth2AuthorizationRequestMixin.class);
+		context.setMixInAnnotations(OAuth2CompositeAuthenticationToken.class, OAuth2CompositeAuthenticationTokenMixin.class);
 		context.setMixInAnnotations(Duration.class, DurationMixin.class);
 		context.setMixInAnnotations(Duration.class, DurationMixin.class);
 		context.setMixInAnnotations(SignatureAlgorithm.class, JwsAlgorithmMixin.class);
 		context.setMixInAnnotations(SignatureAlgorithm.class, JwsAlgorithmMixin.class);
 		context.setMixInAnnotations(MacAlgorithm.class, JwsAlgorithmMixin.class);
 		context.setMixInAnnotations(MacAlgorithm.class, JwsAlgorithmMixin.class);

+ 48 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2CompositeAuthenticationTokenMixin.java

@@ -0,0 +1,48 @@
+/*
+ * Copyright 2020-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.server.authorization.jackson2;
+
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2CompositeAuthenticationToken;
+
+/**
+ * This mixin class is used to serialize/deserialize {@link OAuth2CompositeAuthenticationToken}.
+ *
+ * @author Steve Riesenberg
+ * @since 1.3
+ * @see OAuth2CompositeAuthenticationToken
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
+@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
+		isGetterVisibility = JsonAutoDetect.Visibility.NONE, creatorVisibility = JsonAutoDetect.Visibility.NONE)
+@JsonIgnoreProperties(ignoreUnknown = true)
+abstract class OAuth2CompositeAuthenticationTokenMixin {
+
+	@JsonCreator
+	OAuth2CompositeAuthenticationTokenMixin(@JsonProperty("subject") Authentication subject,
+			@JsonProperty("actors") List<Authentication> actors) {
+	}
+
+}

+ 3 - 1
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilter.java

@@ -48,6 +48,7 @@ import org.springframework.security.oauth2.server.authorization.web.authenticati
 import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2DeviceCodeAuthenticationConverter;
 import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2DeviceCodeAuthenticationConverter;
 import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ErrorAuthenticationFailureHandler;
 import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ErrorAuthenticationFailureHandler;
 import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2RefreshTokenAuthenticationConverter;
 import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2RefreshTokenAuthenticationConverter;
+import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2TokenExchangeAuthenticationConverter;
 import org.springframework.security.web.authentication.AuthenticationConverter;
 import org.springframework.security.web.authentication.AuthenticationConverter;
 import org.springframework.security.web.authentication.AuthenticationFailureHandler;
 import org.springframework.security.web.authentication.AuthenticationFailureHandler;
 import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
 import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
@@ -128,7 +129,8 @@ public final class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
 						new OAuth2AuthorizationCodeAuthenticationConverter(),
 						new OAuth2AuthorizationCodeAuthenticationConverter(),
 						new OAuth2RefreshTokenAuthenticationConverter(),
 						new OAuth2RefreshTokenAuthenticationConverter(),
 						new OAuth2ClientCredentialsAuthenticationConverter(),
 						new OAuth2ClientCredentialsAuthenticationConverter(),
-						new OAuth2DeviceCodeAuthenticationConverter()));
+						new OAuth2DeviceCodeAuthenticationConverter(),
+						new OAuth2TokenExchangeAuthenticationConverter()));
 	}
 	}
 
 
 	@Override
 	@Override

+ 241 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2TokenExchangeAuthenticationConverter.java

@@ -0,0 +1,241 @@
+/*
+ * Copyright 2020-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.server.authorization.web.authentication;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+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.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
+import org.springframework.security.web.authentication.AuthenticationConverter;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.MultiValueMap;
+import org.springframework.util.StringUtils;
+
+/**
+ * Attempts to extract an Access Token Request from {@link HttpServletRequest} for the OAuth 2.0 Token Exchange Grant
+ * and then converts it to an {@link OAuth2TokenExchangeAuthenticationToken} used for authenticating the authorization grant.
+ *
+ * @author Steve Riesenberg
+ * @since 1.3
+ * @see AuthenticationConverter
+ * @see OAuth2TokenExchangeAuthenticationToken
+ * @see OAuth2TokenEndpointFilter
+ */
+public final class OAuth2TokenExchangeAuthenticationConverter implements AuthenticationConverter {
+
+	private static final String TOKEN_TYPE_IDENTIFIERS_URI = "https://datatracker.ietf.org/doc/html/rfc8693#section-3";
+
+	private static final AuthorizationGrantType TOKEN_EXCHANGE = new AuthorizationGrantType(
+			"urn:ietf:params:oauth:grant-type:token-exchange");
+
+	private static final String AUDIENCE = "audience";
+
+	private static final String RESOURCE = "resource";
+
+	private static final String REQUESTED_TOKEN_TYPE = "requested_token_type";
+
+	private static final String SUBJECT_TOKEN = "subject_token";
+
+	private static final String SUBJECT_TOKEN_TYPE = "subject_token_type";
+
+	private static final String ACTOR_TOKEN = "actor_token";
+
+	private static final String ACTOR_TOKEN_TYPE = "actor_token_type";
+
+	private static final String ACCESS_TOKEN_TYPE_VALUE = "urn:ietf:params:oauth:token-type:access_token";
+
+	private static final String JWT_TOKEN_TYPE_VALUE = "urn:ietf:params:oauth:token-type:jwt";
+
+	private static final Set<String> SUPPORTED_TOKEN_TYPES = Set.of(ACCESS_TOKEN_TYPE_VALUE, JWT_TOKEN_TYPE_VALUE);
+
+	@Nullable
+	@Override
+	public Authentication convert(HttpServletRequest request) {
+		MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getFormParameters(request);
+
+		// grant_type (REQUIRED)
+		String grantType = parameters.getFirst(OAuth2ParameterNames.GRANT_TYPE);
+		if (!TOKEN_EXCHANGE.getValue().equals(grantType)) {
+			return null;
+		}
+
+		Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
+
+		// resource (OPTIONAL)
+		List<String> resources = parameters.getOrDefault(RESOURCE, Collections.emptyList());
+		if (!CollectionUtils.isEmpty(resources)) {
+			for (String resource : resources) {
+				if (!isValidUri(resource)) {
+					OAuth2EndpointUtils.throwError(
+							OAuth2ErrorCodes.INVALID_REQUEST,
+							RESOURCE,
+							OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
+				}
+			}
+		}
+
+		// audience (OPTIONAL)
+		List<String> audiences = parameters.getOrDefault(AUDIENCE, Collections.emptyList());
+
+		// scope (OPTIONAL)
+		String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
+		if (StringUtils.hasText(scope) &&
+				parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
+			OAuth2EndpointUtils.throwError(
+					OAuth2ErrorCodes.INVALID_REQUEST,
+					OAuth2ParameterNames.SCOPE,
+					OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
+		}
+
+		Set<String> requestedScopes = null;
+		if (StringUtils.hasText(scope)) {
+			requestedScopes = new HashSet<>(
+					Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
+		}
+
+		// requested_token_type (OPTIONAL)
+		String requestedTokenType = parameters.getFirst(REQUESTED_TOKEN_TYPE);
+		if (StringUtils.hasText(requestedTokenType)) {
+			if (parameters.get(REQUESTED_TOKEN_TYPE).size() != 1) {
+				OAuth2EndpointUtils.throwError(
+						OAuth2ErrorCodes.INVALID_REQUEST,
+						REQUESTED_TOKEN_TYPE,
+						OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
+			}
+
+			validateTokenType(REQUESTED_TOKEN_TYPE, requestedTokenType);
+		} else {
+			requestedTokenType = ACCESS_TOKEN_TYPE_VALUE;
+		}
+
+		// subject_token (REQUIRED)
+		String subjectToken = parameters.getFirst(SUBJECT_TOKEN);
+		if (!StringUtils.hasText(subjectToken) ||
+				parameters.get(SUBJECT_TOKEN).size() != 1) {
+			OAuth2EndpointUtils.throwError(
+					OAuth2ErrorCodes.INVALID_REQUEST,
+					SUBJECT_TOKEN,
+					OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
+		}
+
+		// subject_token_type (REQUIRED)
+		String subjectTokenType = parameters.getFirst(SUBJECT_TOKEN_TYPE);
+		if (!StringUtils.hasText(subjectTokenType) ||
+				parameters.get(SUBJECT_TOKEN_TYPE).size() != 1) {
+			OAuth2EndpointUtils.throwError(
+					OAuth2ErrorCodes.INVALID_REQUEST,
+					SUBJECT_TOKEN_TYPE,
+					OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
+		} else {
+			validateTokenType(SUBJECT_TOKEN_TYPE, subjectTokenType);
+		}
+
+		// actor_token (OPTIONAL, REQUIRED if actor_token_type is provided)
+		String actorToken = parameters.getFirst(ACTOR_TOKEN);
+		if (StringUtils.hasText(actorToken) &&
+				parameters.get(ACTOR_TOKEN).size() != 1) {
+			OAuth2EndpointUtils.throwError(
+					OAuth2ErrorCodes.INVALID_REQUEST,
+					ACTOR_TOKEN,
+					OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
+		}
+
+		// actor_token_type (OPTIONAL, REQUIRED if actor_token is provided)
+		String actorTokenType = parameters.getFirst(ACTOR_TOKEN_TYPE);
+		if (StringUtils.hasText(actorTokenType)) {
+			if (parameters.get(ACTOR_TOKEN_TYPE).size() != 1) {
+				OAuth2EndpointUtils.throwError(
+						OAuth2ErrorCodes.INVALID_REQUEST,
+						ACTOR_TOKEN_TYPE,
+						OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
+			}
+
+			validateTokenType(ACTOR_TOKEN_TYPE, actorTokenType);
+		}
+
+		if (!StringUtils.hasText(actorToken) && StringUtils.hasText(actorTokenType)) {
+			OAuth2EndpointUtils.throwError(
+					OAuth2ErrorCodes.INVALID_REQUEST,
+					ACTOR_TOKEN,
+					OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
+		} else if (StringUtils.hasText(actorToken) && !StringUtils.hasText(actorTokenType)) {
+			OAuth2EndpointUtils.throwError(
+					OAuth2ErrorCodes.INVALID_REQUEST,
+					ACTOR_TOKEN_TYPE,
+					OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
+		}
+
+		Map<String, Object> additionalParameters = new HashMap<>();
+		parameters.forEach((key, value) -> {
+			if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) &&
+					!key.equals(RESOURCE) &&
+					!key.equals(AUDIENCE) &&
+					!key.equals(REQUESTED_TOKEN_TYPE) &&
+					!key.equals(SUBJECT_TOKEN) &&
+					!key.equals(SUBJECT_TOKEN_TYPE) &&
+					!key.equals(ACTOR_TOKEN) &&
+					!key.equals(ACTOR_TOKEN_TYPE) &&
+					!key.equals(OAuth2ParameterNames.SCOPE)) {
+				additionalParameters.put(key, (value.size() == 1) ? value.get(0) : value.toArray(new String[0]));
+			}
+		});
+
+		return new OAuth2TokenExchangeAuthenticationToken(resources, audiences, requestedScopes, requestedTokenType,
+			subjectToken, subjectTokenType, actorToken, actorTokenType, clientPrincipal, additionalParameters);
+	}
+
+	private static void validateTokenType(String parameterName, String tokenTypeValue) {
+		if (!SUPPORTED_TOKEN_TYPES.contains(tokenTypeValue)) {
+			OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.UNSUPPORTED_TOKEN_TYPE,
+					String.format("OAuth 2.0 Token Exchange parameter: %s", parameterName), TOKEN_TYPE_IDENTIFIERS_URI);
+			// @formatter:off
+			String message = String.format(
+					"OAuth 2.0 Token Exchange parameter: %s - " +
+					"The provided value is not supported by this authorization server. " +
+					"Supported values are %s and %s.",
+					parameterName, ACCESS_TOKEN_TYPE_VALUE, JWT_TOKEN_TYPE_VALUE);
+			// @formatter:on
+			throw new OAuth2AuthenticationException(error, message);
+		}
+	}
+
+	private static boolean isValidUri(String uri) {
+		try {
+			URI validUri = new URI(uri);
+			return validUri.isAbsolute() && validUri.getFragment() == null;
+		} catch (URISyntaxException ex) {
+			return false;
+		}
+	}
+}

+ 9 - 6
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientCredentialsGrantTests.java

@@ -24,13 +24,12 @@ import java.util.Base64;
 import java.util.List;
 import java.util.List;
 import java.util.function.Consumer;
 import java.util.function.Consumer;
 
 
-import jakarta.servlet.ServletException;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-
 import com.nimbusds.jose.jwk.JWKSet;
 import com.nimbusds.jose.jwk.JWKSet;
 import com.nimbusds.jose.jwk.source.JWKSource;
 import com.nimbusds.jose.jwk.source.JWKSource;
 import com.nimbusds.jose.proc.SecurityContext;
 import com.nimbusds.jose.proc.SecurityContext;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.BeforeAll;
@@ -74,6 +73,7 @@ import org.springframework.security.oauth2.server.authorization.authentication.O
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationToken;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationToken;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceCodeAuthenticationProvider;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceCodeAuthenticationProvider;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2RefreshTokenAuthenticationProvider;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2RefreshTokenAuthenticationProvider;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeAuthenticationProvider;
 import org.springframework.security.oauth2.server.authorization.authentication.PublicClientAuthenticationProvider;
 import org.springframework.security.oauth2.server.authorization.authentication.PublicClientAuthenticationProvider;
 import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
 import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
 import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository.RegisteredClientParametersMapper;
 import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository.RegisteredClientParametersMapper;
@@ -93,6 +93,7 @@ import org.springframework.security.oauth2.server.authorization.web.authenticati
 import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ClientCredentialsAuthenticationConverter;
 import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ClientCredentialsAuthenticationConverter;
 import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2DeviceCodeAuthenticationConverter;
 import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2DeviceCodeAuthenticationConverter;
 import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2RefreshTokenAuthenticationConverter;
 import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2RefreshTokenAuthenticationConverter;
+import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2TokenExchangeAuthenticationConverter;
 import org.springframework.security.oauth2.server.authorization.web.authentication.PublicClientAuthenticationConverter;
 import org.springframework.security.oauth2.server.authorization.web.authentication.PublicClientAuthenticationConverter;
 import org.springframework.security.web.SecurityFilterChain;
 import org.springframework.security.web.SecurityFilterChain;
 import org.springframework.security.web.authentication.AuthenticationConverter;
 import org.springframework.security.web.authentication.AuthenticationConverter;
@@ -294,7 +295,8 @@ public class OAuth2ClientCredentialsGrantTests {
 						converter instanceof OAuth2AuthorizationCodeAuthenticationConverter ||
 						converter instanceof OAuth2AuthorizationCodeAuthenticationConverter ||
 						converter instanceof OAuth2RefreshTokenAuthenticationConverter ||
 						converter instanceof OAuth2RefreshTokenAuthenticationConverter ||
 						converter instanceof OAuth2ClientCredentialsAuthenticationConverter ||
 						converter instanceof OAuth2ClientCredentialsAuthenticationConverter ||
-						converter instanceof OAuth2DeviceCodeAuthenticationConverter);
+						converter instanceof OAuth2DeviceCodeAuthenticationConverter ||
+						converter instanceof OAuth2TokenExchangeAuthenticationConverter);
 
 
 		verify(authenticationProvider).authenticate(eq(clientCredentialsAuthentication));
 		verify(authenticationProvider).authenticate(eq(clientCredentialsAuthentication));
 
 
@@ -307,7 +309,8 @@ public class OAuth2ClientCredentialsGrantTests {
 						provider instanceof OAuth2AuthorizationCodeAuthenticationProvider ||
 						provider instanceof OAuth2AuthorizationCodeAuthenticationProvider ||
 						provider instanceof OAuth2RefreshTokenAuthenticationProvider ||
 						provider instanceof OAuth2RefreshTokenAuthenticationProvider ||
 						provider instanceof OAuth2ClientCredentialsAuthenticationProvider ||
 						provider instanceof OAuth2ClientCredentialsAuthenticationProvider ||
-						provider instanceof OAuth2DeviceCodeAuthenticationProvider);
+						provider instanceof OAuth2DeviceCodeAuthenticationProvider ||
+						provider instanceof OAuth2TokenExchangeAuthenticationProvider);
 
 
 		verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), eq(accessTokenAuthentication));
 		verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), eq(accessTokenAuthentication));
 	}
 	}