Kaynağa Gözat

Add support for OAuth 2.0 Pushed Authorization Requests (PAR)

Closes gh-210

Signed-off-by: Joe Grandja <10884212+jgrandja@users.noreply.github.com>
Joe Grandja 5 ay önce
ebeveyn
işleme
4337884e87
24 değiştirilmiş dosya ile 2481 ekleme ve 214 silme
  1. 1 0
      etc/checkstyle/checkstyle-suppressions.xml
  2. 135 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/AbstractOAuth2AuthorizationCodeRequestAuthenticationToken.java
  3. 3 1
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/JwtClientAssertionDecoderFactory.java
  4. 54 56
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java
  5. 5 107
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationToken.java
  6. 94 14
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationValidator.java
  7. 184 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2PushedAuthorizationRequestAuthenticationProvider.java
  8. 94 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2PushedAuthorizationRequestAuthenticationToken.java
  9. 80 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2PushedAuthorizationRequestUri.java
  10. 38 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OidcPrompt.java
  11. 55 16
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerConfigurer.java
  12. 7 2
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientAuthenticationConfigurer.java
  13. 265 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2PushedAuthorizationRequestEndpointConfigurer.java
  14. 24 1
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/AuthorizationServerSettings.java
  15. 8 1
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/ConfigurationSettingNames.java
  16. 63 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/HttpMessageConverters.java
  17. 223 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2PushedAuthorizationRequestEndpointFilter.java
  18. 51 9
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2AuthorizationCodeRequestAuthenticationConverter.java
  19. 67 5
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProviderTests.java
  20. 422 0
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2PushedAuthorizationRequestAuthenticationProviderTests.java
  21. 87 0
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationCodeGrantTests.java
  22. 15 2
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/settings/AuthorizationServerSettingsTests.java
  23. 16 0
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilterTests.java
  24. 490 0
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2PushedAuthorizationRequestEndpointFilterTests.java

+ 1 - 0
etc/checkstyle/checkstyle-suppressions.xml

@@ -7,4 +7,5 @@
 	<suppress files="SpringAuthorizationServerVersion\.java" checks="HideUtilityClassConstructor"/>
 	<suppress files="[\\/]src[\\/]test[\\/]" checks="RegexpSinglelineJava" id="toLowerCaseWithoutLocale"/>
 	<suppress files="[\\/]src[\\/]test[\\/]" checks="RegexpSinglelineJava" id="toUpperCaseWithoutLocale"/>
+	<suppress files="AbstractOAuth2AuthorizationCodeRequestAuthenticationToken\.java" checks="SpringMethodVisibility"/>
 </suppressions>

+ 135 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/AbstractOAuth2AuthorizationCodeRequestAuthenticationToken.java

@@ -0,0 +1,135 @@
+/*
+ * Copyright 2020-2025 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.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.server.authorization.util.SpringAuthorizationServerVersion;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link Authentication} base implementation for the OAuth 2.0 Authorization Request
+ * used in the Authorization Code Grant.
+ *
+ * @author Joe Grandja
+ * @since 1.5
+ * @see OAuth2AuthorizationCodeRequestAuthenticationToken
+ * @see OAuth2PushedAuthorizationRequestAuthenticationToken
+ */
+abstract class AbstractOAuth2AuthorizationCodeRequestAuthenticationToken extends AbstractAuthenticationToken {
+
+	private static final long serialVersionUID = SpringAuthorizationServerVersion.SERIAL_VERSION_UID;
+
+	private final String authorizationUri;
+
+	private final String clientId;
+
+	private final Authentication principal;
+
+	private final String redirectUri;
+
+	private final String state;
+
+	private final Set<String> scopes;
+
+	private final Map<String, Object> additionalParameters;
+
+	protected AbstractOAuth2AuthorizationCodeRequestAuthenticationToken(String authorizationUri, String clientId,
+			Authentication principal, @Nullable String redirectUri, @Nullable String state,
+			@Nullable Set<String> scopes, @Nullable Map<String, Object> additionalParameters) {
+		super(Collections.emptyList());
+		Assert.hasText(authorizationUri, "authorizationUri cannot be empty");
+		Assert.hasText(clientId, "clientId cannot be empty");
+		Assert.notNull(principal, "principal cannot be null");
+		this.authorizationUri = authorizationUri;
+		this.clientId = clientId;
+		this.principal = principal;
+		this.redirectUri = redirectUri;
+		this.state = state;
+		this.scopes = Collections.unmodifiableSet((scopes != null) ? new HashSet<>(scopes) : Collections.emptySet());
+		this.additionalParameters = Collections.unmodifiableMap(
+				(additionalParameters != null) ? new HashMap<>(additionalParameters) : Collections.emptyMap());
+	}
+
+	@Override
+	public Object getPrincipal() {
+		return this.principal;
+	}
+
+	@Override
+	public Object getCredentials() {
+		return "";
+	}
+
+	/**
+	 * Returns the authorization URI.
+	 * @return the authorization URI
+	 */
+	public String getAuthorizationUri() {
+		return this.authorizationUri;
+	}
+
+	/**
+	 * Returns the client identifier.
+	 * @return the client identifier
+	 */
+	public String getClientId() {
+		return this.clientId;
+	}
+
+	/**
+	 * Returns the redirect uri.
+	 * @return the redirect uri
+	 */
+	@Nullable
+	public String getRedirectUri() {
+		return this.redirectUri;
+	}
+
+	/**
+	 * Returns the state.
+	 * @return the state
+	 */
+	@Nullable
+	public String getState() {
+		return this.state;
+	}
+
+	/**
+	 * Returns the requested (or authorized) scope(s).
+	 * @return the requested (or authorized) scope(s), or an empty {@code Set} if not
+	 * available
+	 */
+	public Set<String> getScopes() {
+		return this.scopes;
+	}
+
+	/**
+	 * Returns the additional parameters.
+	 * @return the additional parameters, or an empty {@code Map} if not available
+	 */
+	public Map<String, Object> getAdditionalParameters() {
+		return this.additionalParameters;
+	}
+
+}

+ 3 - 1
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/JwtClientAssertionDecoderFactory.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-2023 the original author or authors.
+ * Copyright 2020-2025 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.
@@ -206,6 +206,8 @@ public final class JwtClientAssertionDecoderFactory implements JwtDecoderFactory
 				authorizationServerSettings.getTokenIntrospectionEndpoint()));
 		audience.add(asUrl(authorizationServerContext.getIssuer(),
 				authorizationServerSettings.getTokenRevocationEndpoint()));
+		audience.add(asUrl(authorizationServerContext.getIssuer(),
+				authorizationServerSettings.getPushedAuthorizationRequestEndpoint()));
 		return audience;
 	}
 

+ 54 - 56
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-2024 the original author or authors.
+ * Copyright 2020-2025 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.
@@ -27,7 +27,6 @@ import java.util.function.Predicate;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 
-import org.springframework.core.log.LogMessage;
 import org.springframework.security.authentication.AnonymousAuthenticationToken;
 import org.springframework.security.authentication.AuthenticationProvider;
 import org.springframework.security.core.Authentication;
@@ -39,7 +38,6 @@ import org.springframework.security.oauth2.core.OAuth2Error;
 import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
 import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
 import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
-import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
 import org.springframework.security.oauth2.core.oidc.OidcScopes;
 import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
@@ -81,7 +79,7 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
 
 	private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1";
 
-	private static final String PKCE_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7636#section-4.4.1";
+	private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
 
 	private static final StringKeyGenerator DEFAULT_STATE_GENERATOR = new Base64StringKeyGenerator(
 			Base64.getUrlEncoder());
@@ -122,6 +120,13 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
 	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
 		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = (OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
 
+		String requestUri = (String) authorizationCodeRequestAuthentication.getAdditionalParameters()
+			.get("request_uri");
+		if (StringUtils.hasText(requestUri)) {
+			authorizationCodeRequestAuthentication = fromPushedAuthorizationRequest(
+					authorizationCodeRequestAuthentication);
+		}
+
 		RegisteredClient registeredClient = this.registeredClientRepository
 			.findByClientId(authorizationCodeRequestAuthentication.getClientId());
 		if (registeredClient == null) {
@@ -136,47 +141,28 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
 		OAuth2AuthorizationCodeRequestAuthenticationContext.Builder authenticationContextBuilder = OAuth2AuthorizationCodeRequestAuthenticationContext
 			.with(authorizationCodeRequestAuthentication)
 			.registeredClient(registeredClient);
-		this.authenticationValidator.accept(authenticationContextBuilder.build());
+		OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext = authenticationContextBuilder
+			.build();
 
-		if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.AUTHORIZATION_CODE)) {
-			if (this.logger.isDebugEnabled()) {
-				this.logger.debug(LogMessage.format(
-						"Invalid request: requested grant_type is not allowed" + " for registered client '%s'",
-						registeredClient.getId()));
-			}
-			throwError(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT, OAuth2ParameterNames.CLIENT_ID,
-					authorizationCodeRequestAuthentication, registeredClient);
-		}
+		// grant_type
+		OAuth2AuthorizationCodeRequestAuthenticationValidator.DEFAULT_AUTHORIZATION_GRANT_TYPE_VALIDATOR
+			.accept(authenticationContext);
+
+		// redirect_uri and scope
+		this.authenticationValidator.accept(authenticationContext);
 
 		// code_challenge (REQUIRED for public clients) - RFC 7636 (PKCE)
-		String codeChallenge = (String) authorizationCodeRequestAuthentication.getAdditionalParameters()
-			.get(PkceParameterNames.CODE_CHALLENGE);
-		if (StringUtils.hasText(codeChallenge)) {
-			String codeChallengeMethod = (String) authorizationCodeRequestAuthentication.getAdditionalParameters()
-				.get(PkceParameterNames.CODE_CHALLENGE_METHOD);
-			if (!StringUtils.hasText(codeChallengeMethod) || !"S256".equals(codeChallengeMethod)) {
-				throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD, PKCE_ERROR_URI,
-						authorizationCodeRequestAuthentication, registeredClient, null);
-			}
-		}
-		else if (registeredClient.getClientSettings().isRequireProofKey()) {
-			throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE, PKCE_ERROR_URI,
-					authorizationCodeRequestAuthentication, registeredClient, null);
-		}
+		OAuth2AuthorizationCodeRequestAuthenticationValidator.DEFAULT_CODE_CHALLENGE_VALIDATOR
+			.accept(authenticationContext);
 
 		// prompt (OPTIONAL for OpenID Connect 1.0 Authentication Request)
 		Set<String> promptValues = Collections.emptySet();
 		if (authorizationCodeRequestAuthentication.getScopes().contains(OidcScopes.OPENID)) {
 			String prompt = (String) authorizationCodeRequestAuthentication.getAdditionalParameters().get("prompt");
 			if (StringUtils.hasText(prompt)) {
+				OAuth2AuthorizationCodeRequestAuthenticationValidator.DEFAULT_PROMPT_VALIDATOR
+					.accept(authenticationContext);
 				promptValues = new HashSet<>(Arrays.asList(StringUtils.delimitedListToStringArray(prompt, " ")));
-				if (promptValues.contains(OidcPrompts.NONE)) {
-					if (promptValues.contains(OidcPrompts.LOGIN) || promptValues.contains(OidcPrompts.CONSENT)
-							|| promptValues.contains(OidcPrompts.SELECT_ACCOUNT)) {
-						throwError(OAuth2ErrorCodes.INVALID_REQUEST, "prompt", authorizationCodeRequestAuthentication,
-								registeredClient);
-					}
-				}
 			}
 		}
 
@@ -190,7 +176,7 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
 
 		Authentication principal = (Authentication) authorizationCodeRequestAuthentication.getPrincipal();
 		if (!isPrincipalAuthenticated(principal)) {
-			if (promptValues.contains(OidcPrompts.NONE)) {
+			if (promptValues.contains(OidcPrompt.NONE)) {
 				// Return an error instead of displaying the login page (via the
 				// configured AuthenticationEntryPoint)
 				throwError("login_required", "prompt", authorizationCodeRequestAuthentication, registeredClient);
@@ -219,7 +205,7 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
 		}
 
 		if (this.authorizationConsentRequired.test(authenticationContextBuilder.build())) {
-			if (promptValues.contains(OidcPrompts.NONE)) {
+			if (promptValues.contains(OidcPrompt.NONE)) {
 				// Return an error instead of displaying the consent page
 				throwError("consent_required", "prompt", authorizationCodeRequestAuthentication, registeredClient);
 			}
@@ -347,6 +333,37 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
 		this.authorizationConsentRequired = authorizationConsentRequired;
 	}
 
+	private OAuth2AuthorizationCodeRequestAuthenticationToken fromPushedAuthorizationRequest(
+			OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication) {
+
+		String requestUri = (String) authorizationCodeRequestAuthentication.getAdditionalParameters()
+			.get("request_uri");
+
+		OAuth2PushedAuthorizationRequestUri pushedAuthorizationRequestUri = null;
+		try {
+			pushedAuthorizationRequestUri = OAuth2PushedAuthorizationRequestUri.parse(requestUri);
+		}
+		catch (Exception ex) {
+			throwError(OAuth2ErrorCodes.INVALID_REQUEST, "request_uri", authorizationCodeRequestAuthentication, null);
+		}
+
+		OAuth2Authorization authorization = this.authorizationService
+			.findByToken(pushedAuthorizationRequestUri.getState(), STATE_TOKEN_TYPE);
+		if (authorization == null) {
+			throwError(OAuth2ErrorCodes.INVALID_REQUEST, "request_uri", authorizationCodeRequestAuthentication, null);
+		}
+
+		OAuth2AuthorizationRequest authorizationRequest = authorization
+			.getAttribute(OAuth2AuthorizationRequest.class.getName());
+
+		return new OAuth2AuthorizationCodeRequestAuthenticationToken(
+				authorizationCodeRequestAuthentication.getAuthorizationUri(),
+				authorizationCodeRequestAuthentication.getClientId(),
+				(Authentication) authorizationCodeRequestAuthentication.getPrincipal(),
+				authorizationRequest.getRedirectUri(), authorizationRequest.getState(),
+				authorizationRequest.getScopes(), authorizationRequest.getAdditionalParameters());
+	}
+
 	private static boolean isAuthorizationConsentRequired(
 			OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext) {
 		if (!authenticationContext.getRegisteredClient().getClientSettings().isRequireAuthorizationConsent()) {
@@ -457,23 +474,4 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
 		return null;
 	}
 
-	/*
-	 * The values defined for the "prompt" parameter for the OpenID Connect 1.0
-	 * Authentication Request.
-	 */
-	private static final class OidcPrompts {
-
-		private static final String NONE = "none";
-
-		private static final String LOGIN = "login";
-
-		private static final String CONSENT = "consent";
-
-		private static final String SELECT_ACCOUNT = "select_account";
-
-		private OidcPrompts() {
-		}
-
-	}
-
 }

+ 5 - 107
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationToken.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-2022 the original author or authors.
+ * Copyright 2020-2025 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.
@@ -15,17 +15,12 @@
  */
 package org.springframework.security.oauth2.server.authorization.authentication;
 
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 
 import org.springframework.lang.Nullable;
-import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
-import org.springframework.security.oauth2.server.authorization.util.SpringAuthorizationServerVersion;
 import org.springframework.util.Assert;
 
 /**
@@ -37,23 +32,8 @@ import org.springframework.util.Assert;
  * @see OAuth2AuthorizationCodeRequestAuthenticationProvider
  * @see OAuth2AuthorizationConsentAuthenticationProvider
  */
-public class OAuth2AuthorizationCodeRequestAuthenticationToken extends AbstractAuthenticationToken {
-
-	private static final long serialVersionUID = SpringAuthorizationServerVersion.SERIAL_VERSION_UID;
-
-	private final String authorizationUri;
-
-	private final String clientId;
-
-	private final Authentication principal;
-
-	private final String redirectUri;
-
-	private final String state;
-
-	private final Set<String> scopes;
-
-	private final Map<String, Object> additionalParameters;
+public class OAuth2AuthorizationCodeRequestAuthenticationToken
+		extends AbstractOAuth2AuthorizationCodeRequestAuthenticationToken {
 
 	private final OAuth2AuthorizationCode authorizationCode;
 
@@ -72,18 +52,7 @@ public class OAuth2AuthorizationCodeRequestAuthenticationToken extends AbstractA
 	public OAuth2AuthorizationCodeRequestAuthenticationToken(String authorizationUri, String clientId,
 			Authentication principal, @Nullable String redirectUri, @Nullable String state,
 			@Nullable Set<String> scopes, @Nullable Map<String, Object> additionalParameters) {
-		super(Collections.emptyList());
-		Assert.hasText(authorizationUri, "authorizationUri cannot be empty");
-		Assert.hasText(clientId, "clientId cannot be empty");
-		Assert.notNull(principal, "principal cannot be null");
-		this.authorizationUri = authorizationUri;
-		this.clientId = clientId;
-		this.principal = principal;
-		this.redirectUri = redirectUri;
-		this.state = state;
-		this.scopes = Collections.unmodifiableSet((scopes != null) ? new HashSet<>(scopes) : Collections.emptySet());
-		this.additionalParameters = Collections.unmodifiableMap(
-				(additionalParameters != null) ? new HashMap<>(additionalParameters) : Collections.emptyMap());
+		super(authorizationUri, clientId, principal, redirectUri, state, scopes, additionalParameters);
 		this.authorizationCode = null;
 	}
 
@@ -102,83 +71,12 @@ public class OAuth2AuthorizationCodeRequestAuthenticationToken extends AbstractA
 	public OAuth2AuthorizationCodeRequestAuthenticationToken(String authorizationUri, String clientId,
 			Authentication principal, OAuth2AuthorizationCode authorizationCode, @Nullable String redirectUri,
 			@Nullable String state, @Nullable Set<String> scopes) {
-		super(Collections.emptyList());
-		Assert.hasText(authorizationUri, "authorizationUri cannot be empty");
-		Assert.hasText(clientId, "clientId cannot be empty");
-		Assert.notNull(principal, "principal cannot be null");
+		super(authorizationUri, clientId, principal, redirectUri, state, scopes, null);
 		Assert.notNull(authorizationCode, "authorizationCode cannot be null");
-		this.authorizationUri = authorizationUri;
-		this.clientId = clientId;
-		this.principal = principal;
 		this.authorizationCode = authorizationCode;
-		this.redirectUri = redirectUri;
-		this.state = state;
-		this.scopes = Collections.unmodifiableSet((scopes != null) ? new HashSet<>(scopes) : Collections.emptySet());
-		this.additionalParameters = Collections.emptyMap();
 		setAuthenticated(true);
 	}
 
-	@Override
-	public Object getPrincipal() {
-		return this.principal;
-	}
-
-	@Override
-	public Object getCredentials() {
-		return "";
-	}
-
-	/**
-	 * Returns the authorization URI.
-	 * @return the authorization URI
-	 */
-	public String getAuthorizationUri() {
-		return this.authorizationUri;
-	}
-
-	/**
-	 * Returns the client identifier.
-	 * @return the client identifier
-	 */
-	public String getClientId() {
-		return this.clientId;
-	}
-
-	/**
-	 * Returns the redirect uri.
-	 * @return the redirect uri
-	 */
-	@Nullable
-	public String getRedirectUri() {
-		return this.redirectUri;
-	}
-
-	/**
-	 * Returns the state.
-	 * @return the state
-	 */
-	@Nullable
-	public String getState() {
-		return this.state;
-	}
-
-	/**
-	 * Returns the requested (or authorized) scope(s).
-	 * @return the requested (or authorized) scope(s), or an empty {@code Set} if not
-	 * available
-	 */
-	public Set<String> getScopes() {
-		return this.scopes;
-	}
-
-	/**
-	 * Returns the additional parameters.
-	 * @return the additional parameters, or an empty {@code Map} if not available
-	 */
-	public Map<String, Object> getAdditionalParameters() {
-		return this.additionalParameters;
-	}
-
 	/**
 	 * Returns the {@link OAuth2AuthorizationCode}.
 	 * @return the {@link OAuth2AuthorizationCode}

+ 94 - 14
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationValidator.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-2023 the original author or authors.
+ * Copyright 2020-2025 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.
@@ -15,6 +15,8 @@
  */
 package org.springframework.security.oauth2.server.authorization.authentication;
 
+import java.util.Arrays;
+import java.util.HashSet;
 import java.util.Set;
 import java.util.function.Consumer;
 
@@ -23,9 +25,11 @@ import org.apache.commons.logging.LogFactory;
 
 import org.springframework.core.log.LogMessage;
 import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
 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.core.endpoint.PkceParameterNames;
 import org.springframework.security.oauth2.core.oidc.OidcScopes;
 import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
 import org.springframework.util.StringUtils;
@@ -51,25 +55,34 @@ import org.springframework.web.util.UriComponentsBuilder;
  * @see OAuth2AuthorizationCodeRequestAuthenticationContext
  * @see OAuth2AuthorizationCodeRequestAuthenticationToken
  * @see OAuth2AuthorizationCodeRequestAuthenticationProvider#setAuthenticationValidator(Consumer)
+ * @see OAuth2PushedAuthorizationRequestAuthenticationProvider#setAuthenticationValidator(Consumer)
  */
 public final class OAuth2AuthorizationCodeRequestAuthenticationValidator
 		implements Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> {
 
 	private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1";
 
+	private static final String PKCE_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7636#section-4.4.1";
+
 	private static final Log LOGGER = LogFactory.getLog(OAuth2AuthorizationCodeRequestAuthenticationValidator.class);
 
+	static final Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> DEFAULT_AUTHORIZATION_GRANT_TYPE_VALIDATOR = OAuth2AuthorizationCodeRequestAuthenticationValidator::validateAuthorizationGrantType;
+
+	static final Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> DEFAULT_CODE_CHALLENGE_VALIDATOR = OAuth2AuthorizationCodeRequestAuthenticationValidator::validateCodeChallenge;
+
+	static final Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> DEFAULT_PROMPT_VALIDATOR = OAuth2AuthorizationCodeRequestAuthenticationValidator::validatePrompt;
+
 	/**
 	 * The default validator for
-	 * {@link OAuth2AuthorizationCodeRequestAuthenticationToken#getScopes()}.
+	 * {@link OAuth2AuthorizationCodeRequestAuthenticationToken#getRedirectUri()}.
 	 */
-	public static final Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> DEFAULT_SCOPE_VALIDATOR = OAuth2AuthorizationCodeRequestAuthenticationValidator::validateScope;
+	public static final Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> DEFAULT_REDIRECT_URI_VALIDATOR = OAuth2AuthorizationCodeRequestAuthenticationValidator::validateRedirectUri;
 
 	/**
 	 * The default validator for
-	 * {@link OAuth2AuthorizationCodeRequestAuthenticationToken#getRedirectUri()}.
+	 * {@link OAuth2AuthorizationCodeRequestAuthenticationToken#getScopes()}.
 	 */
-	public static final Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> DEFAULT_REDIRECT_URI_VALIDATOR = OAuth2AuthorizationCodeRequestAuthenticationValidator::validateRedirectUri;
+	public static final Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> DEFAULT_SCOPE_VALIDATOR = OAuth2AuthorizationCodeRequestAuthenticationValidator::validateScope;
 
 	private final Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator = DEFAULT_REDIRECT_URI_VALIDATOR
 		.andThen(DEFAULT_SCOPE_VALIDATOR);
@@ -79,20 +92,18 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationValidator
 		this.authenticationValidator.accept(authenticationContext);
 	}
 
-	private static void validateScope(OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext) {
+	private static void validateAuthorizationGrantType(
+			OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext) {
 		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = authenticationContext
 			.getAuthentication();
 		RegisteredClient registeredClient = authenticationContext.getRegisteredClient();
-
-		Set<String> requestedScopes = authorizationCodeRequestAuthentication.getScopes();
-		Set<String> allowedScopes = registeredClient.getScopes();
-		if (!requestedScopes.isEmpty() && !allowedScopes.containsAll(requestedScopes)) {
+		if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.AUTHORIZATION_CODE)) {
 			if (LOGGER.isDebugEnabled()) {
 				LOGGER.debug(LogMessage.format(
-						"Invalid request: requested scope is not allowed" + " for registered client '%s'",
+						"Invalid request: requested grant_type is not allowed for registered client '%s'",
 						registeredClient.getId()));
 			}
-			throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE,
+			throwError(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT, OAuth2ParameterNames.CLIENT_ID,
 					authorizationCodeRequestAuthentication, registeredClient);
 		}
 	}
@@ -151,7 +162,7 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationValidator
 				if (!validRedirectUri) {
 					if (LOGGER.isDebugEnabled()) {
 						LOGGER.debug(LogMessage.format(
-								"Invalid request: redirect_uri does not match" + " for registered client '%s'",
+								"Invalid request: redirect_uri does not match for registered client '%s'",
 								registeredClient.getId()));
 					}
 					throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
@@ -172,6 +183,69 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationValidator
 		}
 	}
 
+	private static void validateScope(OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext) {
+		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = authenticationContext
+			.getAuthentication();
+		RegisteredClient registeredClient = authenticationContext.getRegisteredClient();
+
+		Set<String> requestedScopes = authorizationCodeRequestAuthentication.getScopes();
+		Set<String> allowedScopes = registeredClient.getScopes();
+		if (!requestedScopes.isEmpty() && !allowedScopes.containsAll(requestedScopes)) {
+			if (LOGGER.isDebugEnabled()) {
+				LOGGER.debug(
+						LogMessage.format("Invalid request: requested scope is not allowed for registered client '%s'",
+								registeredClient.getId()));
+			}
+			throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE,
+					authorizationCodeRequestAuthentication, registeredClient);
+		}
+	}
+
+	private static void validateCodeChallenge(
+			OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext) {
+		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = authenticationContext
+			.getAuthentication();
+		RegisteredClient registeredClient = authenticationContext.getRegisteredClient();
+
+		// code_challenge (REQUIRED for public clients) - RFC 7636 (PKCE)
+		String codeChallenge = (String) authorizationCodeRequestAuthentication.getAdditionalParameters()
+			.get(PkceParameterNames.CODE_CHALLENGE);
+		if (StringUtils.hasText(codeChallenge)) {
+			String codeChallengeMethod = (String) authorizationCodeRequestAuthentication.getAdditionalParameters()
+				.get(PkceParameterNames.CODE_CHALLENGE_METHOD);
+			if (!StringUtils.hasText(codeChallengeMethod) || !"S256".equals(codeChallengeMethod)) {
+				throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD, PKCE_ERROR_URI,
+						authorizationCodeRequestAuthentication, registeredClient);
+			}
+		}
+		else if (registeredClient.getClientSettings().isRequireProofKey()) {
+			throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE, PKCE_ERROR_URI,
+					authorizationCodeRequestAuthentication, registeredClient);
+		}
+	}
+
+	private static void validatePrompt(OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext) {
+		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = authenticationContext
+			.getAuthentication();
+		RegisteredClient registeredClient = authenticationContext.getRegisteredClient();
+
+		// prompt (OPTIONAL for OpenID Connect 1.0 Authentication Request)
+		if (authorizationCodeRequestAuthentication.getScopes().contains(OidcScopes.OPENID)) {
+			String prompt = (String) authorizationCodeRequestAuthentication.getAdditionalParameters().get("prompt");
+			if (StringUtils.hasText(prompt)) {
+				Set<String> promptValues = new HashSet<>(
+						Arrays.asList(StringUtils.delimitedListToStringArray(prompt, " ")));
+				if (promptValues.contains(OidcPrompt.NONE)) {
+					if (promptValues.contains(OidcPrompt.LOGIN) || promptValues.contains(OidcPrompt.CONSENT)
+							|| promptValues.contains(OidcPrompt.SELECT_ACCOUNT)) {
+						throwError(OAuth2ErrorCodes.INVALID_REQUEST, "prompt", authorizationCodeRequestAuthentication,
+								registeredClient);
+					}
+				}
+			}
+		}
+	}
+
 	private static boolean isLoopbackAddress(String host) {
 		if (!StringUtils.hasText(host)) {
 			return false;
@@ -201,7 +275,13 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationValidator
 	private static void throwError(String errorCode, String parameterName,
 			OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
 			RegisteredClient registeredClient) {
-		OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, ERROR_URI);
+		throwError(errorCode, parameterName, ERROR_URI, authorizationCodeRequestAuthentication, registeredClient);
+	}
+
+	private static void throwError(String errorCode, String parameterName, String errorUri,
+			OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
+			RegisteredClient registeredClient) {
+		OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, errorUri);
 		throwError(error, parameterName, authorizationCodeRequestAuthentication, registeredClient);
 	}
 

+ 184 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2PushedAuthorizationRequestAuthenticationProvider.java

@@ -0,0 +1,184 @@
+/*
+ * Copyright 2020-2025 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.function.Consumer;
+
+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.endpoint.OAuth2AuthorizationRequest;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link AuthenticationProvider} implementation for the OAuth 2.0 Pushed Authorization
+ * Request used in the Authorization Code Grant.
+ *
+ * @author Joe Grandja
+ * @since 1.5
+ * @see OAuth2PushedAuthorizationRequestAuthenticationToken
+ * @see OAuth2AuthorizationCodeRequestAuthenticationToken
+ * @see OAuth2AuthorizationCodeRequestAuthenticationValidator
+ * @see OAuth2AuthorizationService
+ * @see <a target="_blank" href=
+ * "https://datatracker.ietf.org/doc/html/rfc9126#section-2.1">Section 2.1 Pushed
+ * Authorization Request</a>
+ * @see <a target="_blank" href=
+ * "https://datatracker.ietf.org/doc/html/rfc9126#section-2.2">Section 2.2 Pushed
+ * Authorization Response</a>
+ */
+public final class OAuth2PushedAuthorizationRequestAuthenticationProvider implements AuthenticationProvider {
+
+	private final Log logger = LogFactory.getLog(getClass());
+
+	private final OAuth2AuthorizationService authorizationService;
+
+	private Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator = new OAuth2AuthorizationCodeRequestAuthenticationValidator();
+
+	/**
+	 * Constructs an {@code OAuth2PushedAuthorizationRequestAuthenticationProvider} using
+	 * the provided parameters.
+	 * @param authorizationService the authorization service
+	 */
+	public OAuth2PushedAuthorizationRequestAuthenticationProvider(OAuth2AuthorizationService authorizationService) {
+		Assert.notNull(authorizationService, "authorizationService cannot be null");
+		this.authorizationService = authorizationService;
+	}
+
+	@Override
+	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+		OAuth2PushedAuthorizationRequestAuthenticationToken pushedAuthorizationRequestAuthentication = (OAuth2PushedAuthorizationRequestAuthenticationToken) authentication;
+
+		OAuth2ClientAuthenticationToken clientPrincipal = OAuth2AuthenticationProviderUtils
+			.getAuthenticatedClientElseThrowInvalidClient(pushedAuthorizationRequestAuthentication);
+		RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Retrieved registered client");
+		}
+
+		OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext = OAuth2AuthorizationCodeRequestAuthenticationContext
+			.with(toAuthorizationCodeRequestAuthentication(pushedAuthorizationRequestAuthentication))
+			.registeredClient(registeredClient)
+			.build();
+
+		// grant_type
+		OAuth2AuthorizationCodeRequestAuthenticationValidator.DEFAULT_AUTHORIZATION_GRANT_TYPE_VALIDATOR
+			.accept(authenticationContext);
+
+		// redirect_uri and scope
+		this.authenticationValidator.accept(authenticationContext);
+
+		// code_challenge (REQUIRED for public clients) - RFC 7636 (PKCE)
+		OAuth2AuthorizationCodeRequestAuthenticationValidator.DEFAULT_CODE_CHALLENGE_VALIDATOR
+			.accept(authenticationContext);
+
+		// prompt (OPTIONAL for OpenID Connect 1.0 Authentication Request)
+		OAuth2AuthorizationCodeRequestAuthenticationValidator.DEFAULT_PROMPT_VALIDATOR.accept(authenticationContext);
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Validated pushed authorization request parameters");
+		}
+
+		// @formatter:off
+		OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
+			.authorizationUri(pushedAuthorizationRequestAuthentication.getAuthorizationUri())
+			.clientId(registeredClient.getClientId())
+			.redirectUri(pushedAuthorizationRequestAuthentication.getRedirectUri())
+			.scopes(pushedAuthorizationRequestAuthentication.getScopes())
+			.state(pushedAuthorizationRequestAuthentication.getState())
+			.additionalParameters(pushedAuthorizationRequestAuthentication.getAdditionalParameters())
+			.build();
+		// @formatter:on
+
+		OAuth2PushedAuthorizationRequestUri pushedAuthorizationRequestUri = OAuth2PushedAuthorizationRequestUri
+			.create();
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Generated pushed authorization request uri");
+		}
+
+		// @formatter:off
+		OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(registeredClient)
+				.principalName(clientPrincipal.getName())
+				.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
+				.attribute(OAuth2AuthorizationRequest.class.getName(), authorizationRequest)
+				.attribute(OAuth2ParameterNames.STATE, pushedAuthorizationRequestUri.getState())
+				.build();
+		// @formatter:on
+		this.authorizationService.save(authorization);
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Saved authorization");
+		}
+
+		if (this.logger.isTraceEnabled()) {
+			this.logger.trace("Authenticated pushed authorization request");
+		}
+
+		return new OAuth2PushedAuthorizationRequestAuthenticationToken(authorizationRequest.getAuthorizationUri(),
+				authorizationRequest.getClientId(), clientPrincipal, pushedAuthorizationRequestUri.getRequestUri(),
+				pushedAuthorizationRequestUri.getExpiresAt(), authorizationRequest.getRedirectUri(),
+				authorizationRequest.getState(), authorizationRequest.getScopes());
+	}
+
+	@Override
+	public boolean supports(Class<?> authentication) {
+		return OAuth2PushedAuthorizationRequestAuthenticationToken.class.isAssignableFrom(authentication);
+	}
+
+	/**
+	 * Sets the {@code Consumer} providing access to the
+	 * {@link OAuth2AuthorizationCodeRequestAuthenticationContext} and is responsible for
+	 * validating specific OAuth 2.0 Pushed Authorization Request parameters associated in
+	 * the {@link OAuth2AuthorizationCodeRequestAuthenticationToken}. The default
+	 * authentication validator is
+	 * {@link OAuth2AuthorizationCodeRequestAuthenticationValidator}.
+	 *
+	 * <p>
+	 * <b>NOTE:</b> The authentication validator MUST throw
+	 * {@link OAuth2AuthorizationCodeRequestAuthenticationException} if validation fails.
+	 * @param authenticationValidator the {@code Consumer} providing access to the
+	 * {@link OAuth2AuthorizationCodeRequestAuthenticationContext} and is responsible for
+	 * validating specific OAuth 2.0 Pushed Authorization Request parameters
+	 */
+	public void setAuthenticationValidator(
+			Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator) {
+		Assert.notNull(authenticationValidator, "authenticationValidator cannot be null");
+		this.authenticationValidator = authenticationValidator;
+	}
+
+	private static OAuth2AuthorizationCodeRequestAuthenticationToken toAuthorizationCodeRequestAuthentication(
+			OAuth2PushedAuthorizationRequestAuthenticationToken pushedAuthorizationCodeRequestAuthentication) {
+		return new OAuth2AuthorizationCodeRequestAuthenticationToken(
+				pushedAuthorizationCodeRequestAuthentication.getAuthorizationUri(),
+				pushedAuthorizationCodeRequestAuthentication.getClientId(),
+				(Authentication) pushedAuthorizationCodeRequestAuthentication.getPrincipal(),
+				pushedAuthorizationCodeRequestAuthentication.getRedirectUri(),
+				pushedAuthorizationCodeRequestAuthentication.getState(),
+				pushedAuthorizationCodeRequestAuthentication.getScopes(),
+				pushedAuthorizationCodeRequestAuthentication.getAdditionalParameters());
+	}
+
+}

+ 94 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2PushedAuthorizationRequestAuthenticationToken.java

@@ -0,0 +1,94 @@
+/*
+ * Copyright 2020-2025 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.time.Instant;
+import java.util.Map;
+import java.util.Set;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.core.Authentication;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link Authentication} implementation for the OAuth 2.0 Pushed Authorization Request
+ * used in the Authorization Code Grant.
+ *
+ * @author Joe Grandja
+ * @since 1.5
+ * @see OAuth2PushedAuthorizationRequestAuthenticationProvider
+ */
+public class OAuth2PushedAuthorizationRequestAuthenticationToken
+		extends AbstractOAuth2AuthorizationCodeRequestAuthenticationToken {
+
+	private final String requestUri;
+
+	private final Instant requestUriExpiresAt;
+
+	/**
+	 * Constructs an {@code OAuth2PushedAuthorizationRequestAuthenticationToken} using the
+	 * provided parameters.
+	 * @param authorizationUri the authorization URI
+	 * @param clientId the client identifier
+	 * @param principal the authenticated client principal
+	 * @param redirectUri the redirect uri
+	 * @param state the state
+	 * @param scopes the requested scope(s)
+	 * @param additionalParameters the additional parameters
+	 */
+	public OAuth2PushedAuthorizationRequestAuthenticationToken(String authorizationUri, String clientId,
+			Authentication principal, @Nullable String redirectUri, @Nullable String state,
+			@Nullable Set<String> scopes, @Nullable Map<String, Object> additionalParameters) {
+		super(authorizationUri, clientId, principal, redirectUri, state, scopes, additionalParameters);
+		this.requestUri = null;
+		this.requestUriExpiresAt = null;
+	}
+
+	/**
+	 * Constructs an {@code OAuth2PushedAuthorizationRequestAuthenticationToken} using the
+	 * provided parameters.
+	 * @param authorizationUri the authorization URI
+	 * @param clientId the client identifier
+	 * @param principal the authenticated client principal
+	 * @param requestUri the request URI corresponding to the authorization request posted
+	 * @param requestUriExpiresAt the expiration time on or after which the
+	 * {@code requestUri} MUST NOT be accepted
+	 * @param redirectUri the redirect uri
+	 * @param state the state
+	 * @param scopes the authorized scope(s)
+	 */
+	public OAuth2PushedAuthorizationRequestAuthenticationToken(String authorizationUri, String clientId,
+			Authentication principal, String requestUri, Instant requestUriExpiresAt, @Nullable String redirectUri,
+			@Nullable String state, @Nullable Set<String> scopes) {
+		super(authorizationUri, clientId, principal, redirectUri, state, scopes, null);
+		Assert.hasText(requestUri, "requestUri cannot be empty");
+		Assert.notNull(requestUriExpiresAt, "requestUriExpiresAt cannot be null");
+		this.requestUri = requestUri;
+		this.requestUriExpiresAt = requestUriExpiresAt;
+		setAuthenticated(true);
+	}
+
+	@Nullable
+	public String getRequestUri() {
+		return this.requestUri;
+	}
+
+	@Nullable
+	public Instant getRequestUriExpiresAt() {
+		return this.requestUriExpiresAt;
+	}
+
+}

+ 80 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2PushedAuthorizationRequestUri.java

@@ -0,0 +1,80 @@
+/*
+ * Copyright 2020-2025 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.time.Instant;
+import java.util.Base64;
+
+import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
+import org.springframework.security.crypto.keygen.StringKeyGenerator;
+
+/**
+ * @author Joe Grandja
+ * @since 1.5
+ */
+final class OAuth2PushedAuthorizationRequestUri {
+
+	private static final String REQUEST_URI_PREFIX = "urn:ietf:params:oauth:request_uri:";
+
+	private static final String REQUEST_URI_DELIMITER = "___";
+
+	private static final StringKeyGenerator DEFAULT_STATE_GENERATOR = new Base64StringKeyGenerator(
+			Base64.getUrlEncoder());
+
+	private String requestUri;
+
+	private String state;
+
+	private Instant expiresAt;
+
+	static OAuth2PushedAuthorizationRequestUri create() {
+		String state = DEFAULT_STATE_GENERATOR.generateKey();
+		Instant expiresAt = Instant.now().plusSeconds(30);
+		OAuth2PushedAuthorizationRequestUri pushedAuthorizationRequestUri = new OAuth2PushedAuthorizationRequestUri();
+		pushedAuthorizationRequestUri.requestUri = REQUEST_URI_PREFIX + state + REQUEST_URI_DELIMITER
+				+ expiresAt.toEpochMilli();
+		pushedAuthorizationRequestUri.state = state + REQUEST_URI_DELIMITER + expiresAt.toEpochMilli();
+		pushedAuthorizationRequestUri.expiresAt = expiresAt;
+		return pushedAuthorizationRequestUri;
+	}
+
+	static OAuth2PushedAuthorizationRequestUri parse(String requestUri) {
+		int stateStartIndex = REQUEST_URI_PREFIX.length();
+		int expiresAtStartIndex = requestUri.indexOf(REQUEST_URI_DELIMITER) + REQUEST_URI_DELIMITER.length();
+		OAuth2PushedAuthorizationRequestUri pushedAuthorizationRequestUri = new OAuth2PushedAuthorizationRequestUri();
+		pushedAuthorizationRequestUri.requestUri = requestUri;
+		pushedAuthorizationRequestUri.state = requestUri.substring(stateStartIndex);
+		pushedAuthorizationRequestUri.expiresAt = Instant
+			.ofEpochMilli(Long.parseLong(requestUri.substring(expiresAtStartIndex)));
+		return pushedAuthorizationRequestUri;
+	}
+
+	String getRequestUri() {
+		return this.requestUri;
+	}
+
+	String getState() {
+		return this.state;
+	}
+
+	Instant getExpiresAt() {
+		return this.expiresAt;
+	}
+
+	private OAuth2PushedAuthorizationRequestUri() {
+	}
+
+}

+ 38 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OidcPrompt.java

@@ -0,0 +1,38 @@
+/*
+ * Copyright 2020-2025 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;
+
+/**
+ * The values defined for the "prompt" parameter for the OpenID Connect 1.0 Authentication
+ * Request.
+ *
+ * @author Joe Grandja
+ * @since 1.5
+ */
+final class OidcPrompt {
+
+	static final String NONE = "none";
+
+	static final String LOGIN = "login";
+
+	static final String CONSENT = "consent";
+
+	static final String SELECT_ACCOUNT = "select_account";
+
+	private OidcPrompt() {
+	}
+
+}

+ 55 - 16
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerConfigurer.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-2024 the original author or authors.
+ * Copyright 2020-2025 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.
@@ -20,6 +20,7 @@ import java.util.ArrayList;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Consumer;
 
 import com.nimbusds.jose.jwk.source.JWKSource;
 
@@ -42,6 +43,7 @@ import org.springframework.security.oauth2.core.OAuth2Token;
 import org.springframework.security.oauth2.core.oidc.OidcScopes;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationContext;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken;
 import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
@@ -69,6 +71,7 @@ import org.springframework.util.Assert;
  * @see OAuth2ClientAuthenticationConfigurer
  * @see OAuth2AuthorizationServerMetadataEndpointConfigurer
  * @see OAuth2AuthorizationEndpointConfigurer
+ * @see OAuth2PushedAuthorizationRequestEndpointConfigurer
  * @see OAuth2TokenEndpointConfigurer
  * @see OAuth2TokenIntrospectionEndpointConfigurer
  * @see OAuth2TokenRevocationEndpointConfigurer
@@ -196,6 +199,27 @@ public final class OAuth2AuthorizationServerConfigurer
 		return this;
 	}
 
+	/**
+	 * Configures the OAuth 2.0 Pushed Authorization Request Endpoint.
+	 * @param pushedAuthorizationRequestEndpointCustomizer the {@link Customizer}
+	 * providing access to the {@link OAuth2PushedAuthorizationRequestEndpointConfigurer}
+	 * @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
+	 * @since 1.5
+	 */
+	public OAuth2AuthorizationServerConfigurer pushedAuthorizationRequestEndpoint(
+			Customizer<OAuth2PushedAuthorizationRequestEndpointConfigurer> pushedAuthorizationRequestEndpointCustomizer) {
+		OAuth2PushedAuthorizationRequestEndpointConfigurer pushedAuthorizationRequestEndpointConfigurer = getConfigurer(
+				OAuth2PushedAuthorizationRequestEndpointConfigurer.class);
+		if (pushedAuthorizationRequestEndpointConfigurer == null) {
+			addConfigurer(OAuth2PushedAuthorizationRequestEndpointConfigurer.class,
+					new OAuth2PushedAuthorizationRequestEndpointConfigurer(this::postProcess));
+			pushedAuthorizationRequestEndpointConfigurer = getConfigurer(
+					OAuth2PushedAuthorizationRequestEndpointConfigurer.class);
+		}
+		pushedAuthorizationRequestEndpointCustomizer.customize(pushedAuthorizationRequestEndpointConfigurer);
+		return this;
+	}
+
 	/**
 	 * Configures the OAuth 2.0 Token Endpoint.
 	 * @param tokenEndpointCustomizer the {@link Customizer} providing access to the
@@ -314,20 +338,28 @@ public final class OAuth2AuthorizationServerConfigurer
 		else {
 			// OpenID Connect is disabled.
 			// Add an authentication validator that rejects authentication requests.
+			Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> oidcAuthenticationRequestValidator = (
+					authenticationContext) -> {
+				OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = authenticationContext
+					.getAuthentication();
+				if (authorizationCodeRequestAuthentication.getScopes().contains(OidcScopes.OPENID)) {
+					OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_SCOPE,
+							"OpenID Connect 1.0 authentication requests are restricted.",
+							"https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1");
+					throw new OAuth2AuthorizationCodeRequestAuthenticationException(error,
+							authorizationCodeRequestAuthentication);
+				}
+			};
 			OAuth2AuthorizationEndpointConfigurer authorizationEndpointConfigurer = getConfigurer(
 					OAuth2AuthorizationEndpointConfigurer.class);
 			authorizationEndpointConfigurer
-				.addAuthorizationCodeRequestAuthenticationValidator((authenticationContext) -> {
-					OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = authenticationContext
-						.getAuthentication();
-					if (authorizationCodeRequestAuthentication.getScopes().contains(OidcScopes.OPENID)) {
-						OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_SCOPE,
-								"OpenID Connect 1.0 authentication requests are restricted.",
-								"https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1");
-						throw new OAuth2AuthorizationCodeRequestAuthenticationException(error,
-								authorizationCodeRequestAuthentication);
-					}
-				});
+				.addAuthorizationCodeRequestAuthenticationValidator(oidcAuthenticationRequestValidator);
+			OAuth2PushedAuthorizationRequestEndpointConfigurer pushedAuthorizationRequestEndpointConfigurer = getConfigurer(
+					OAuth2PushedAuthorizationRequestEndpointConfigurer.class);
+			if (pushedAuthorizationRequestEndpointConfigurer != null) {
+				pushedAuthorizationRequestEndpointConfigurer
+					.addAuthorizationCodeRequestAuthenticationValidator(oidcAuthenticationRequestValidator);
+			}
 		}
 
 		List<RequestMatcher> requestMatchers = new ArrayList<>();
@@ -344,11 +376,18 @@ public final class OAuth2AuthorizationServerConfigurer
 		ExceptionHandlingConfigurer<HttpSecurity> exceptionHandling = httpSecurity
 			.getConfigurer(ExceptionHandlingConfigurer.class);
 		if (exceptionHandling != null) {
+			List<RequestMatcher> preferredMatchers = new ArrayList<>();
+			preferredMatchers.add(getRequestMatcher(OAuth2TokenEndpointConfigurer.class));
+			preferredMatchers.add(getRequestMatcher(OAuth2TokenIntrospectionEndpointConfigurer.class));
+			preferredMatchers.add(getRequestMatcher(OAuth2TokenRevocationEndpointConfigurer.class));
+			preferredMatchers.add(getRequestMatcher(OAuth2DeviceAuthorizationEndpointConfigurer.class));
+			RequestMatcher preferredMatcher = getRequestMatcher(
+					OAuth2PushedAuthorizationRequestEndpointConfigurer.class);
+			if (preferredMatcher != null) {
+				preferredMatchers.add(preferredMatcher);
+			}
 			exceptionHandling.defaultAuthenticationEntryPointFor(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED),
-					new OrRequestMatcher(getRequestMatcher(OAuth2TokenEndpointConfigurer.class),
-							getRequestMatcher(OAuth2TokenIntrospectionEndpointConfigurer.class),
-							getRequestMatcher(OAuth2TokenRevocationEndpointConfigurer.class),
-							getRequestMatcher(OAuth2DeviceAuthorizationEndpointConfigurer.class)));
+					new OrRequestMatcher(preferredMatchers));
 		}
 
 		httpSecurity.csrf((csrf) -> csrf.ignoringRequestMatchers(this.endpointsMatcher));

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

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-2024 the original author or authors.
+ * Copyright 2020-2025 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.
@@ -196,10 +196,15 @@ public final class OAuth2ClientAuthenticationConfigurer extends AbstractOAuth2Co
 				? OAuth2ConfigurerUtils
 					.withMultipleIssuersPattern(authorizationServerSettings.getDeviceAuthorizationEndpoint())
 				: authorizationServerSettings.getDeviceAuthorizationEndpoint();
+		String pushedAuthorizationRequestEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
+				? OAuth2ConfigurerUtils
+					.withMultipleIssuersPattern(authorizationServerSettings.getPushedAuthorizationRequestEndpoint())
+				: authorizationServerSettings.getPushedAuthorizationRequestEndpoint();
 		this.requestMatcher = new OrRequestMatcher(new AntPathRequestMatcher(tokenEndpointUri, HttpMethod.POST.name()),
 				new AntPathRequestMatcher(tokenIntrospectionEndpointUri, HttpMethod.POST.name()),
 				new AntPathRequestMatcher(tokenRevocationEndpointUri, HttpMethod.POST.name()),
-				new AntPathRequestMatcher(deviceAuthorizationEndpointUri, HttpMethod.POST.name()));
+				new AntPathRequestMatcher(deviceAuthorizationEndpointUri, HttpMethod.POST.name()),
+				new AntPathRequestMatcher(pushedAuthorizationRequestEndpointUri, HttpMethod.POST.name()));
 		List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity);
 		if (!this.authenticationProviders.isEmpty()) {
 			authenticationProviders.addAll(0, this.authenticationProviders);

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

@@ -0,0 +1,265 @@
+/*
+ * Copyright 2020-2025 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.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+import org.springframework.http.HttpMethod;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.config.annotation.ObjectPostProcessor;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationContext;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationValidator;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2PushedAuthorizationRequestAuthenticationProvider;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2PushedAuthorizationRequestAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
+import org.springframework.security.oauth2.server.authorization.web.OAuth2PushedAuthorizationRequestEndpointFilter;
+import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeRequestAuthenticationConverter;
+import org.springframework.security.web.access.intercept.AuthorizationFilter;
+import org.springframework.security.web.authentication.AuthenticationConverter;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.security.web.authentication.DelegatingAuthenticationConverter;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.util.Assert;
+
+/**
+ * Configurer for the OAuth 2.0 Pushed Authorization Request Endpoint.
+ *
+ * @author Joe Grandja
+ * @since 1.5
+ * @see OAuth2AuthorizationServerConfigurer#pushedAuthorizationRequestEndpoint
+ * @see OAuth2PushedAuthorizationRequestEndpointFilter
+ */
+public final class OAuth2PushedAuthorizationRequestEndpointConfigurer extends AbstractOAuth2Configurer {
+
+	private RequestMatcher requestMatcher;
+
+	private final List<AuthenticationConverter> pushedAuthorizationRequestConverters = new ArrayList<>();
+
+	private Consumer<List<AuthenticationConverter>> pushedAuthorizationRequestConvertersConsumer = (
+			authorizationRequestConverters) -> {
+	};
+
+	private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
+
+	private Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer = (authenticationProviders) -> {
+	};
+
+	private AuthenticationSuccessHandler pushedAuthorizationResponseHandler;
+
+	private AuthenticationFailureHandler errorResponseHandler;
+
+	private Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authorizationCodeRequestAuthenticationValidator;
+
+	/**
+	 * Restrict for internal use only.
+	 * @param objectPostProcessor an {@code ObjectPostProcessor}
+	 */
+	OAuth2PushedAuthorizationRequestEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
+		super(objectPostProcessor);
+	}
+
+	/**
+	 * Adds an {@link AuthenticationConverter} used when attempting to extract a Pushed
+	 * Authorization Request from {@link HttpServletRequest} to an instance of
+	 * {@link OAuth2PushedAuthorizationRequestAuthenticationToken} used for authenticating
+	 * the request.
+	 * @param pushedAuthorizationRequestConverter an {@link AuthenticationConverter} used
+	 * when attempting to extract a Pushed Authorization Request from
+	 * {@link HttpServletRequest}
+	 * @return the {@link OAuth2PushedAuthorizationRequestEndpointConfigurer} for further
+	 * configuration
+	 */
+	public OAuth2PushedAuthorizationRequestEndpointConfigurer pushedAuthorizationRequestConverter(
+			AuthenticationConverter pushedAuthorizationRequestConverter) {
+		Assert.notNull(pushedAuthorizationRequestConverter, "pushedAuthorizationRequestConverter cannot be null");
+		this.pushedAuthorizationRequestConverters.add(pushedAuthorizationRequestConverter);
+		return this;
+	}
+
+	/**
+	 * Sets the {@code Consumer} providing access to the {@code List} of default and
+	 * (optionally) added
+	 * {@link #pushedAuthorizationRequestConverter(AuthenticationConverter)
+	 * AuthenticationConverter}'s allowing the ability to add, remove, or customize a
+	 * specific {@link AuthenticationConverter}.
+	 * @param pushedAuthorizationRequestConvertersConsumer the {@code Consumer} providing
+	 * access to the {@code List} of default and (optionally) added
+	 * {@link AuthenticationConverter}'s
+	 * @return the {@link OAuth2PushedAuthorizationRequestEndpointConfigurer} for further
+	 * configuration
+	 */
+	public OAuth2PushedAuthorizationRequestEndpointConfigurer pushedAuthorizationRequestConverters(
+			Consumer<List<AuthenticationConverter>> pushedAuthorizationRequestConvertersConsumer) {
+		Assert.notNull(pushedAuthorizationRequestConvertersConsumer,
+				"pushedAuthorizationRequestConvertersConsumer cannot be null");
+		this.pushedAuthorizationRequestConvertersConsumer = pushedAuthorizationRequestConvertersConsumer;
+		return this;
+	}
+
+	/**
+	 * Adds an {@link AuthenticationProvider} used for authenticating an
+	 * {@link OAuth2PushedAuthorizationRequestAuthenticationToken}.
+	 * @param authenticationProvider an {@link AuthenticationProvider} used for
+	 * authenticating an {@link OAuth2PushedAuthorizationRequestAuthenticationToken}
+	 * @return the {@link OAuth2PushedAuthorizationRequestEndpointConfigurer} for further
+	 * configuration
+	 */
+	public OAuth2PushedAuthorizationRequestEndpointConfigurer authenticationProvider(
+			AuthenticationProvider authenticationProvider) {
+		Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
+		this.authenticationProviders.add(authenticationProvider);
+		return this;
+	}
+
+	/**
+	 * Sets the {@code Consumer} providing access to the {@code List} of default and
+	 * (optionally) added {@link #authenticationProvider(AuthenticationProvider)
+	 * AuthenticationProvider}'s allowing the ability to add, remove, or customize a
+	 * specific {@link AuthenticationProvider}.
+	 * @param authenticationProvidersConsumer the {@code Consumer} providing access to the
+	 * {@code List} of default and (optionally) added {@link AuthenticationProvider}'s
+	 * @return the {@link OAuth2PushedAuthorizationRequestEndpointConfigurer} for further
+	 * configuration
+	 */
+	public OAuth2PushedAuthorizationRequestEndpointConfigurer authenticationProviders(
+			Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer) {
+		Assert.notNull(authenticationProvidersConsumer, "authenticationProvidersConsumer cannot be null");
+		this.authenticationProvidersConsumer = authenticationProvidersConsumer;
+		return this;
+	}
+
+	/**
+	 * Sets the {@link AuthenticationSuccessHandler} used for handling an
+	 * {@link OAuth2PushedAuthorizationRequestAuthenticationToken} and returning the
+	 * Pushed Authorization Response.
+	 * @param pushedAuthorizationResponseHandler the {@link AuthenticationSuccessHandler}
+	 * used for handling an {@link OAuth2PushedAuthorizationRequestAuthenticationToken}
+	 * @return the {@link OAuth2PushedAuthorizationRequestEndpointConfigurer} for further
+	 * configuration
+	 */
+	public OAuth2PushedAuthorizationRequestEndpointConfigurer pushedAuthorizationResponseHandler(
+			AuthenticationSuccessHandler pushedAuthorizationResponseHandler) {
+		this.pushedAuthorizationResponseHandler = pushedAuthorizationResponseHandler;
+		return this;
+	}
+
+	/**
+	 * Sets the {@link AuthenticationFailureHandler} used for handling an
+	 * {@link OAuth2AuthorizationCodeRequestAuthenticationException} and returning the
+	 * {@link OAuth2Error Error Response}.
+	 * @param errorResponseHandler the {@link AuthenticationFailureHandler} used for
+	 * handling an {@link OAuth2AuthorizationCodeRequestAuthenticationException}
+	 * @return the {@link OAuth2PushedAuthorizationRequestEndpointConfigurer} for further
+	 * configuration
+	 */
+	public OAuth2PushedAuthorizationRequestEndpointConfigurer errorResponseHandler(
+			AuthenticationFailureHandler errorResponseHandler) {
+		this.errorResponseHandler = errorResponseHandler;
+		return this;
+	}
+
+	void addAuthorizationCodeRequestAuthenticationValidator(
+			Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator) {
+		this.authorizationCodeRequestAuthenticationValidator = (this.authorizationCodeRequestAuthenticationValidator == null)
+				? authenticationValidator
+				: this.authorizationCodeRequestAuthenticationValidator.andThen(authenticationValidator);
+	}
+
+	@Override
+	void init(HttpSecurity httpSecurity) {
+		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
+			.getAuthorizationServerSettings(httpSecurity);
+		String pushedAuthorizationRequestEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
+				? OAuth2ConfigurerUtils
+					.withMultipleIssuersPattern(authorizationServerSettings.getPushedAuthorizationRequestEndpoint())
+				: authorizationServerSettings.getPushedAuthorizationRequestEndpoint();
+		this.requestMatcher = new AntPathRequestMatcher(pushedAuthorizationRequestEndpointUri, HttpMethod.POST.name());
+		List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity);
+		if (!this.authenticationProviders.isEmpty()) {
+			authenticationProviders.addAll(0, this.authenticationProviders);
+		}
+		this.authenticationProvidersConsumer.accept(authenticationProviders);
+		authenticationProviders.forEach(
+				(authenticationProvider) -> httpSecurity.authenticationProvider(postProcess(authenticationProvider)));
+	}
+
+	@Override
+	void configure(HttpSecurity httpSecurity) {
+		AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class);
+		AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
+			.getAuthorizationServerSettings(httpSecurity);
+		String pushedAuthorizationRequestEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
+				? OAuth2ConfigurerUtils
+					.withMultipleIssuersPattern(authorizationServerSettings.getPushedAuthorizationRequestEndpoint())
+				: authorizationServerSettings.getPushedAuthorizationRequestEndpoint();
+		OAuth2PushedAuthorizationRequestEndpointFilter pushedAuthorizationRequestEndpointFilter = new OAuth2PushedAuthorizationRequestEndpointFilter(
+				authenticationManager, pushedAuthorizationRequestEndpointUri);
+		List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
+		if (!this.pushedAuthorizationRequestConverters.isEmpty()) {
+			authenticationConverters.addAll(0, this.pushedAuthorizationRequestConverters);
+		}
+		this.pushedAuthorizationRequestConvertersConsumer.accept(authenticationConverters);
+		pushedAuthorizationRequestEndpointFilter
+			.setAuthenticationConverter(new DelegatingAuthenticationConverter(authenticationConverters));
+		if (this.pushedAuthorizationResponseHandler != null) {
+			pushedAuthorizationRequestEndpointFilter
+				.setAuthenticationSuccessHandler(this.pushedAuthorizationResponseHandler);
+		}
+		if (this.errorResponseHandler != null) {
+			pushedAuthorizationRequestEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
+		}
+		httpSecurity.addFilterAfter(postProcess(pushedAuthorizationRequestEndpointFilter), AuthorizationFilter.class);
+	}
+
+	@Override
+	RequestMatcher getRequestMatcher() {
+		return this.requestMatcher;
+	}
+
+	private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
+		List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
+
+		authenticationConverters.add(new OAuth2AuthorizationCodeRequestAuthenticationConverter());
+
+		return authenticationConverters;
+	}
+
+	private List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
+		List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
+
+		OAuth2PushedAuthorizationRequestAuthenticationProvider pushedAuthorizationRequestAuthenticationProvider = new OAuth2PushedAuthorizationRequestAuthenticationProvider(
+				OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity));
+		if (this.authorizationCodeRequestAuthenticationValidator != null) {
+			pushedAuthorizationRequestAuthenticationProvider
+				.setAuthenticationValidator(new OAuth2AuthorizationCodeRequestAuthenticationValidator()
+					.andThen(this.authorizationCodeRequestAuthenticationValidator));
+		}
+		authenticationProviders.add(pushedAuthorizationRequestAuthenticationProvider);
+
+		return authenticationProviders;
+	}
+
+}

+ 24 - 1
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/AuthorizationServerSettings.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-2024 the original author or authors.
+ * Copyright 2020-2025 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.
@@ -72,6 +72,16 @@ public final class AuthorizationServerSettings extends AbstractSettings {
 		return getSetting(ConfigurationSettingNames.AuthorizationServer.AUTHORIZATION_ENDPOINT);
 	}
 
+	/**
+	 * Returns the OAuth 2.0 Pushed Authorization Request endpoint. The default is
+	 * {@code /oauth2/par}.
+	 * @return the Pushed Authorization Request endpoint
+	 * @since 1.5
+	 */
+	public String getPushedAuthorizationRequestEndpoint() {
+		return getSetting(ConfigurationSettingNames.AuthorizationServer.PUSHED_AUTHORIZATION_REQUEST_ENDPOINT);
+	}
+
 	/**
 	 * Returns the OAuth 2.0 Device Authorization endpoint. The default is
 	 * {@code /oauth2/device_authorization}.
@@ -160,6 +170,7 @@ public final class AuthorizationServerSettings extends AbstractSettings {
 	public static Builder builder() {
 		return new Builder().multipleIssuersAllowed(false)
 			.authorizationEndpoint("/oauth2/authorize")
+			.pushedAuthorizationRequestEndpoint("/oauth2/par")
 			.deviceAuthorizationEndpoint("/oauth2/device_authorization")
 			.deviceVerificationEndpoint("/oauth2/device_verification")
 			.tokenEndpoint("/oauth2/token")
@@ -236,6 +247,18 @@ public final class AuthorizationServerSettings extends AbstractSettings {
 			return setting(ConfigurationSettingNames.AuthorizationServer.AUTHORIZATION_ENDPOINT, authorizationEndpoint);
 		}
 
+		/**
+		 * Sets the OAuth 2.0 Pushed Authorization Request endpoint.
+		 * @param pushedAuthorizationRequestEndpoint the Pushed Authorization Request
+		 * endpoint
+		 * @return the {@link Builder} for further configuration
+		 * @since 1.5
+		 */
+		public Builder pushedAuthorizationRequestEndpoint(String pushedAuthorizationRequestEndpoint) {
+			return setting(ConfigurationSettingNames.AuthorizationServer.PUSHED_AUTHORIZATION_REQUEST_ENDPOINT,
+					pushedAuthorizationRequestEndpoint);
+		}
+
 		/**
 		 * Sets the OAuth 2.0 Device Authorization endpoint.
 		 * @param deviceAuthorizationEndpoint the Device Authorization endpoint

+ 8 - 1
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/ConfigurationSettingNames.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-2024 the original author or authors.
+ * Copyright 2020-2025 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.
@@ -112,6 +112,13 @@ public final class ConfigurationSettingNames {
 		public static final String AUTHORIZATION_ENDPOINT = AUTHORIZATION_SERVER_SETTINGS_NAMESPACE
 			.concat("authorization-endpoint");
 
+		/**
+		 * Set the OAuth 2.0 Pushed Authorization Request endpoint.
+		 * @since 1.5
+		 */
+		public static final String PUSHED_AUTHORIZATION_REQUEST_ENDPOINT = AUTHORIZATION_SERVER_SETTINGS_NAMESPACE
+			.concat("pushed-authorization-request-endpoint");
+
 		/**
 		 * Set the OAuth 2.0 Device Authorization endpoint.
 		 */

+ 63 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/HttpMessageConverters.java

@@ -0,0 +1,63 @@
+/*
+ * Copyright 2020-2025 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;
+
+import org.springframework.http.converter.GenericHttpMessageConverter;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.converter.json.GsonHttpMessageConverter;
+import org.springframework.http.converter.json.JsonbHttpMessageConverter;
+import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+import org.springframework.util.ClassUtils;
+
+/**
+ * Utility methods for {@link HttpMessageConverter}'s.
+ *
+ * @author Joe Grandja
+ * @since 1.5
+ */
+final class HttpMessageConverters {
+
+	private static final boolean jackson2Present;
+
+	private static final boolean gsonPresent;
+
+	private static final boolean jsonbPresent;
+
+	static {
+		ClassLoader classLoader = HttpMessageConverters.class.getClassLoader();
+		jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader)
+				&& ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
+		gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
+		jsonbPresent = ClassUtils.isPresent("jakarta.json.bind.Jsonb", classLoader);
+	}
+
+	private HttpMessageConverters() {
+	}
+
+	static GenericHttpMessageConverter<Object> getJsonMessageConverter() {
+		if (jackson2Present) {
+			return new MappingJackson2HttpMessageConverter();
+		}
+		if (gsonPresent) {
+			return new GsonHttpMessageConverter();
+		}
+		if (jsonbPresent) {
+			return new JsonbHttpMessageConverter();
+		}
+		return null;
+	}
+
+}

+ 223 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2PushedAuthorizationRequestEndpointFilter.java

@@ -0,0 +1,223 @@
+/*
+ * Copyright 2020-2025 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;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.core.log.LogMessage;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.converter.GenericHttpMessageConverter;
+import org.springframework.http.server.ServletServerHttpResponse;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.authentication.AuthenticationDetailsSource;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2PushedAuthorizationRequestAuthenticationProvider;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2PushedAuthorizationRequestAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeRequestAuthenticationConverter;
+import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ErrorAuthenticationFailureHandler;
+import org.springframework.security.web.authentication.AuthenticationConverter;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.util.Assert;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+/**
+ * A {@code Filter} for the OAuth 2.0 Pushed Authorization Request endpoint, which handles
+ * the processing of the OAuth 2.0 Pushed Authorization Request.
+ *
+ * @author Joe Grandja
+ * @since 1.5
+ * @see AuthenticationManager
+ * @see OAuth2PushedAuthorizationRequestAuthenticationProvider
+ * @see <a target="_blank" href=
+ * "https://datatracker.ietf.org/doc/html/rfc9126#name-pushed-authorization-reques">Section
+ * 2. Pushed Authorization Request Endpoint</a>
+ * @see <a target="_blank" href=
+ * "https://datatracker.ietf.org/doc/html/rfc9126#section-2.1">Section 2.1 Pushed
+ * Authorization Request</a>
+ * @see <a target="_blank" href=
+ * "https://datatracker.ietf.org/doc/html/rfc9126#section-2.2">Section 2.2 Pushed
+ * Authorization Response</a>
+ */
+public final class OAuth2PushedAuthorizationRequestEndpointFilter extends OncePerRequestFilter {
+
+	/**
+	 * The default endpoint {@code URI} for pushed authorization requests.
+	 */
+	private static final String DEFAULT_PUSHED_AUTHORIZATION_REQUEST_ENDPOINT_URI = "/oauth2/par";
+
+	private static final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP = new ParameterizedTypeReference<>() {
+	};
+
+	private static final GenericHttpMessageConverter<Object> JSON_MESSAGE_CONVERTER = HttpMessageConverters
+		.getJsonMessageConverter();
+
+	private final AuthenticationManager authenticationManager;
+
+	private final RequestMatcher pushedAuthorizationRequestEndpointMatcher;
+
+	private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
+
+	private AuthenticationConverter authenticationConverter;
+
+	private AuthenticationSuccessHandler authenticationSuccessHandler = this::sendPushedAuthorizationResponse;
+
+	private AuthenticationFailureHandler authenticationFailureHandler = new OAuth2ErrorAuthenticationFailureHandler();
+
+	/**
+	 * Constructs an {@code OAuth2PushedAuthorizationRequestEndpointFilter} using the
+	 * provided parameters.
+	 * @param authenticationManager the authentication manager
+	 */
+	public OAuth2PushedAuthorizationRequestEndpointFilter(AuthenticationManager authenticationManager) {
+		this(authenticationManager, DEFAULT_PUSHED_AUTHORIZATION_REQUEST_ENDPOINT_URI);
+	}
+
+	/**
+	 * Constructs an {@code OAuth2PushedAuthorizationRequestEndpointFilter} using the
+	 * provided parameters.
+	 * @param authenticationManager the authentication manager
+	 * @param pushedAuthorizationRequestEndpointUri the endpoint {@code URI} for pushed
+	 * authorization requests
+	 */
+	public OAuth2PushedAuthorizationRequestEndpointFilter(AuthenticationManager authenticationManager,
+			String pushedAuthorizationRequestEndpointUri) {
+		Assert.notNull(authenticationManager, "authenticationManager cannot be null");
+		Assert.hasText(pushedAuthorizationRequestEndpointUri, "pushedAuthorizationRequestEndpointUri cannot be empty");
+		this.authenticationManager = authenticationManager;
+		this.pushedAuthorizationRequestEndpointMatcher = new AntPathRequestMatcher(
+				pushedAuthorizationRequestEndpointUri, HttpMethod.POST.name());
+		this.authenticationConverter = new OAuth2AuthorizationCodeRequestAuthenticationConverter();
+	}
+
+	@Override
+	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+			throws ServletException, IOException {
+
+		if (!this.pushedAuthorizationRequestEndpointMatcher.matches(request)) {
+			filterChain.doFilter(request, response);
+			return;
+		}
+
+		try {
+			Authentication pushedAuthorizationRequestAuthentication = this.authenticationConverter.convert(request);
+			if (pushedAuthorizationRequestAuthentication instanceof AbstractAuthenticationToken) {
+				((AbstractAuthenticationToken) pushedAuthorizationRequestAuthentication)
+					.setDetails(this.authenticationDetailsSource.buildDetails(request));
+			}
+			Authentication pushedAuthorizationRequestAuthenticationResult = this.authenticationManager
+				.authenticate(pushedAuthorizationRequestAuthentication);
+
+			this.authenticationSuccessHandler.onAuthenticationSuccess(request, response,
+					pushedAuthorizationRequestAuthenticationResult);
+
+		}
+		catch (OAuth2AuthenticationException ex) {
+			if (this.logger.isTraceEnabled()) {
+				this.logger.trace(LogMessage.format("Pushed authorization request failed: %s", ex.getError()), ex);
+			}
+			this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
+		}
+	}
+
+	/**
+	 * Sets the {@link AuthenticationDetailsSource} used for building an authentication
+	 * details instance from {@link HttpServletRequest}.
+	 * @param authenticationDetailsSource the {@link AuthenticationDetailsSource} used for
+	 * building an authentication details instance from {@link HttpServletRequest}
+	 */
+	public void setAuthenticationDetailsSource(
+			AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
+		Assert.notNull(authenticationDetailsSource, "authenticationDetailsSource cannot be null");
+		this.authenticationDetailsSource = authenticationDetailsSource;
+	}
+
+	/**
+	 * Sets the {@link AuthenticationConverter} used when attempting to extract a Pushed
+	 * Authorization Request from {@link HttpServletRequest} to an instance of
+	 * {@link OAuth2PushedAuthorizationRequestAuthenticationToken} used for authenticating
+	 * the request.
+	 * @param authenticationConverter the {@link AuthenticationConverter} used when
+	 * attempting to extract a Pushed Authorization Request from
+	 * {@link HttpServletRequest}
+	 */
+	public void setAuthenticationConverter(AuthenticationConverter authenticationConverter) {
+		Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
+		this.authenticationConverter = authenticationConverter;
+	}
+
+	/**
+	 * Sets the {@link AuthenticationSuccessHandler} used for handling an
+	 * {@link OAuth2PushedAuthorizationRequestAuthenticationToken} and returning the
+	 * Pushed Authorization Response.
+	 * @param authenticationSuccessHandler the {@link AuthenticationSuccessHandler} used
+	 * for handling an {@link OAuth2PushedAuthorizationRequestAuthenticationToken}
+	 */
+	public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler authenticationSuccessHandler) {
+		Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null");
+		this.authenticationSuccessHandler = authenticationSuccessHandler;
+	}
+
+	/**
+	 * Sets the {@link AuthenticationFailureHandler} used for handling an
+	 * {@link OAuth2AuthenticationException} and returning the {@link OAuth2Error Error
+	 * Response}.
+	 * @param authenticationFailureHandler the {@link AuthenticationFailureHandler} used
+	 * for handling an {@link OAuth2AuthenticationException}
+	 */
+	public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
+		Assert.notNull(authenticationFailureHandler, "authenticationFailureHandler cannot be null");
+		this.authenticationFailureHandler = authenticationFailureHandler;
+	}
+
+	private void sendPushedAuthorizationResponse(HttpServletRequest request, HttpServletResponse response,
+			Authentication authentication) throws IOException {
+
+		OAuth2PushedAuthorizationRequestAuthenticationToken pushedAuthorizationRequestAuthentication = (OAuth2PushedAuthorizationRequestAuthenticationToken) authentication;
+
+		Map<String, Object> pushedAuthorizationResponse = new LinkedHashMap<>();
+		pushedAuthorizationResponse.put("request_uri", pushedAuthorizationRequestAuthentication.getRequestUri());
+		long expiresIn = ChronoUnit.SECONDS.between(Instant.now(),
+				pushedAuthorizationRequestAuthentication.getRequestUriExpiresAt());
+		pushedAuthorizationResponse.put(OAuth2ParameterNames.EXPIRES_IN, expiresIn);
+
+		ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
+		httpResponse.setStatusCode(HttpStatus.CREATED);
+
+		JSON_MESSAGE_CONVERTER.write(pushedAuthorizationResponse, STRING_OBJECT_MAP.getType(),
+				MediaType.APPLICATION_JSON, httpResponse);
+	}
+
+}

+ 51 - 9
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2AuthorizationCodeRequestAuthenticationConverter.java

@@ -18,6 +18,7 @@ package org.springframework.security.oauth2.server.authorization.web.authenticat
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 
@@ -35,7 +36,12 @@ import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
 import org.springframework.security.oauth2.core.oidc.OidcScopes;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2PushedAuthorizationRequestAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContext;
+import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
+import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
 import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter;
+import org.springframework.security.oauth2.server.authorization.web.OAuth2PushedAuthorizationRequestEndpointFilter;
 import org.springframework.security.web.authentication.AuthenticationConverter;
 import org.springframework.security.web.util.matcher.AndRequestMatcher;
 import org.springframework.security.web.util.matcher.OrRequestMatcher;
@@ -47,14 +53,17 @@ import org.springframework.util.StringUtils;
 /**
  * Attempts to extract an Authorization Request from {@link HttpServletRequest} for the
  * OAuth 2.0 Authorization Code Grant and then converts it to an
- * {@link OAuth2AuthorizationCodeRequestAuthenticationToken} used for authenticating the
+ * {@link OAuth2AuthorizationCodeRequestAuthenticationToken} OR
+ * {@link OAuth2PushedAuthorizationRequestAuthenticationToken} used for authenticating the
  * request.
  *
  * @author Joe Grandja
  * @since 0.1.2
  * @see AuthenticationConverter
  * @see OAuth2AuthorizationCodeRequestAuthenticationToken
+ * @see OAuth2PushedAuthorizationRequestAuthenticationToken
  * @see OAuth2AuthorizationEndpointFilter
+ * @see OAuth2PushedAuthorizationRequestEndpointFilter
  */
 public final class OAuth2AuthorizationCodeRequestAuthenticationConverter implements AuthenticationConverter {
 
@@ -76,13 +85,30 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationConverter impleme
 		MultiValueMap<String, String> parameters = "GET".equals(request.getMethod())
 				? OAuth2EndpointUtils.getQueryParameters(request) : OAuth2EndpointUtils.getFormParameters(request);
 
-		// response_type (REQUIRED)
-		String responseType = parameters.getFirst(OAuth2ParameterNames.RESPONSE_TYPE);
-		if (!StringUtils.hasText(responseType) || parameters.get(OAuth2ParameterNames.RESPONSE_TYPE).size() != 1) {
-			throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.RESPONSE_TYPE);
+		boolean pushedAuthorizationRequest = isPushedAuthorizationRequest(request);
+
+		// request_uri (OPTIONAL) - provided if an authorization request was previously
+		// pushed (RFC 9126 OAuth 2.0 Pushed Authorization Requests)
+		String requestUri = parameters.getFirst("request_uri");
+		if (StringUtils.hasText(requestUri)) {
+			if (pushedAuthorizationRequest) {
+				throwError(OAuth2ErrorCodes.INVALID_REQUEST, "request_uri");
+			}
+			else if (parameters.get("request_uri").size() != 1) {
+				// Authorization Request
+				throwError(OAuth2ErrorCodes.INVALID_REQUEST, "request_uri");
+			}
 		}
-		else if (!responseType.equals(OAuth2AuthorizationResponseType.CODE.getValue())) {
-			throwError(OAuth2ErrorCodes.UNSUPPORTED_RESPONSE_TYPE, OAuth2ParameterNames.RESPONSE_TYPE);
+
+		if (!StringUtils.hasText(requestUri)) {
+			// response_type (REQUIRED)
+			String responseType = parameters.getFirst(OAuth2ParameterNames.RESPONSE_TYPE);
+			if (!StringUtils.hasText(responseType) || parameters.get(OAuth2ParameterNames.RESPONSE_TYPE).size() != 1) {
+				throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.RESPONSE_TYPE);
+			}
+			else if (!responseType.equals(OAuth2AuthorizationResponseType.CODE.getValue())) {
+				throwError(OAuth2ErrorCodes.UNSUPPORTED_RESPONSE_TYPE, OAuth2ParameterNames.RESPONSE_TYPE);
+			}
 		}
 
 		String authorizationUri = request.getRequestURL().toString();
@@ -150,8 +176,24 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationConverter impleme
 			}
 		});
 
-		return new OAuth2AuthorizationCodeRequestAuthenticationToken(authorizationUri, clientId, principal, redirectUri,
-				state, scopes, additionalParameters);
+		if (pushedAuthorizationRequest) {
+			return new OAuth2PushedAuthorizationRequestAuthenticationToken(authorizationUri, clientId, principal,
+					redirectUri, state, scopes, additionalParameters);
+		}
+		else {
+			return new OAuth2AuthorizationCodeRequestAuthenticationToken(authorizationUri, clientId, principal,
+					redirectUri, state, scopes, additionalParameters);
+		}
+	}
+
+	private boolean isPushedAuthorizationRequest(HttpServletRequest request) {
+		AuthorizationServerContext authorizationServerContext = AuthorizationServerContextHolder.getContext();
+		AuthorizationServerSettings authorizationServerSettings = authorizationServerContext
+			.getAuthorizationServerSettings();
+		return request.getRequestURL()
+			.toString()
+			.toLowerCase(Locale.ROOT)
+			.endsWith(authorizationServerSettings.getPushedAuthorizationRequestEndpoint().toLowerCase(Locale.ROOT));
 	}
 
 	private static RequestMatcher createDefaultRequestMatcher() {

+ 67 - 5
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProviderTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-2024 the original author or authors.
+ * Copyright 2020-2025 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.
@@ -42,6 +42,8 @@ import org.springframework.security.oauth2.server.authorization.OAuth2Authorizat
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
+import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
 import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
 import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
 import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
@@ -50,6 +52,7 @@ import org.springframework.security.oauth2.server.authorization.context.TestAuth
 import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
 import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
 import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
+import org.springframework.util.StringUtils;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -71,6 +74,8 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
 
 	private static final String STATE = "state";
 
+	private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
+
 	private RegisteredClientRepository registeredClientRepository;
 
 	private OAuth2AuthorizationService authorizationService;
@@ -602,6 +607,59 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
 				authenticationResult);
 	}
 
+	@Test
+	public void authenticateWhenAuthorizationCodeRequestWithRequestUriThenReturnAuthorizationCode() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		given(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+			.willReturn(registeredClient);
+
+		OAuth2PushedAuthorizationRequestUri pushedAuthorizationRequestUri = OAuth2PushedAuthorizationRequestUri
+			.create();
+		Map<String, Object> additionalParameters = new HashMap<>();
+		additionalParameters.put("request_uri", pushedAuthorizationRequestUri.getRequestUri());
+		OAuth2Authorization authorization = TestOAuth2Authorizations
+			.authorization(registeredClient, additionalParameters)
+			.build();
+		given(this.authorizationService.findByToken(eq(pushedAuthorizationRequestUri.getState()), eq(STATE_TOKEN_TYPE)))
+			.willReturn(authorization);
+
+		OAuth2AuthorizationCodeRequestAuthenticationToken authentication = new OAuth2AuthorizationCodeRequestAuthenticationToken(
+				AUTHORIZATION_URI, registeredClient.getClientId(), this.principal, null, null, null,
+				additionalParameters);
+
+		OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult = (OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider
+			.authenticate(authentication);
+
+		assertAuthorizationCodeRequestWithAuthorizationCodeResult(registeredClient, authentication,
+				authenticationResult);
+	}
+
+	@Test
+	public void authenticateWhenAuthorizationCodeRequestWithInvalidRequestUriThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		given(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+			.willReturn(registeredClient);
+
+		OAuth2PushedAuthorizationRequestUri pushedAuthorizationRequestUri = OAuth2PushedAuthorizationRequestUri
+			.create();
+		Map<String, Object> additionalParameters = new HashMap<>();
+		additionalParameters.put("request_uri", pushedAuthorizationRequestUri.getRequestUri());
+		OAuth2Authorization authorization = TestOAuth2Authorizations
+			.authorization(registeredClient, additionalParameters)
+			.build();
+		given(this.authorizationService.findByToken(eq(pushedAuthorizationRequestUri.getState()), eq(STATE_TOKEN_TYPE)))
+			.willReturn(authorization);
+
+		OAuth2AuthorizationCodeRequestAuthenticationToken authentication = new OAuth2AuthorizationCodeRequestAuthenticationToken(
+				AUTHORIZATION_URI, registeredClient.getClientId(), this.principal, null, null, null,
+				Collections.singletonMap("request_uri", "invalid_request_uri"));
+
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+			.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
+			.satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
+					OAuth2ErrorCodes.INVALID_REQUEST, "request_uri", null));
+	}
+
 	@Test
 	public void authenticateWhenAuthorizationCodeNotGeneratedThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
 		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
@@ -665,11 +723,15 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
 		assertThat(authorizationRequest.getResponseType()).isEqualTo(OAuth2AuthorizationResponseType.CODE);
 		assertThat(authorizationRequest.getAuthorizationUri()).isEqualTo(authentication.getAuthorizationUri());
 		assertThat(authorizationRequest.getClientId()).isEqualTo(registeredClient.getClientId());
-		assertThat(authorizationRequest.getRedirectUri()).isEqualTo(authentication.getRedirectUri());
-		assertThat(authorizationRequest.getScopes()).isEqualTo(authentication.getScopes());
-		assertThat(authorizationRequest.getState()).isEqualTo(authentication.getState());
-		assertThat(authorizationRequest.getAdditionalParameters()).isEqualTo(authentication.getAdditionalParameters());
 
+		String requestUri = (String) authentication.getAdditionalParameters().get("request_uri");
+		if (!StringUtils.hasText(requestUri)) {
+			assertThat(authorizationRequest.getRedirectUri()).isEqualTo(authentication.getRedirectUri());
+			assertThat(authorizationRequest.getScopes()).isEqualTo(authentication.getScopes());
+			assertThat(authorizationRequest.getState()).isEqualTo(authentication.getState());
+		}
+
+		assertThat(authorizationRequest.getAdditionalParameters()).isEqualTo(authentication.getAdditionalParameters());
 		assertThat(authorization.getRegisteredClientId()).isEqualTo(registeredClient.getId());
 		assertThat(authorization.getPrincipalName()).isEqualTo(this.principal.getName());
 		assertThat(authorization.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);

+ 422 - 0
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2PushedAuthorizationRequestAuthenticationProviderTests.java

@@ -0,0 +1,422 @@
+/*
+ * Copyright 2020-2025 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.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Consumer;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+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.OAuth2AuthorizationRequest;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
+import org.springframework.security.oauth2.core.oidc.OidcScopes;
+import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
+import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Tests for {@link OAuth2PushedAuthorizationRequestAuthenticationProvider}.
+ *
+ * @author Joe Grandja
+ */
+public class OAuth2PushedAuthorizationRequestAuthenticationProviderTests {
+
+	private static final String AUTHORIZATION_URI = "https://provider.com/oauth2/par";
+
+	private static final String STATE = "state";
+
+	private OAuth2AuthorizationService authorizationService;
+
+	private OAuth2PushedAuthorizationRequestAuthenticationProvider authenticationProvider;
+
+	@BeforeEach
+	public void setUp() {
+		this.authorizationService = mock(OAuth2AuthorizationService.class);
+		this.authenticationProvider = new OAuth2PushedAuthorizationRequestAuthenticationProvider(
+				this.authorizationService);
+	}
+
+	@Test
+	public void constructorWhenAuthorizationServiceNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> new OAuth2PushedAuthorizationRequestAuthenticationProvider(null))
+			.isInstanceOf(IllegalArgumentException.class)
+			.hasMessage("authorizationService cannot be null");
+	}
+
+	@Test
+	public void supportsWhenTypeOAuth2PushedAuthorizationRequestAuthenticationTokenThenReturnTrue() {
+		assertThat(this.authenticationProvider.supports(OAuth2PushedAuthorizationRequestAuthenticationToken.class))
+			.isTrue();
+	}
+
+	@Test
+	public void setAuthenticationValidatorWhenNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> this.authenticationProvider.setAuthenticationValidator(null))
+			.isInstanceOf(IllegalArgumentException.class)
+			.hasMessage("authenticationValidator cannot be null");
+	}
+
+	@Test
+	public void authenticateWhenClientNotAuthenticatedThenThrowOAuth2AuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		String redirectUri = registeredClient.getRedirectUris().toArray(new String[0])[1];
+		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(
+				registeredClient.getClientId(), ClientAuthenticationMethod.CLIENT_SECRET_BASIC, null, null);
+		OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken(
+				AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, redirectUri, STATE,
+				registeredClient.getScopes(), null);
+		// @formatter:off
+		assertThatExceptionOfType(OAuth2AuthenticationException.class)
+				.isThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+				.extracting(OAuth2AuthenticationException::getError)
+				.extracting(OAuth2Error::getErrorCode)
+				.isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
+		// @formatter:on
+	}
+
+	@Test
+	public void authenticateWhenClientNotAuthorizedToRequestCodeThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+			.authorizationGrantTypes(Set::clear)
+			.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
+			.build();
+		String redirectUri = registeredClient.getRedirectUris().toArray(new String[0])[1];
+		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
+				ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
+		OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken(
+				AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, redirectUri, null,
+				registeredClient.getScopes(), null);
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+			.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
+			.satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
+					OAuth2ErrorCodes.UNAUTHORIZED_CLIENT, OAuth2ParameterNames.CLIENT_ID,
+					authentication.getRedirectUri()));
+	}
+
+	@Test
+	public void authenticateWhenInvalidRedirectUriHostThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
+				ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
+		OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken(
+				AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, "https:///invalid", STATE,
+				registeredClient.getScopes(), null);
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+			.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
+			.satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
+					OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, null));
+	}
+
+	@Test
+	public void authenticateWhenInvalidRedirectUriFragmentThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
+				ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
+		OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken(
+				AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, "https://example.com#fragment",
+				STATE, registeredClient.getScopes(), null);
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+			.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
+			.satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
+					OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, null));
+	}
+
+	@Test
+	public void authenticateWhenUnregisteredRedirectUriThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
+				ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
+		OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken(
+				AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, "https://invalid-example.com",
+				STATE, registeredClient.getScopes(), null);
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+			.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
+			.satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
+					OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, null));
+	}
+
+	@Test
+	public void authenticateWhenRedirectUriIPv4LoopbackAndDifferentPortThenReturnPushedAuthorizationResponse() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+			.redirectUri("https://127.0.0.1:8080")
+			.build();
+		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
+				ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
+		OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken(
+				AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, "https://127.0.0.1:5000", STATE,
+				registeredClient.getScopes(), null);
+		OAuth2PushedAuthorizationRequestAuthenticationToken authenticationResult = (OAuth2PushedAuthorizationRequestAuthenticationToken) this.authenticationProvider
+			.authenticate(authentication);
+		assertPushedAuthorizationResponse(registeredClient, authentication, authenticationResult);
+	}
+
+	@Test
+	public void authenticateWhenRedirectUriIPv6LoopbackAndDifferentPortThenReturnPushedAuthorizationResponse() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+			.redirectUri("https://[::1]:8080")
+			.build();
+		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
+				ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
+		OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken(
+				AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, "https://[::1]:5000", STATE,
+				registeredClient.getScopes(), null);
+		OAuth2PushedAuthorizationRequestAuthenticationToken authenticationResult = (OAuth2PushedAuthorizationRequestAuthenticationToken) this.authenticationProvider
+			.authenticate(authentication);
+		assertPushedAuthorizationResponse(registeredClient, authentication, authenticationResult);
+	}
+
+	@Test
+	public void authenticateWhenMissingRedirectUriAndMultipleRegisteredThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+			.redirectUri("https://example2.com")
+			.build();
+		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
+				ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
+		OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken(
+				AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, null, STATE,
+				registeredClient.getScopes(), null);
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+			.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
+			.satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
+					OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, null));
+	}
+
+	@Test
+	public void authenticateWhenAuthenticationRequestMissingRedirectUriThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
+		// redirect_uri is REQUIRED for OpenID Connect requests
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scope(OidcScopes.OPENID).build();
+		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
+				ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
+		OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken(
+				AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, null, STATE,
+				registeredClient.getScopes(), null);
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+			.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
+			.satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
+					OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, null));
+	}
+
+	@Test
+	public void authenticateWhenInvalidScopeThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
+				ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
+		String redirectUri = registeredClient.getRedirectUris().toArray(new String[0])[2];
+		OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken(
+				AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, redirectUri, STATE,
+				Collections.singleton("invalid-scope"), null);
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+			.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
+			.satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
+					OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE, authentication.getRedirectUri()));
+	}
+
+	@Test
+	public void authenticateWhenPkceRequiredAndMissingCodeChallengeThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+			.clientSettings(ClientSettings.builder().requireProofKey(true).build())
+			.build();
+		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
+				ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
+		String redirectUri = registeredClient.getRedirectUris().toArray(new String[0])[2];
+		OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken(
+				AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, redirectUri, STATE,
+				registeredClient.getScopes(), null);
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+			.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
+			.satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
+					OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE,
+					authentication.getRedirectUri()));
+	}
+
+	@Test
+	public void authenticateWhenPkceUnsupportedCodeChallengeMethodThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
+				ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
+		String redirectUri = registeredClient.getRedirectUris().toArray(new String[0])[2];
+		Map<String, Object> additionalParameters = new HashMap<>();
+		additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, "code-challenge");
+		additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "unsupported");
+		OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken(
+				AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, redirectUri, STATE,
+				registeredClient.getScopes(), additionalParameters);
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+			.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
+			.satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
+					OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD,
+					authentication.getRedirectUri()));
+	}
+
+	@Test
+	public void authenticateWhenPkceMissingCodeChallengeMethodThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
+				ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
+		String redirectUri = registeredClient.getRedirectUris().toArray(new String[0])[2];
+		Map<String, Object> additionalParameters = new HashMap<>();
+		additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, "code-challenge");
+		OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken(
+				AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, redirectUri, STATE,
+				registeredClient.getScopes(), additionalParameters);
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+			.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
+			.satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
+					OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD,
+					authentication.getRedirectUri()));
+	}
+
+	@Test
+	public void authenticateWhenAuthenticationRequestWithPromptNoneLoginThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
+		assertWhenAuthenticationRequestWithInvalidPromptThenThrowOAuth2AuthorizationCodeRequestAuthenticationException(
+				"none login");
+	}
+
+	@Test
+	public void authenticateWhenAuthenticationRequestWithPromptNoneConsentThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
+		assertWhenAuthenticationRequestWithInvalidPromptThenThrowOAuth2AuthorizationCodeRequestAuthenticationException(
+				"none consent");
+	}
+
+	@Test
+	public void authenticateWhenAuthenticationRequestWithPromptNoneSelectAccountThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
+		assertWhenAuthenticationRequestWithInvalidPromptThenThrowOAuth2AuthorizationCodeRequestAuthenticationException(
+				"none select_account");
+	}
+
+	private void assertWhenAuthenticationRequestWithInvalidPromptThenThrowOAuth2AuthorizationCodeRequestAuthenticationException(
+			String prompt) {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scope(OidcScopes.OPENID).build();
+		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
+				ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
+		String redirectUri = registeredClient.getRedirectUris().toArray(new String[0])[2];
+		Map<String, Object> additionalParameters = new HashMap<>();
+		additionalParameters.put("prompt", prompt);
+		OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken(
+				AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, redirectUri, STATE,
+				registeredClient.getScopes(), additionalParameters);
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+			.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
+			.satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
+					OAuth2ErrorCodes.INVALID_REQUEST, "prompt", authentication.getRedirectUri()));
+	}
+
+	@Test
+	public void authenticateWhenPushedAuthorizationRequestValidThenReturnPushedAuthorizationResponse() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
+				ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
+		String redirectUri = registeredClient.getRedirectUris().toArray(new String[0])[0];
+		Map<String, Object> additionalParameters = new HashMap<>();
+		additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, "code-challenge");
+		additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
+		OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken(
+				AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, redirectUri, STATE,
+				registeredClient.getScopes(), additionalParameters);
+		OAuth2PushedAuthorizationRequestAuthenticationToken authenticationResult = (OAuth2PushedAuthorizationRequestAuthenticationToken) this.authenticationProvider
+			.authenticate(authentication);
+		assertPushedAuthorizationResponse(registeredClient, authentication, authenticationResult);
+	}
+
+	@Test
+	public void authenticateWhenCustomAuthenticationValidatorThenUsed() {
+		@SuppressWarnings("unchecked")
+		Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator = mock(Consumer.class);
+		this.authenticationProvider.setAuthenticationValidator(authenticationValidator);
+
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
+				ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
+		String redirectUri = registeredClient.getRedirectUris().toArray(new String[0])[2];
+		OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken(
+				AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, redirectUri, STATE,
+				registeredClient.getScopes(), null);
+		OAuth2PushedAuthorizationRequestAuthenticationToken authenticationResult = (OAuth2PushedAuthorizationRequestAuthenticationToken) this.authenticationProvider
+			.authenticate(authentication);
+		assertPushedAuthorizationResponse(registeredClient, authentication, authenticationResult);
+		verify(authenticationValidator).accept(any());
+	}
+
+	private void assertPushedAuthorizationResponse(RegisteredClient registeredClient,
+			OAuth2PushedAuthorizationRequestAuthenticationToken authentication,
+			OAuth2PushedAuthorizationRequestAuthenticationToken authenticationResult) {
+
+		ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
+		verify(this.authorizationService).save(authorizationCaptor.capture());
+		OAuth2Authorization authorization = authorizationCaptor.getValue();
+
+		OAuth2AuthorizationRequest authorizationRequest = authorization
+			.getAttribute(OAuth2AuthorizationRequest.class.getName());
+		assertThat(authorizationRequest.getGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
+		assertThat(authorizationRequest.getResponseType()).isEqualTo(OAuth2AuthorizationResponseType.CODE);
+		assertThat(authorizationRequest.getAuthorizationUri()).isEqualTo(authentication.getAuthorizationUri());
+		assertThat(authorizationRequest.getClientId()).isEqualTo(registeredClient.getClientId());
+		assertThat(authorizationRequest.getRedirectUri()).isEqualTo(authentication.getRedirectUri());
+		assertThat(authorizationRequest.getScopes()).isEqualTo(authentication.getScopes());
+		assertThat(authorizationRequest.getState()).isEqualTo(authentication.getState());
+		assertThat(authorizationRequest.getAdditionalParameters()).isEqualTo(authentication.getAdditionalParameters());
+
+		assertThat(authorization.getRegisteredClientId()).isEqualTo(registeredClient.getId());
+		assertThat(authorization.getPrincipalName()).isEqualTo(authentication.getName());
+		assertThat(authorization.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
+		assertThat(authorization.<String>getAttribute(OAuth2ParameterNames.STATE)).isNotNull();
+
+		assertThat(authenticationResult.getClientId()).isEqualTo(authorizationRequest.getClientId());
+		assertThat(authenticationResult.getPrincipal()).isEqualTo(authentication.getPrincipal());
+		assertThat(authenticationResult.getAuthorizationUri()).isEqualTo(authorizationRequest.getAuthorizationUri());
+		assertThat(authenticationResult.getRedirectUri()).isEqualTo(authorizationRequest.getRedirectUri());
+		assertThat(authenticationResult.getScopes()).isEqualTo(authorizationRequest.getScopes());
+		assertThat(authenticationResult.getState()).isEqualTo(authorizationRequest.getState());
+		assertThat(authenticationResult.getRequestUri()).isNotNull();
+		assertThat(authenticationResult.getRequestUriExpiresAt()).isNotNull();
+		assertThat(authenticationResult.isAuthenticated()).isTrue();
+	}
+
+	private static void assertAuthenticationException(
+			OAuth2AuthorizationCodeRequestAuthenticationException authenticationException, String errorCode,
+			String parameterName, String redirectUri) {
+
+		OAuth2Error error = authenticationException.getError();
+		assertThat(error.getErrorCode()).isEqualTo(errorCode);
+		assertThat(error.getDescription()).contains(parameterName);
+
+		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = authenticationException
+			.getAuthorizationCodeRequestAuthentication();
+		assertThat(authorizationCodeRequestAuthentication.getRedirectUri()).isEqualTo(redirectUri);
+	}
+
+}

+ 87 - 0
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationCodeGrantTests.java

@@ -32,6 +32,7 @@ import java.util.Set;
 import java.util.UUID;
 import java.util.function.Consumer;
 
+import com.jayway.jsonpath.JsonPath;
 import com.nimbusds.jose.jwk.JWKSet;
 import com.nimbusds.jose.jwk.source.JWKSource;
 import com.nimbusds.jose.proc.SecurityContext;
@@ -1012,6 +1013,67 @@ public class OAuth2AuthorizationCodeGrantTests {
 		assertThat(cnfClaims).containsKey("jkt");
 	}
 
+	@Test
+	public void requestWhenPushedAuthorizationRequestThenReturnAccessTokenResponse() throws Exception {
+		this.spring.register(AuthorizationServerConfigurationWithPushedAuthorizationRequests.class).autowire();
+
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		this.registeredClientRepository.save(registeredClient);
+
+		MvcResult mvcResult = this.mvc
+			.perform(post("/oauth2/par").params(getAuthorizationRequestParameters(registeredClient))
+				.param(PkceParameterNames.CODE_CHALLENGE, S256_CODE_CHALLENGE)
+				.param(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256")
+				.header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(registeredClient)))
+			.andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store")))
+			.andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache")))
+			.andExpect(status().isCreated())
+			.andExpect(jsonPath("$.request_uri").isNotEmpty())
+			.andExpect(jsonPath("$.expires_in").isNotEmpty())
+			.andReturn();
+
+		String requestUri = JsonPath.read(mvcResult.getResponse().getContentAsString(), "$.request_uri");
+
+		mvcResult = this.mvc
+			.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI)
+				.queryParam(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())
+				.queryParam("request_uri", requestUri)
+				.with(user("user")))
+			.andExpect(status().is3xxRedirection())
+			.andReturn();
+
+		String authorizationCode = extractParameterFromRedirectUri(mvcResult.getResponse().getRedirectedUrl(), "code");
+		OAuth2Authorization authorizationCodeAuthorization = this.authorizationService.findByToken(authorizationCode,
+				AUTHORIZATION_CODE_TOKEN_TYPE);
+
+		this.mvc
+			.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
+				.params(getTokenRequestParameters(registeredClient, authorizationCodeAuthorization))
+				.param(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())
+				.param(PkceParameterNames.CODE_VERIFIER, S256_CODE_VERIFIER)
+				.header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(registeredClient)))
+			.andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store")))
+			.andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache")))
+			.andExpect(status().isOk())
+			.andExpect(jsonPath("$.access_token").isNotEmpty())
+			.andExpect(jsonPath("$.token_type").isNotEmpty())
+			.andExpect(jsonPath("$.expires_in").isNotEmpty())
+			.andExpect(jsonPath("$.refresh_token").isNotEmpty())
+			.andExpect(jsonPath("$.scope").isNotEmpty())
+			.andReturn();
+
+		OAuth2Authorization accessTokenAuthorization = this.authorizationService
+			.findById(authorizationCodeAuthorization.getId());
+		assertThat(accessTokenAuthorization).isNotNull();
+		assertThat(accessTokenAuthorization.getAccessToken()).isNotNull();
+
+		OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCodeToken = accessTokenAuthorization
+			.getToken(OAuth2AuthorizationCode.class);
+		assertThat(authorizationCodeToken).isNotNull();
+		assertThat(authorizationCodeToken.getMetadata().get(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME))
+			.isEqualTo(true);
+	}
+
 	private static String generateDPoPProof(String tokenEndpointUri) {
 		// @formatter:off
 		Map<String, Object> publicJwk = TestJwks.DEFAULT_EC_JWK
@@ -1417,4 +1479,29 @@ public class OAuth2AuthorizationCodeGrantTests {
 
 	}
 
+	@EnableWebSecurity
+	@Configuration(proxyBeanMethods = false)
+	static class AuthorizationServerConfigurationWithPushedAuthorizationRequests
+			extends AuthorizationServerConfiguration {
+
+		// @formatter:off
+		@Bean
+		SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
+			OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
+					OAuth2AuthorizationServerConfigurer.authorizationServer();
+			http
+					.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
+					.with(authorizationServerConfigurer, (authorizationServer) ->
+							authorizationServer
+									.pushedAuthorizationRequestEndpoint(Customizer.withDefaults())
+					)
+					.authorizeHttpRequests((authorize) ->
+							authorize.anyRequest().authenticated()
+					);
+			return http.build();
+		}
+		// @formatter:on
+
+	}
+
 }

+ 15 - 2
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/settings/AuthorizationServerSettingsTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-2024 the original author or authors.
+ * Copyright 2020-2025 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.
@@ -35,6 +35,7 @@ public class AuthorizationServerSettingsTests {
 		assertThat(authorizationServerSettings.getIssuer()).isNull();
 		assertThat(authorizationServerSettings.isMultipleIssuersAllowed()).isFalse();
 		assertThat(authorizationServerSettings.getAuthorizationEndpoint()).isEqualTo("/oauth2/authorize");
+		assertThat(authorizationServerSettings.getPushedAuthorizationRequestEndpoint()).isEqualTo("/oauth2/par");
 		assertThat(authorizationServerSettings.getTokenEndpoint()).isEqualTo("/oauth2/token");
 		assertThat(authorizationServerSettings.getJwkSetEndpoint()).isEqualTo("/oauth2/jwks");
 		assertThat(authorizationServerSettings.getTokenRevocationEndpoint()).isEqualTo("/oauth2/revoke");
@@ -47,6 +48,7 @@ public class AuthorizationServerSettingsTests {
 	@Test
 	public void buildWhenSettingsProvidedThenSet() {
 		String authorizationEndpoint = "/oauth2/v1/authorize";
+		String pushedAuthorizationRequestEndpoint = "/oauth2/v1/par";
 		String tokenEndpoint = "/oauth2/v1/token";
 		String jwkSetEndpoint = "/oauth2/v1/jwks";
 		String tokenRevocationEndpoint = "/oauth2/v1/revoke";
@@ -59,6 +61,7 @@ public class AuthorizationServerSettingsTests {
 		AuthorizationServerSettings authorizationServerSettings = AuthorizationServerSettings.builder()
 			.issuer(issuer)
 			.authorizationEndpoint(authorizationEndpoint)
+			.pushedAuthorizationRequestEndpoint(pushedAuthorizationRequestEndpoint)
 			.tokenEndpoint(tokenEndpoint)
 			.jwkSetEndpoint(jwkSetEndpoint)
 			.tokenRevocationEndpoint(tokenRevocationEndpoint)
@@ -72,6 +75,8 @@ public class AuthorizationServerSettingsTests {
 		assertThat(authorizationServerSettings.getIssuer()).isEqualTo(issuer);
 		assertThat(authorizationServerSettings.isMultipleIssuersAllowed()).isFalse();
 		assertThat(authorizationServerSettings.getAuthorizationEndpoint()).isEqualTo(authorizationEndpoint);
+		assertThat(authorizationServerSettings.getPushedAuthorizationRequestEndpoint())
+			.isEqualTo(pushedAuthorizationRequestEndpoint);
 		assertThat(authorizationServerSettings.getTokenEndpoint()).isEqualTo(tokenEndpoint);
 		assertThat(authorizationServerSettings.getJwkSetEndpoint()).isEqualTo(jwkSetEndpoint);
 		assertThat(authorizationServerSettings.getTokenRevocationEndpoint()).isEqualTo(tokenRevocationEndpoint);
@@ -100,6 +105,7 @@ public class AuthorizationServerSettingsTests {
 		assertThat(authorizationServerSettings.getIssuer()).isNull();
 		assertThat(authorizationServerSettings.isMultipleIssuersAllowed()).isTrue();
 		assertThat(authorizationServerSettings.getAuthorizationEndpoint()).isEqualTo("/oauth2/authorize");
+		assertThat(authorizationServerSettings.getPushedAuthorizationRequestEndpoint()).isEqualTo("/oauth2/par");
 		assertThat(authorizationServerSettings.getTokenEndpoint()).isEqualTo("/oauth2/token");
 		assertThat(authorizationServerSettings.getJwkSetEndpoint()).isEqualTo("/oauth2/jwks");
 		assertThat(authorizationServerSettings.getTokenRevocationEndpoint()).isEqualTo("/oauth2/revoke");
@@ -116,7 +122,7 @@ public class AuthorizationServerSettingsTests {
 			.settings((settings) -> settings.put("name2", "value2"))
 			.build();
 
-		assertThat(authorizationServerSettings.getSettings()).hasSize(13);
+		assertThat(authorizationServerSettings.getSettings()).hasSize(14);
 		assertThat(authorizationServerSettings.<String>getSetting("name1")).isEqualTo("value1");
 		assertThat(authorizationServerSettings.<String>getSetting("name2")).isEqualTo("value2");
 	}
@@ -134,6 +140,13 @@ public class AuthorizationServerSettingsTests {
 			.withMessage("value cannot be null");
 	}
 
+	@Test
+	public void pushedAuthorizationRequestEndpointWhenNullThenThrowIllegalArgumentException() {
+		assertThatIllegalArgumentException()
+			.isThrownBy(() -> AuthorizationServerSettings.builder().pushedAuthorizationRequestEndpoint(null))
+			.withMessage("value cannot be null");
+	}
+
 	@Test
 	public void tokenEndpointWhenNullThenThrowIllegalArgumentException() {
 		assertThatIllegalArgumentException().isThrownBy(() -> AuthorizationServerSettings.builder().tokenEndpoint(null))

+ 16 - 0
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilterTests.java

@@ -54,6 +54,9 @@ import org.springframework.security.oauth2.server.authorization.authentication.O
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationToken;
 import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
 import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
+import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
+import org.springframework.security.oauth2.server.authorization.context.TestAuthorizationServerContext;
+import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
 import org.springframework.security.web.authentication.AuthenticationConverter;
 import org.springframework.security.web.authentication.AuthenticationFailureHandler;
 import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
@@ -112,11 +115,14 @@ public class OAuth2AuthorizationEndpointFilterTests {
 		Instant issuedAt = Instant.now();
 		Instant expiresAt = issuedAt.plus(5, ChronoUnit.MINUTES);
 		this.authorizationCode = new OAuth2AuthorizationCode("code", issuedAt, expiresAt);
+		AuthorizationServerContextHolder
+			.setContext(new TestAuthorizationServerContext(AuthorizationServerSettings.builder().build(), null));
 	}
 
 	@AfterEach
 	public void cleanup() {
 		SecurityContextHolder.clearContext();
+		AuthorizationServerContextHolder.resetContext();
 	}
 
 	@Test
@@ -181,6 +187,16 @@ public class OAuth2AuthorizationEndpointFilterTests {
 		verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
 	}
 
+	@Test
+	public void doFilterWhenAuthorizationRequestMultipleRequestUriThenInvalidRequestError() throws Exception {
+		doFilterWhenAuthorizationRequestInvalidParameterThenError(TestRegisteredClients.registeredClient().build(),
+				"request_uri", OAuth2ErrorCodes.INVALID_REQUEST, (request) -> {
+					request.addParameter("request_uri", "request_uri");
+					request.addParameter("request_uri", "request_uri_2");
+					updateQueryString(request);
+				});
+	}
+
 	@Test
 	public void doFilterWhenAuthorizationRequestMissingResponseTypeThenInvalidRequestError() throws Exception {
 		doFilterWhenAuthorizationRequestInvalidParameterThenError(TestRegisteredClients.registeredClient().build(),

+ 490 - 0
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2PushedAuthorizationRequestEndpointFilterTests.java

@@ -0,0 +1,490 @@
+/*
+ * Copyright 2020-2025 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;
+
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Map;
+import java.util.function.Consumer;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.assertj.core.api.InstanceOfAssertFactories;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.converter.GenericHttpMessageConverter;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.mock.http.client.MockClientHttpResponse;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.security.authentication.AuthenticationDetailsSource;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+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.OAuth2AuthorizationResponseType;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
+import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
+import org.springframework.security.oauth2.core.oidc.OidcScopes;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2PushedAuthorizationRequestAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
+import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
+import org.springframework.security.oauth2.server.authorization.context.TestAuthorizationServerContext;
+import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
+import org.springframework.security.web.authentication.AuthenticationConverter;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.security.web.authentication.WebAuthenticationDetails;
+import org.springframework.util.StringUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+/**
+ * Tests for {@link OAuth2PushedAuthorizationRequestEndpointFilter}.
+ *
+ * @author Joe Grandja
+ */
+public class OAuth2PushedAuthorizationRequestEndpointFilterTests {
+
+	private static final String AUTHORIZATION_URI = "https://provider.com/oauth2/par";
+
+	private static final String STATE = "state";
+
+	private static final String REMOTE_ADDRESS = "remote-address";
+
+	private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter = new OAuth2ErrorHttpMessageConverter();
+
+	private final GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters
+		.getJsonMessageConverter();
+
+	private AuthenticationManager authenticationManager;
+
+	private OAuth2PushedAuthorizationRequestEndpointFilter filter;
+
+	private TestingAuthenticationToken clientPrincipal;
+
+	@BeforeEach
+	public void setUp() {
+		this.authenticationManager = mock(AuthenticationManager.class);
+		this.filter = new OAuth2PushedAuthorizationRequestEndpointFilter(this.authenticationManager);
+		this.clientPrincipal = new TestingAuthenticationToken("client-id", "client-secret");
+		this.clientPrincipal.setAuthenticated(true);
+		SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
+		securityContext.setAuthentication(this.clientPrincipal);
+		SecurityContextHolder.setContext(securityContext);
+		AuthorizationServerContextHolder
+			.setContext(new TestAuthorizationServerContext(AuthorizationServerSettings.builder().build(), null));
+	}
+
+	@AfterEach
+	public void cleanup() {
+		SecurityContextHolder.clearContext();
+		AuthorizationServerContextHolder.resetContext();
+	}
+
+	@Test
+	public void constructorWhenAuthenticationManagerNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> new OAuth2PushedAuthorizationRequestEndpointFilter(null))
+			.isInstanceOf(IllegalArgumentException.class)
+			.hasMessage("authenticationManager cannot be null");
+	}
+
+	@Test
+	public void constructorWhenPushedAuthorizationRequestEndpointUriNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> new OAuth2PushedAuthorizationRequestEndpointFilter(this.authenticationManager, null))
+			.isInstanceOf(IllegalArgumentException.class)
+			.hasMessage("pushedAuthorizationRequestEndpointUri cannot be empty");
+	}
+
+	@Test
+	public void setAuthenticationDetailsSourceWhenNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> this.filter.setAuthenticationDetailsSource(null))
+			.isInstanceOf(IllegalArgumentException.class)
+			.hasMessage("authenticationDetailsSource cannot be null");
+	}
+
+	@Test
+	public void setAuthenticationConverterWhenNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> this.filter.setAuthenticationConverter(null))
+			.isInstanceOf(IllegalArgumentException.class)
+			.hasMessage("authenticationConverter cannot be null");
+	}
+
+	@Test
+	public void setAuthenticationSuccessHandlerWhenNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> this.filter.setAuthenticationSuccessHandler(null))
+			.isInstanceOf(IllegalArgumentException.class)
+			.hasMessage("authenticationSuccessHandler cannot be null");
+	}
+
+	@Test
+	public void setAuthenticationFailureHandlerWhenNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> this.filter.setAuthenticationFailureHandler(null))
+			.isInstanceOf(IllegalArgumentException.class)
+			.hasMessage("authenticationFailureHandler cannot be null");
+	}
+
+	@Test
+	public void doFilterWhenNotPushedAuthorizationRequestThenNotProcessed() throws Exception {
+		String requestUri = "/path";
+		MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri);
+		request.setServletPath(requestUri);
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		FilterChain filterChain = mock(FilterChain.class);
+
+		this.filter.doFilter(request, response, filterChain);
+
+		verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
+	}
+
+	@Test
+	public void doFilterWhenPushedAuthorizationRequestIncludesRequestUriThenInvalidRequestError() throws Exception {
+		doFilterWhenPushedAuthorizationRequestInvalidParameterThenError(
+				TestRegisteredClients.registeredClient().build(), "request_uri", OAuth2ErrorCodes.INVALID_REQUEST,
+				(request) -> request.addParameter("request_uri", "request_uri"));
+	}
+
+	@Test
+	public void doFilterWhenPushedAuthorizationRequestMultipleResponseTypeThenInvalidRequestError() throws Exception {
+		doFilterWhenPushedAuthorizationRequestInvalidParameterThenError(
+				TestRegisteredClients.registeredClient().build(), OAuth2ParameterNames.RESPONSE_TYPE,
+				OAuth2ErrorCodes.INVALID_REQUEST,
+				(request) -> request.addParameter(OAuth2ParameterNames.RESPONSE_TYPE, "id_token"));
+	}
+
+	@Test
+	public void doFilterWhenPushedAuthorizationRequestInvalidResponseTypeThenUnsupportedResponseTypeError()
+			throws Exception {
+		doFilterWhenPushedAuthorizationRequestInvalidParameterThenError(
+				TestRegisteredClients.registeredClient().build(), OAuth2ParameterNames.RESPONSE_TYPE,
+				OAuth2ErrorCodes.UNSUPPORTED_RESPONSE_TYPE,
+				(request) -> request.setParameter(OAuth2ParameterNames.RESPONSE_TYPE, "id_token"));
+	}
+
+	@Test
+	public void doFilterWhenPushedAuthorizationRequestMissingClientIdThenInvalidRequestError() throws Exception {
+		doFilterWhenPushedAuthorizationRequestInvalidParameterThenError(
+				TestRegisteredClients.registeredClient().build(), OAuth2ParameterNames.CLIENT_ID,
+				OAuth2ErrorCodes.INVALID_REQUEST, (request) -> request.removeParameter(OAuth2ParameterNames.CLIENT_ID));
+	}
+
+	@Test
+	public void doFilterWhenPushedAuthorizationRequestMultipleClientIdThenInvalidRequestError() throws Exception {
+		doFilterWhenPushedAuthorizationRequestInvalidParameterThenError(
+				TestRegisteredClients.registeredClient().build(), OAuth2ParameterNames.CLIENT_ID,
+				OAuth2ErrorCodes.INVALID_REQUEST,
+				(request) -> request.addParameter(OAuth2ParameterNames.CLIENT_ID, "client-2"));
+	}
+
+	@Test
+	public void doFilterWhenPushedAuthorizationRequestMultipleRedirectUriThenInvalidRequestError() throws Exception {
+		doFilterWhenPushedAuthorizationRequestInvalidParameterThenError(
+				TestRegisteredClients.registeredClient().build(), OAuth2ParameterNames.REDIRECT_URI,
+				OAuth2ErrorCodes.INVALID_REQUEST,
+				(request) -> request.addParameter(OAuth2ParameterNames.REDIRECT_URI, "https://example2.com"));
+	}
+
+	@Test
+	public void doFilterWhenPushedAuthorizationRequestMultipleScopeThenInvalidRequestError() throws Exception {
+		doFilterWhenPushedAuthorizationRequestInvalidParameterThenError(
+				TestRegisteredClients.registeredClient().build(), OAuth2ParameterNames.SCOPE,
+				OAuth2ErrorCodes.INVALID_REQUEST,
+				(request) -> request.addParameter(OAuth2ParameterNames.SCOPE, "scope2"));
+	}
+
+	@Test
+	public void doFilterWhenPushedAuthorizationRequestMultipleStateThenInvalidRequestError() throws Exception {
+		doFilterWhenPushedAuthorizationRequestInvalidParameterThenError(
+				TestRegisteredClients.registeredClient().build(), OAuth2ParameterNames.STATE,
+				OAuth2ErrorCodes.INVALID_REQUEST,
+				(request) -> request.addParameter(OAuth2ParameterNames.STATE, "state2"));
+	}
+
+	@Test
+	public void doFilterWhenPushedAuthorizationRequestMultipleCodeChallengeThenInvalidRequestError() throws Exception {
+		doFilterWhenPushedAuthorizationRequestInvalidParameterThenError(
+				TestRegisteredClients.registeredClient().build(), PkceParameterNames.CODE_CHALLENGE,
+				OAuth2ErrorCodes.INVALID_REQUEST, (request) -> {
+					request.addParameter(PkceParameterNames.CODE_CHALLENGE, "code-challenge");
+					request.addParameter(PkceParameterNames.CODE_CHALLENGE, "another-code-challenge");
+				});
+	}
+
+	@Test
+	public void doFilterWhenPushedAuthorizationRequestMultipleCodeChallengeMethodThenInvalidRequestError()
+			throws Exception {
+		doFilterWhenPushedAuthorizationRequestInvalidParameterThenError(
+				TestRegisteredClients.registeredClient().build(), PkceParameterNames.CODE_CHALLENGE_METHOD,
+				OAuth2ErrorCodes.INVALID_REQUEST, (request) -> {
+					request.addParameter(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
+					request.addParameter(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
+				});
+	}
+
+	@Test
+	public void doFilterWhenPushedAuthenticationRequestMultiplePromptThenInvalidRequestError() throws Exception {
+		// Setup OpenID Connect request
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scopes((scopes) -> {
+			scopes.clear();
+			scopes.add(OidcScopes.OPENID);
+		}).build();
+		doFilterWhenPushedAuthorizationRequestInvalidParameterThenError(registeredClient, "prompt",
+				OAuth2ErrorCodes.INVALID_REQUEST, (request) -> {
+					request.addParameter("prompt", "none");
+					request.addParameter("prompt", "login");
+				});
+	}
+
+	@Test
+	public void doFilterWhenPushedAuthorizationRequestAuthenticationExceptionThenErrorResponse() throws Exception {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST, "error description", "error uri");
+		given(this.authenticationManager.authenticate(any())).willThrow(new OAuth2AuthenticationException(error));
+
+		MockHttpServletRequest request = createPushedAuthorizationRequest(registeredClient);
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		FilterChain filterChain = mock(FilterChain.class);
+
+		this.filter.doFilter(request, response, filterChain);
+
+		verify(this.authenticationManager).authenticate(any());
+		verifyNoInteractions(filterChain);
+
+		assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value());
+		OAuth2Error errorResponse = readError(response);
+		assertThat(errorResponse.getErrorCode()).isEqualTo(error.getErrorCode());
+		assertThat(errorResponse.getDescription()).isEqualTo(error.getDescription());
+		assertThat(errorResponse.getUri()).isEqualTo(error.getUri());
+		assertThat(SecurityContextHolder.getContext().getAuthentication()).isSameAs(this.clientPrincipal);
+	}
+
+	@Test
+	public void doFilterWhenCustomAuthenticationConverterThenUsed() throws Exception {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		OAuth2PushedAuthorizationRequestAuthenticationToken pushedAuthorizationRequestAuthenticationResult = new OAuth2PushedAuthorizationRequestAuthenticationToken(
+				AUTHORIZATION_URI, registeredClient.getClientId(), this.clientPrincipal, "request_uri",
+				Instant.now().plusSeconds(30), registeredClient.getRedirectUris().iterator().next(), STATE,
+				registeredClient.getScopes());
+
+		AuthenticationConverter authenticationConverter = mock(AuthenticationConverter.class);
+		given(authenticationConverter.convert(any())).willReturn(pushedAuthorizationRequestAuthenticationResult);
+		this.filter.setAuthenticationConverter(authenticationConverter);
+
+		given(this.authenticationManager.authenticate(any()))
+			.willReturn(pushedAuthorizationRequestAuthenticationResult);
+
+		MockHttpServletRequest request = createPushedAuthorizationRequest(registeredClient);
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		FilterChain filterChain = mock(FilterChain.class);
+
+		this.filter.doFilter(request, response, filterChain);
+
+		verify(authenticationConverter).convert(any());
+		verify(this.authenticationManager).authenticate(any());
+	}
+
+	@Test
+	public void doFilterWhenCustomAuthenticationSuccessHandlerThenUsed() throws Exception {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		OAuth2PushedAuthorizationRequestAuthenticationToken pushedAuthorizationRequestAuthenticationResult = new OAuth2PushedAuthorizationRequestAuthenticationToken(
+				AUTHORIZATION_URI, registeredClient.getClientId(), this.clientPrincipal, "request_uri",
+				Instant.now().plusSeconds(30), registeredClient.getRedirectUris().iterator().next(), STATE,
+				registeredClient.getScopes());
+		given(this.authenticationManager.authenticate(any()))
+			.willReturn(pushedAuthorizationRequestAuthenticationResult);
+
+		AuthenticationSuccessHandler authenticationSuccessHandler = mock(AuthenticationSuccessHandler.class);
+		this.filter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
+
+		MockHttpServletRequest request = createPushedAuthorizationRequest(registeredClient);
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		FilterChain filterChain = mock(FilterChain.class);
+
+		this.filter.doFilter(request, response, filterChain);
+
+		verify(this.authenticationManager).authenticate(any());
+		verifyNoInteractions(filterChain);
+		verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(),
+				same(pushedAuthorizationRequestAuthenticationResult));
+	}
+
+	@Test
+	public void doFilterWhenCustomAuthenticationFailureHandlerThenUsed() throws Exception {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		OAuth2Error error = new OAuth2Error("errorCode", "errorDescription", "errorUri");
+		OAuth2AuthenticationException authenticationException = new OAuth2AuthenticationException(error);
+		given(this.authenticationManager.authenticate(any())).willThrow(authenticationException);
+
+		AuthenticationFailureHandler authenticationFailureHandler = mock(AuthenticationFailureHandler.class);
+		this.filter.setAuthenticationFailureHandler(authenticationFailureHandler);
+
+		MockHttpServletRequest request = createPushedAuthorizationRequest(registeredClient);
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		FilterChain filterChain = mock(FilterChain.class);
+
+		this.filter.doFilter(request, response, filterChain);
+
+		verify(this.authenticationManager).authenticate(any());
+		verifyNoInteractions(filterChain);
+		verify(authenticationFailureHandler).onAuthenticationFailure(any(), any(), same(authenticationException));
+	}
+
+	@Test
+	public void doFilterWhenCustomAuthenticationDetailsSourceThenUsed() throws Exception {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		MockHttpServletRequest request = createPushedAuthorizationRequest(registeredClient);
+
+		AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> authenticationDetailsSource = mock(
+				AuthenticationDetailsSource.class);
+		WebAuthenticationDetails webAuthenticationDetails = new WebAuthenticationDetails(request);
+		given(authenticationDetailsSource.buildDetails(request)).willReturn(webAuthenticationDetails);
+		this.filter.setAuthenticationDetailsSource(authenticationDetailsSource);
+
+		OAuth2PushedAuthorizationRequestAuthenticationToken pushedAuthorizationRequestAuthenticationResult = new OAuth2PushedAuthorizationRequestAuthenticationToken(
+				AUTHORIZATION_URI, registeredClient.getClientId(), this.clientPrincipal, "request_uri",
+				Instant.now().plusSeconds(30), registeredClient.getRedirectUris().iterator().next(), STATE,
+				registeredClient.getScopes());
+
+		given(this.authenticationManager.authenticate(any()))
+			.willReturn(pushedAuthorizationRequestAuthenticationResult);
+
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		FilterChain filterChain = mock(FilterChain.class);
+
+		this.filter.doFilter(request, response, filterChain);
+
+		verify(authenticationDetailsSource).buildDetails(any());
+		verify(this.authenticationManager).authenticate(any());
+	}
+
+	@Test
+	public void doFilterWhenPushedAuthorizationRequestAuthenticatedThenPushedAuthorizationResponse() throws Exception {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		String requestUri = "request_uri";
+		Instant requestUriExpiresAt = Instant.now().plusSeconds(30);
+		OAuth2PushedAuthorizationRequestAuthenticationToken pushedAuthorizationRequestAuthenticationResult = new OAuth2PushedAuthorizationRequestAuthenticationToken(
+				AUTHORIZATION_URI, registeredClient.getClientId(), this.clientPrincipal, requestUri,
+				requestUriExpiresAt, registeredClient.getRedirectUris().iterator().next(), STATE,
+				registeredClient.getScopes());
+		given(this.authenticationManager.authenticate(any()))
+			.willReturn(pushedAuthorizationRequestAuthenticationResult);
+
+		MockHttpServletRequest request = createPushedAuthorizationRequest(registeredClient);
+		request.addParameter("custom-param", "custom-value-1", "custom-value-2");
+
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		FilterChain filterChain = mock(FilterChain.class);
+
+		this.filter.doFilter(request, response, filterChain);
+
+		ArgumentCaptor<OAuth2PushedAuthorizationRequestAuthenticationToken> pushedAuthorizationRequestAuthenticationCaptor = ArgumentCaptor
+			.forClass(OAuth2PushedAuthorizationRequestAuthenticationToken.class);
+		verify(this.authenticationManager).authenticate(pushedAuthorizationRequestAuthenticationCaptor.capture());
+		verifyNoInteractions(filterChain);
+
+		assertThat(pushedAuthorizationRequestAuthenticationCaptor.getValue().getDetails())
+			.asInstanceOf(InstanceOfAssertFactories.type(WebAuthenticationDetails.class))
+			.extracting(WebAuthenticationDetails::getRemoteAddress)
+			.isEqualTo(REMOTE_ADDRESS);
+
+		// Assert that multi-valued request parameters are preserved
+		assertThat(pushedAuthorizationRequestAuthenticationCaptor.getValue().getAdditionalParameters())
+			.extracting((params) -> params.get("custom-param"))
+			.asInstanceOf(InstanceOfAssertFactories.type(String[].class))
+			.isEqualTo(new String[] { "custom-value-1", "custom-value-2" });
+		assertThat(response.getStatus()).isEqualTo(HttpStatus.CREATED.value());
+		Map<String, Object> responseParameters = readPushedAuthorizationResponse(response);
+		assertThat(responseParameters.get("request_uri")).isEqualTo(requestUri);
+		assertThat(responseParameters.get("expires_in"))
+			.isEqualTo((int) ChronoUnit.SECONDS.between(Instant.now(), requestUriExpiresAt));
+	}
+
+	private void doFilterWhenPushedAuthorizationRequestInvalidParameterThenError(RegisteredClient registeredClient,
+			String parameterName, String errorCode, Consumer<MockHttpServletRequest> requestConsumer) throws Exception {
+
+		doFilterWhenRequestInvalidParameterThenError(createPushedAuthorizationRequest(registeredClient), parameterName,
+				errorCode, requestConsumer);
+	}
+
+	private void doFilterWhenRequestInvalidParameterThenError(MockHttpServletRequest request, String parameterName,
+			String errorCode, Consumer<MockHttpServletRequest> requestConsumer) throws Exception {
+
+		requestConsumer.accept(request);
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		FilterChain filterChain = mock(FilterChain.class);
+
+		this.filter.doFilter(request, response, filterChain);
+
+		verifyNoInteractions(filterChain);
+
+		assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value());
+		OAuth2Error error = readError(response);
+		assertThat(error.getErrorCode()).isEqualTo(errorCode);
+		assertThat(error.getDescription()).isEqualTo("OAuth 2.0 Parameter: " + parameterName);
+	}
+
+	private static MockHttpServletRequest createPushedAuthorizationRequest(RegisteredClient registeredClient) {
+		String requestUri = AuthorizationServerContextHolder.getContext()
+			.getAuthorizationServerSettings()
+			.getPushedAuthorizationRequestEndpoint();
+		MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri);
+		request.setServletPath(requestUri);
+		request.setRemoteAddr(REMOTE_ADDRESS);
+
+		request.addParameter(OAuth2ParameterNames.RESPONSE_TYPE, OAuth2AuthorizationResponseType.CODE.getValue());
+		request.addParameter(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
+		request.addParameter(OAuth2ParameterNames.REDIRECT_URI, registeredClient.getRedirectUris().iterator().next());
+		request.addParameter(OAuth2ParameterNames.SCOPE,
+				StringUtils.collectionToDelimitedString(registeredClient.getScopes(), " "));
+		request.addParameter(OAuth2ParameterNames.STATE, "state");
+
+		return request;
+	}
+
+	private OAuth2Error readError(MockHttpServletResponse response) throws Exception {
+		MockClientHttpResponse httpResponse = new MockClientHttpResponse(response.getContentAsByteArray(),
+				HttpStatus.valueOf(response.getStatus()));
+		return this.errorHttpResponseConverter.read(OAuth2Error.class, httpResponse);
+	}
+
+	@SuppressWarnings("unchecked")
+	private Map<String, Object> readPushedAuthorizationResponse(MockHttpServletResponse response) throws Exception {
+		final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP = new ParameterizedTypeReference<>() {
+		};
+		MockClientHttpResponse httpResponse = new MockClientHttpResponse(response.getContentAsByteArray(),
+				HttpStatus.valueOf(response.getStatus()));
+		return (Map<String, Object>) this.jsonMessageConverter.read(STRING_OBJECT_MAP.getType(), null, httpResponse);
+	}
+
+}