Ver código fonte

Merge branch 0.4.x into main

The following commits are merged using the default merge strategy.

80b01854f2f41083406916809090dcd303b37c25 Update README with documentation links
4d94e7095d22ca8455c43f3cf604a4fa84c5241d Decompose OAuth2AuthorizationCodeRequestAuthenticationProvider
cd6f1d7dc33226a2b2241ce891907da531b36344 Return registration_endpoint when client registration is enabled
26aed3c183213a4936a5aa903b5d983f676b0076 Polish gh-881
92dbcf29a5bbb1c4b032d4ad4f1c90f1f6213587 Move integration tests for OidcProviderConfiguration
Joe Grandja 2 anos atrás
pai
commit
2ed0080f72
26 arquivos alterados com 1971 adições e 1458 exclusões
  1. 2 2
      README.adoc
  2. 4 4
      docs/src/docs/asciidoc/protocol-endpoints.adoc
  3. 14 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/AbstractOAuth2AuthorizationServerMetadata.java
  4. 10 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimAccessor.java
  5. 6 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimNames.java
  6. 55 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeGenerator.java
  7. 45 262
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java
  8. 83 209
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationToken.java
  9. 11 26
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationValidator.java
  10. 5 5
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationContext.java
  11. 316 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationProvider.java
  12. 135 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationToken.java
  13. 14 2
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationEndpointConfigurer.java
  14. 23 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcConfigurer.java
  15. 31 18
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilter.java
  16. 26 49
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2AuthorizationCodeRequestAuthenticationConverter.java
  17. 109 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2AuthorizationConsentAuthenticationConverter.java
  18. 68 498
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProviderTests.java
  19. 28 99
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationTokenTests.java
  20. 9 9
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationContextTests.java
  21. 548 0
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationProviderTests.java
  22. 21 21
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationCodeGrantTests.java
  23. 347 0
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcProviderConfigurationTests.java
  24. 0 205
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OidcTests.java
  25. 16 0
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderConfigurationTests.java
  26. 45 49
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilterTests.java

+ 2 - 2
README.adoc

@@ -36,9 +36,9 @@ The goal is to leverage all the knowledge learned thus far and apply the same to
 Submitted work via pull requests should follow the same coding style/conventions and adopt the same or similar design patterns that have been established in Spring Security's OAuth 2.0 support.
 
 == Documentation
-Be sure to read the https://docs.spring.io/spring-security/reference[Spring Security Reference], as well as the https://docs.spring.io/spring-security/reference/servlet/oauth2/index.html[OAuth 2.0 Reference], which describes the Client and Resource Server features available.
+Be sure to read the https://docs.spring.io/spring-authorization-server/docs/current/reference/html/[Spring Authorization Server Reference] and https://docs.spring.io/spring-security/reference[Spring Security Reference], as well as the https://docs.spring.io/spring-security/reference/servlet/oauth2/index.html[OAuth 2.0 Reference], which describes the Client and Resource Server features available.
 
-Extensive JavaDoc for the Spring Security code is also available in the https://docs.spring.io/spring-security/site/docs/current/api/[Spring Security API Documentation].
+JavaDoc is also available for the https://docs.spring.io/spring-authorization-server/docs/current/api/[Spring Authorization Server API] and https://docs.spring.io/spring-security/site/docs/current/api/[Spring Security API].
 
 == Code of Conduct
 This project adheres to the Contributor Covenant link:CODE_OF_CONDUCT.adoc[code of conduct].

+ 4 - 4
docs/src/docs/asciidoc/protocol-endpoints.adoc

@@ -32,9 +32,9 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h
 	return http.build();
 }
 ----
-<1> `authorizationRequestConverter()`: Adds an `AuthenticationConverter` (_pre-processor_) used when attempting to extract an https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1[OAuth2 authorization request] (or consent) from `HttpServletRequest` to an instance of `OAuth2AuthorizationCodeRequestAuthenticationToken`.
+<1> `authorizationRequestConverter()`: Adds an `AuthenticationConverter` (_pre-processor_) used when attempting to extract an https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1[OAuth2 authorization request] (or consent) from `HttpServletRequest` to an instance of `OAuth2AuthorizationCodeRequestAuthenticationToken` or `OAuth2AuthorizationConsentAuthenticationToken`.
 <2> `authorizationRequestConverters()`: Sets the `Consumer` providing access to the `List` of default and (optionally) added ``AuthenticationConverter``'s allowing the ability to add, remove, or customize a specific `AuthenticationConverter`.
-<3> `authenticationProvider()`: Adds an `AuthenticationProvider` (_main processor_) used for authenticating the `OAuth2AuthorizationCodeRequestAuthenticationToken`.
+<3> `authenticationProvider()`: Adds an `AuthenticationProvider` (_main processor_) used for authenticating the `OAuth2AuthorizationCodeRequestAuthenticationToken` or `OAuth2AuthorizationConsentAuthenticationToken`.
 <4> `authenticationProviders()`: Sets the `Consumer` providing access to the `List` of default and (optionally) added ``AuthenticationProvider``'s allowing the ability to add, remove, or customize a specific `AuthenticationProvider`.
 <5> `authorizationResponseHandler()`: The `AuthenticationSuccessHandler` (_post-processor_) used for handling an "`authenticated`" `OAuth2AuthorizationCodeRequestAuthenticationToken` and returning the https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2[OAuth2AuthorizationResponse].
 <6> `errorResponseHandler()`: The `AuthenticationFailureHandler` (_post-processor_) used for handling an `OAuth2AuthorizationCodeRequestAuthenticationException` and returning the https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1[OAuth2Error response].
@@ -45,8 +45,8 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h
 
 `OAuth2AuthorizationEndpointFilter` is configured with the following defaults:
 
-* `*AuthenticationConverter*` -- An `OAuth2AuthorizationCodeRequestAuthenticationConverter`.
-* `*AuthenticationManager*` -- An `AuthenticationManager` composed of `OAuth2AuthorizationCodeRequestAuthenticationProvider`.
+* `*AuthenticationConverter*` -- A `DelegatingAuthenticationConverter` composed of `OAuth2AuthorizationCodeRequestAuthenticationConverter` and `OAuth2AuthorizationConsentAuthenticationConverter`.
+* `*AuthenticationManager*` -- An `AuthenticationManager` composed of `OAuth2AuthorizationCodeRequestAuthenticationProvider` and `OAuth2AuthorizationConsentAuthenticationProvider`.
 * `*AuthenticationSuccessHandler*` -- An internal implementation that handles an "`authenticated`" `OAuth2AuthorizationCodeRequestAuthenticationToken` and returns the `OAuth2AuthorizationResponse`.
 * `*AuthenticationFailureHandler*` -- An internal implementation that uses the `OAuth2Error` associated with the `OAuth2AuthorizationCodeRequestAuthenticationException` and returns the `OAuth2Error` response.
 

+ 14 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/AbstractOAuth2AuthorizationServerMetadata.java

@@ -274,6 +274,17 @@ public abstract class AbstractOAuth2AuthorizationServerMetadata implements OAuth
 			return getThis();
 		}
 
+		/**
+		 * Use this {@code registration_endpoint} in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL.
+		 *
+		 * @param clientRegistrationEndpoint the {@code URL} of the OAuth 2.0 Dynamic Client Registration Endpoint
+		 * @return the {@link AbstractBuilder} for further configuration
+		 * @since 0.4.0
+		 */
+		public B clientRegistrationEndpoint(String clientRegistrationEndpoint) {
+			return claim(OAuth2AuthorizationServerMetadataClaimNames.REGISTRATION_ENDPOINT, clientRegistrationEndpoint);
+		}
+
 		/**
 		 * Add this Proof Key for Code Exchange (PKCE) {@code code_challenge_method} to the collection of {@code code_challenge_methods_supported}
 		 * in the resulting {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL.
@@ -369,6 +380,9 @@ public abstract class AbstractOAuth2AuthorizationServerMetadata implements OAuth
 				Assert.isInstanceOf(List.class, getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED), "tokenIntrospectionEndpointAuthenticationMethods must be of type List");
 				Assert.notEmpty((List<?>) getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED), "tokenIntrospectionEndpointAuthenticationMethods cannot be empty");
 			}
+			if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.REGISTRATION_ENDPOINT) != null) {
+				validateURL(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.REGISTRATION_ENDPOINT), "clientRegistrationEndpoint must be a valid URL");
+			}
 			if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED) != null) {
 				Assert.isInstanceOf(List.class, getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED), "codeChallengeMethods must be of type List");
 				Assert.notEmpty((List<?>) getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.CODE_CHALLENGE_METHODS_SUPPORTED), "codeChallengeMethods cannot be empty");

+ 10 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimAccessor.java

@@ -141,6 +141,16 @@ public interface OAuth2AuthorizationServerMetadataClaimAccessor extends ClaimAcc
 		return getClaimAsStringList(OAuth2AuthorizationServerMetadataClaimNames.INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED);
 	}
 
+	/**
+	 * Returns the {@code URL} of the OAuth 2.0 Dynamic Client Registration Endpoint {@code (registration_endpoint)}.
+	 *
+	 * @return the {@code URL} of the OAuth 2.0 Dynamic Client Registration Endpoint
+	 * @since 0.4.0
+	 */
+	default URL getClientRegistrationEndpoint() {
+		return getClaimAsURL(OAuth2AuthorizationServerMetadataClaimNames.REGISTRATION_ENDPOINT);
+	}
+
 	/**
 	 * Returns the Proof Key for Code Exchange (PKCE) {@code code_challenge_method} values supported {@code (code_challenge_methods_supported)}.
 	 *

+ 6 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimNames.java

@@ -86,6 +86,12 @@ public class OAuth2AuthorizationServerMetadataClaimNames {
 	 */
 	public static final String INTROSPECTION_ENDPOINT_AUTH_METHODS_SUPPORTED = "introspection_endpoint_auth_methods_supported";
 
+	/**
+	 * {@code registration_endpoint} - the {@code URL} of the OAuth 2.0 Dynamic Client Registration Endpoint
+	 * @since 0.4.0
+	 */
+	public static final String REGISTRATION_ENDPOINT = "registration_endpoint";
+
 	/**
 	 * {@code code_challenge_methods_supported} - the Proof Key for Code Exchange (PKCE) {@code code_challenge_method} values supported
 	 */

+ 55 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeGenerator.java

@@ -0,0 +1,55 @@
+/*
+ * Copyright 2020-2022 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.lang.Nullable;
+import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
+import org.springframework.security.crypto.keygen.StringKeyGenerator;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
+
+/**
+ * An {@link OAuth2TokenGenerator} that generates an {@link OAuth2AuthorizationCode}.
+ *
+ * @author Joe Grandja
+ * @since 0.4.0
+ * @see OAuth2TokenGenerator
+ * @see OAuth2AuthorizationCode
+ * @see OAuth2AuthorizationCodeRequestAuthenticationProvider
+ * @see OAuth2AuthorizationConsentAuthenticationProvider
+ */
+final class OAuth2AuthorizationCodeGenerator implements OAuth2TokenGenerator<OAuth2AuthorizationCode> {
+	private final StringKeyGenerator authorizationCodeGenerator =
+			new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
+
+	@Nullable
+	@Override
+	public OAuth2AuthorizationCode generate(OAuth2TokenContext context) {
+		if (context.getTokenType() == null ||
+				!OAuth2ParameterNames.CODE.equals(context.getTokenType().getValue())) {
+			return null;
+		}
+		Instant issuedAt = Instant.now();
+		Instant expiresAt = issuedAt.plus(context.getRegisteredClient().getTokenSettings().getAuthorizationCodeTimeToLive());
+		return new OAuth2AuthorizationCode(this.authorizationCodeGenerator.generateKey(), issuedAt, expiresAt);
+	}
+
+}

+ 45 - 262
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java

@@ -16,19 +16,14 @@
 package org.springframework.security.oauth2.server.authorization.authentication;
 
 import java.security.Principal;
-import java.time.Instant;
 import java.util.Base64;
-import java.util.Collections;
-import java.util.HashSet;
 import java.util.Set;
 import java.util.function.Consumer;
 
-import org.springframework.lang.Nullable;
 import org.springframework.security.authentication.AnonymousAuthenticationToken;
 import org.springframework.security.authentication.AuthenticationProvider;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
-import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
 import org.springframework.security.crypto.keygen.StringKeyGenerator;
 import org.springframework.security.oauth2.core.AuthorizationGrantType;
@@ -54,7 +49,7 @@ import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
 
 /**
- * An {@link AuthenticationProvider} implementation for the OAuth 2.0 Authorization Request (and Consent)
+ * An {@link AuthenticationProvider} implementation for the OAuth 2.0 Authorization Request
  * used in the Authorization Code Grant.
  *
  * @author Joe Grandja
@@ -63,6 +58,7 @@ import org.springframework.util.StringUtils;
  * @see OAuth2AuthorizationCodeRequestAuthenticationToken
  * @see OAuth2AuthorizationCodeRequestAuthenticationValidator
  * @see OAuth2AuthorizationCodeAuthenticationProvider
+ * @see OAuth2AuthorizationConsentAuthenticationProvider
  * @see RegisteredClientRepository
  * @see OAuth2AuthorizationService
  * @see OAuth2AuthorizationConsentService
@@ -71,7 +67,6 @@ import org.springframework.util.StringUtils;
 public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implements AuthenticationProvider {
 	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());
 	private final RegisteredClientRepository registeredClientRepository;
@@ -80,7 +75,6 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
 	private OAuth2TokenGenerator<OAuth2AuthorizationCode> authorizationCodeGenerator = new OAuth2AuthorizationCodeGenerator();
 	private Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator =
 			new OAuth2AuthorizationCodeRequestAuthenticationValidator();
-	private Consumer<OAuth2AuthorizationConsentAuthenticationContext> authorizationConsentCustomizer;
 
 	/**
 	 * Constructs an {@code OAuth2AuthorizationCodeRequestAuthenticationProvider} using the provided parameters.
@@ -104,73 +98,6 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
 		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
 				(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
 
-		return authorizationCodeRequestAuthentication.isConsent() ?
-				authenticateAuthorizationConsent(authentication) :
-				authenticateAuthorizationRequest(authentication);
-	}
-
-	@Override
-	public boolean supports(Class<?> authentication) {
-		return OAuth2AuthorizationCodeRequestAuthenticationToken.class.isAssignableFrom(authentication);
-	}
-
-	/**
-	 * Sets the {@link OAuth2TokenGenerator} that generates the {@link OAuth2AuthorizationCode}.
-	 *
-	 * @param authorizationCodeGenerator the {@link OAuth2TokenGenerator} that generates the {@link OAuth2AuthorizationCode}
-	 * @since 0.2.3
-	 */
-	public void setAuthorizationCodeGenerator(OAuth2TokenGenerator<OAuth2AuthorizationCode> authorizationCodeGenerator) {
-		Assert.notNull(authorizationCodeGenerator, "authorizationCodeGenerator cannot be null");
-		this.authorizationCodeGenerator = authorizationCodeGenerator;
-	}
-
-	/**
-	 * Sets the {@code Consumer} providing access to the {@link OAuth2AuthorizationCodeRequestAuthenticationContext}
-	 * and is responsible for validating specific OAuth 2.0 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 Authorization Request parameters
-	 * @since 0.4.0
-	 */
-	public void setAuthenticationValidator(Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator) {
-		Assert.notNull(authenticationValidator, "authenticationValidator cannot be null");
-		this.authenticationValidator = authenticationValidator;
-	}
-
-	/**
-	 * Sets the {@code Consumer} providing access to the {@link OAuth2AuthorizationConsentAuthenticationContext}
-	 * containing an {@link OAuth2AuthorizationConsent.Builder} and additional context information.
-	 *
-	 * <p>
-	 * The following context attributes are available:
-	 * <ul>
-	 * <li>The {@link OAuth2AuthorizationConsent.Builder} used to build the authorization consent
-	 * prior to {@link OAuth2AuthorizationConsentService#save(OAuth2AuthorizationConsent)}.</li>
-	 * <li>The {@link Authentication} of type
-	 * {@link OAuth2AuthorizationCodeRequestAuthenticationToken}.</li>
-	 * <li>The {@link RegisteredClient} associated with the authorization request.</li>
-	 * <li>The {@link OAuth2Authorization} associated with the state token presented in the
-	 * authorization consent request.</li>
-	 * <li>The {@link OAuth2AuthorizationRequest} associated with the authorization consent request.</li>
-	 * </ul>
-	 *
-	 * @param authorizationConsentCustomizer the {@code Consumer} providing access to the
-	 * {@link OAuth2AuthorizationConsentAuthenticationContext} containing an {@link OAuth2AuthorizationConsent.Builder}
-	 */
-	public void setAuthorizationConsentCustomizer(Consumer<OAuth2AuthorizationConsentAuthenticationContext> authorizationConsentCustomizer) {
-		Assert.notNull(authorizationConsentCustomizer, "authorizationConsentCustomizer cannot be null");
-		this.authorizationConsentCustomizer = authorizationConsentCustomizer;
-	}
-
-	private Authentication authenticateAuthorizationRequest(Authentication authentication) throws AuthenticationException {
-		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
-				(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
-
 		RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(
 				authorizationCodeRequestAuthentication.getClientId());
 		if (registeredClient == null) {
@@ -234,12 +161,8 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
 			Set<String> currentAuthorizedScopes = currentAuthorizationConsent != null ?
 					currentAuthorizationConsent.getScopes() : null;
 
-			return OAuth2AuthorizationCodeRequestAuthenticationToken.with(registeredClient.getClientId(), principal)
-					.authorizationUri(authorizationRequest.getAuthorizationUri())
-					.scopes(currentAuthorizedScopes)
-					.state(state)
-					.consentRequired(true)
-					.build();
+			return new OAuth2AuthorizationConsentAuthenticationToken(authorizationRequest.getAuthorizationUri(),
+					registeredClient.getClientId(), principal, state, currentAuthorizedScopes, null);
 		}
 
 		OAuth2TokenContext tokenContext = createAuthorizationCodeTokenContext(
@@ -262,136 +185,42 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
 			redirectUri = registeredClient.getRedirectUris().iterator().next();
 		}
 
-		return OAuth2AuthorizationCodeRequestAuthenticationToken.with(registeredClient.getClientId(), principal)
-				.authorizationUri(authorizationRequest.getAuthorizationUri())
-				.redirectUri(redirectUri)
-				.scopes(authorizationRequest.getScopes())
-				.state(authorizationRequest.getState())
-				.authorizationCode(authorizationCode)
-				.build();
+		return new OAuth2AuthorizationCodeRequestAuthenticationToken(authorizationRequest.getAuthorizationUri(),
+				registeredClient.getClientId(), principal, authorizationCode, redirectUri,
+				authorizationRequest.getState(), authorizationRequest.getScopes());
 	}
 
-	private Authentication authenticateAuthorizationConsent(Authentication authentication) throws AuthenticationException {
-		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
-				(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
-
-		OAuth2Authorization authorization = this.authorizationService.findByToken(
-				authorizationCodeRequestAuthentication.getState(), STATE_TOKEN_TYPE);
-		if (authorization == null) {
-			throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE,
-					authorizationCodeRequestAuthentication, null, null);
-		}
-
-		// The 'in-flight' authorization must be associated to the current principal
-		Authentication principal = (Authentication) authorizationCodeRequestAuthentication.getPrincipal();
-		if (!isPrincipalAuthenticated(principal) || !principal.getName().equals(authorization.getPrincipalName())) {
-			throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE,
-					authorizationCodeRequestAuthentication, null, null);
-		}
-
-		RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(
-				authorizationCodeRequestAuthentication.getClientId());
-		if (registeredClient == null || !registeredClient.getId().equals(authorization.getRegisteredClientId())) {
-			throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID,
-					authorizationCodeRequestAuthentication, registeredClient);
-		}
-
-		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
-		Set<String> requestedScopes = authorizationRequest.getScopes();
-		Set<String> authorizedScopes = new HashSet<>(authorizationCodeRequestAuthentication.getScopes());
-		if (!requestedScopes.containsAll(authorizedScopes)) {
-			throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE,
-					authorizationCodeRequestAuthentication, registeredClient, authorizationRequest);
-		}
-
-		OAuth2AuthorizationConsent currentAuthorizationConsent = this.authorizationConsentService.findById(
-				authorization.getRegisteredClientId(), authorization.getPrincipalName());
-		Set<String> currentAuthorizedScopes = currentAuthorizationConsent != null ?
-				currentAuthorizationConsent.getScopes() : Collections.emptySet();
-
-		if (!currentAuthorizedScopes.isEmpty()) {
-			for (String requestedScope : requestedScopes) {
-				if (currentAuthorizedScopes.contains(requestedScope)) {
-					authorizedScopes.add(requestedScope);
-				}
-			}
-		}
-
-		if (!authorizedScopes.isEmpty() && requestedScopes.contains(OidcScopes.OPENID)) {
-			// 'openid' scope is auto-approved as it does not require consent
-			authorizedScopes.add(OidcScopes.OPENID);
-		}
-
-		OAuth2AuthorizationConsent.Builder authorizationConsentBuilder;
-		if (currentAuthorizationConsent != null) {
-			authorizationConsentBuilder = OAuth2AuthorizationConsent.from(currentAuthorizationConsent);
-		} else {
-			authorizationConsentBuilder = OAuth2AuthorizationConsent.withId(
-					authorization.getRegisteredClientId(), authorization.getPrincipalName());
-		}
-		authorizedScopes.forEach(authorizationConsentBuilder::scope);
-
-		if (this.authorizationConsentCustomizer != null) {
-			// @formatter:off
-			OAuth2AuthorizationConsentAuthenticationContext authorizationConsentAuthenticationContext =
-					OAuth2AuthorizationConsentAuthenticationContext.with(authorizationCodeRequestAuthentication)
-							.authorizationConsent(authorizationConsentBuilder)
-							.registeredClient(registeredClient)
-							.authorization(authorization)
-							.authorizationRequest(authorizationRequest)
-							.build();
-			// @formatter:on
-			this.authorizationConsentCustomizer.accept(authorizationConsentAuthenticationContext);
-		}
-
-		Set<GrantedAuthority> authorities = new HashSet<>();
-		authorizationConsentBuilder.authorities(authorities::addAll);
-
-		if (authorities.isEmpty()) {
-			// Authorization consent denied (or revoked)
-			if (currentAuthorizationConsent != null) {
-				this.authorizationConsentService.remove(currentAuthorizationConsent);
-			}
-			this.authorizationService.remove(authorization);
-			throwError(OAuth2ErrorCodes.ACCESS_DENIED, OAuth2ParameterNames.CLIENT_ID,
-					authorizationCodeRequestAuthentication, registeredClient, authorizationRequest);
-		}
-
-		OAuth2AuthorizationConsent authorizationConsent = authorizationConsentBuilder.build();
-		if (!authorizationConsent.equals(currentAuthorizationConsent)) {
-			this.authorizationConsentService.save(authorizationConsent);
-		}
-
-		OAuth2TokenContext tokenContext = createAuthorizationCodeTokenContext(
-				authorizationCodeRequestAuthentication, registeredClient, authorization, authorizedScopes);
-		OAuth2AuthorizationCode authorizationCode = this.authorizationCodeGenerator.generate(tokenContext);
-		if (authorizationCode == null) {
-			OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
-					"The token generator failed to generate the authorization code.", ERROR_URI);
-			throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, null);
-		}
-
-		OAuth2Authorization updatedAuthorization = OAuth2Authorization.from(authorization)
-				.authorizedScopes(authorizedScopes)
-				.token(authorizationCode)
-				.attributes(attrs -> {
-					attrs.remove(OAuth2ParameterNames.STATE);
-				})
-				.build();
-		this.authorizationService.save(updatedAuthorization);
+	@Override
+	public boolean supports(Class<?> authentication) {
+		return OAuth2AuthorizationCodeRequestAuthenticationToken.class.isAssignableFrom(authentication);
+	}
 
-		String redirectUri = authorizationRequest.getRedirectUri();
-		if (!StringUtils.hasText(redirectUri)) {
-			redirectUri = registeredClient.getRedirectUris().iterator().next();
-		}
+	/**
+	 * Sets the {@link OAuth2TokenGenerator} that generates the {@link OAuth2AuthorizationCode}.
+	 *
+	 * @param authorizationCodeGenerator the {@link OAuth2TokenGenerator} that generates the {@link OAuth2AuthorizationCode}
+	 * @since 0.2.3
+	 */
+	public void setAuthorizationCodeGenerator(OAuth2TokenGenerator<OAuth2AuthorizationCode> authorizationCodeGenerator) {
+		Assert.notNull(authorizationCodeGenerator, "authorizationCodeGenerator cannot be null");
+		this.authorizationCodeGenerator = authorizationCodeGenerator;
+	}
 
-		return OAuth2AuthorizationCodeRequestAuthenticationToken.with(registeredClient.getClientId(), principal)
-				.authorizationUri(authorizationRequest.getAuthorizationUri())
-				.redirectUri(redirectUri)
-				.scopes(authorizedScopes)
-				.state(authorizationRequest.getState())
-				.authorizationCode(authorizationCode)
-				.build();
+	/**
+	 * Sets the {@code Consumer} providing access to the {@link OAuth2AuthorizationCodeRequestAuthenticationContext}
+	 * and is responsible for validating specific OAuth 2.0 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 Authorization Request parameters
+	 * @since 0.4.0
+	 */
+	public void setAuthenticationValidator(Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator) {
+		Assert.notNull(authenticationValidator, "authenticationValidator cannot be null");
+		this.authenticationValidator = authenticationValidator;
 	}
 
 	private static OAuth2Authorization.Builder authorizationBuilder(RegisteredClient registeredClient, Authentication principal,
@@ -454,14 +283,7 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
 	private static void throwError(String errorCode, String parameterName,
 			OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
 			RegisteredClient registeredClient) {
-		throwError(errorCode, parameterName, authorizationCodeRequestAuthentication, registeredClient, null);
-	}
-
-	private static void throwError(String errorCode, String parameterName,
-			OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
-			RegisteredClient registeredClient, OAuth2AuthorizationRequest authorizationRequest) {
-		throwError(errorCode, parameterName, ERROR_URI,
-				authorizationCodeRequestAuthentication, registeredClient, authorizationRequest);
+		throwError(errorCode, parameterName, ERROR_URI, authorizationCodeRequestAuthentication, registeredClient, null);
 	}
 
 	private static void throwError(String errorCode, String parameterName, String errorUri,
@@ -475,30 +297,19 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
 			OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
 			RegisteredClient registeredClient, OAuth2AuthorizationRequest authorizationRequest) {
 
-		boolean redirectOnError = true;
+		String redirectUri = resolveRedirectUri(authorizationRequest, registeredClient);
 		if (error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_REQUEST) &&
 				(parameterName.equals(OAuth2ParameterNames.CLIENT_ID) ||
 						parameterName.equals(OAuth2ParameterNames.STATE))) {
-			redirectOnError = false;
+			redirectUri = null;		// Prevent redirects
 		}
 
-		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult = authorizationCodeRequestAuthentication;
-
-		if (redirectOnError && !StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) {
-			String redirectUri = resolveRedirectUri(authorizationRequest, registeredClient);
-			String state = authorizationCodeRequestAuthentication.isConsent() && authorizationRequest != null ?
-					authorizationRequest.getState() : authorizationCodeRequestAuthentication.getState();
-			authorizationCodeRequestAuthenticationResult = from(authorizationCodeRequestAuthentication)
-					.redirectUri(redirectUri)
-					.state(state)
-					.build();
-		} else if (!redirectOnError && StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) {
-			authorizationCodeRequestAuthenticationResult = from(authorizationCodeRequestAuthentication)
-					.redirectUri(null)		// Prevent redirects
-					.build();
-		}
-
-		authorizationCodeRequestAuthenticationResult.setAuthenticated(authorizationCodeRequestAuthentication.isAuthenticated());
+		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult =
+				new OAuth2AuthorizationCodeRequestAuthenticationToken(
+						authorizationCodeRequestAuthentication.getAuthorizationUri(), authorizationCodeRequestAuthentication.getClientId(),
+						(Authentication) authorizationCodeRequestAuthentication.getPrincipal(), redirectUri,
+						authorizationCodeRequestAuthentication.getState(), authorizationCodeRequestAuthentication.getScopes(),
+						authorizationCodeRequestAuthentication.getAdditionalParameters());
 
 		throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, authorizationCodeRequestAuthenticationResult);
 	}
@@ -513,32 +324,4 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
 		return null;
 	}
 
-	private static OAuth2AuthorizationCodeRequestAuthenticationToken.Builder from(OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication) {
-		return OAuth2AuthorizationCodeRequestAuthenticationToken.with(authorizationCodeRequestAuthentication.getClientId(), (Authentication) authorizationCodeRequestAuthentication.getPrincipal())
-				.authorizationUri(authorizationCodeRequestAuthentication.getAuthorizationUri())
-				.redirectUri(authorizationCodeRequestAuthentication.getRedirectUri())
-				.scopes(authorizationCodeRequestAuthentication.getScopes())
-				.state(authorizationCodeRequestAuthentication.getState())
-				.additionalParameters(authorizationCodeRequestAuthentication.getAdditionalParameters())
-				.authorizationCode(authorizationCodeRequestAuthentication.getAuthorizationCode());
-	}
-
-	private static class OAuth2AuthorizationCodeGenerator implements OAuth2TokenGenerator<OAuth2AuthorizationCode> {
-		private final StringKeyGenerator authorizationCodeGenerator =
-				new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
-
-		@Nullable
-		@Override
-		public OAuth2AuthorizationCode generate(OAuth2TokenContext context) {
-			if (context.getTokenType() == null ||
-					!OAuth2ParameterNames.CODE.equals(context.getTokenType().getValue())) {
-				return null;
-			}
-			Instant issuedAt = Instant.now();
-			Instant expiresAt = issuedAt.plus(context.getRegisteredClient().getTokenSettings().getAuthorizationCodeTimeToLive());
-			return new OAuth2AuthorizationCode(this.authorizationCodeGenerator.generateKey(), issuedAt, expiresAt);
-		}
-
-	}
-
 }

+ 83 - 209
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationToken.java

@@ -15,45 +15,104 @@
  */
 package org.springframework.security.oauth2.server.authorization.authentication;
 
-import java.io.Serializable;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 
-import org.springframework.lang.NonNull;
 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;
-import org.springframework.util.CollectionUtils;
 
 /**
- * An {@link Authentication} implementation for the OAuth 2.0 Authorization Request (and Consent)
+ * An {@link Authentication} implementation for the OAuth 2.0 Authorization Request
  * used in the Authorization Code Grant.
  *
  * @author Joe Grandja
  * @since 0.1.2
  * @see OAuth2AuthorizationCodeRequestAuthenticationProvider
+ * @see OAuth2AuthorizationConsentAuthenticationProvider
  */
-public final class OAuth2AuthorizationCodeRequestAuthenticationToken extends AbstractAuthenticationToken {
+public class OAuth2AuthorizationCodeRequestAuthenticationToken extends AbstractAuthenticationToken {
 	private static final long serialVersionUID = SpringAuthorizationServerVersion.SERIAL_VERSION_UID;
-	private String authorizationUri;
-	private String clientId;
-	private Authentication principal;
-	private String redirectUri;
-	private Set<String> scopes;
-	private String state;
-	private Map<String, Object> additionalParameters;
-	private boolean consentRequired;
-	private boolean consent;
-	private OAuth2AuthorizationCode authorizationCode;
+	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;
+	private final OAuth2AuthorizationCode authorizationCode;
 
-	private OAuth2AuthorizationCodeRequestAuthenticationToken() {
+	/**
+	 * Constructs an {@code OAuth2AuthorizationCodeRequestAuthenticationToken} using the provided parameters.
+	 *
+	 * @param authorizationUri the authorization URI
+	 * @param clientId the client identifier
+	 * @param principal the {@code Principal} (Resource Owner)
+	 * @param redirectUri the redirect uri
+	 * @param state the state
+	 * @param scopes the requested scope(s)
+	 * @param additionalParameters the additional parameters
+	 * @since 0.4.0
+	 */
+	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());
+		this.authorizationCode = null;
+	}
+
+	/**
+	 * Constructs an {@code OAuth2AuthorizationCodeRequestAuthenticationToken} using the provided parameters.
+	 *
+	 * @param authorizationUri the authorization URI
+	 * @param clientId the client identifier
+	 * @param principal the {@code Principal} (Resource Owner)
+	 * @param authorizationCode the {@link OAuth2AuthorizationCode}
+	 * @param redirectUri the redirect uri
+	 * @param state the state
+	 * @param scopes the authorized scope(s)
+	 * @since 0.4.0
+	 */
+	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");
+		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
@@ -94,15 +153,6 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationToken extends Abs
 		return this.redirectUri;
 	}
 
-	/**
-	 * 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 state.
 	 *
@@ -114,31 +164,21 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationToken extends Abs
 	}
 
 	/**
-	 * Returns the additional parameters.
-	 *
-	 * @return the additional parameters
-	 */
-	public Map<String, Object> getAdditionalParameters() {
-		return this.additionalParameters;
-	}
-
-	/**
-	 * Returns {@code true} if authorization consent is required, {@code false} otherwise.
+	 * Returns the requested (or authorized) scope(s).
 	 *
-	 * @return {@code true} if authorization consent is required, {@code false} otherwise
+	 * @return the requested (or authorized) scope(s), or an empty {@code Set} if not available
 	 */
-	public boolean isConsentRequired() {
-		return this.consentRequired;
+	public Set<String> getScopes() {
+		return this.scopes;
 	}
 
 	/**
-	 * Returns {@code true} if this {@code Authentication} represents an authorization consent request,
-	 * {@code false} otherwise.
+	 * Returns the additional parameters.
 	 *
-	 * @return {@code true} if this {@code Authentication} represents an authorization consent request, {@code false} otherwise
+	 * @return the additional parameters, or an empty {@code Map} if not available
 	 */
-	public boolean isConsent() {
-		return this.consent;
+	public Map<String, Object> getAdditionalParameters() {
+		return this.additionalParameters;
 	}
 
 	/**
@@ -151,170 +191,4 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationToken extends Abs
 		return this.authorizationCode;
 	}
 
-	/**
-	 * Returns a new {@link Builder}, initialized with the given client identifier
-	 * and {@code Principal} (Resource Owner).
-	 *
-	 * @param clientId the client identifier
-	 * @param principal the {@code Principal} (Resource Owner)
-	 * @return the {@link Builder}
-	 */
-	public static Builder with(@NonNull String clientId, @NonNull Authentication principal) {
-		Assert.hasText(clientId, "clientId cannot be empty");
-		Assert.notNull(principal, "principal cannot be null");
-		return new Builder(clientId, principal);
-	}
-
-	/**
-	 * A builder for {@link OAuth2AuthorizationCodeRequestAuthenticationToken}.
-	 */
-	public static final class Builder implements Serializable {
-		private static final long serialVersionUID = SpringAuthorizationServerVersion.SERIAL_VERSION_UID;
-		private String authorizationUri;
-		private String clientId;
-		private Authentication principal;
-		private String redirectUri;
-		private Set<String> scopes;
-		private String state;
-		private Map<String, Object> additionalParameters;
-		private boolean consentRequired;
-		private boolean consent;
-		private OAuth2AuthorizationCode authorizationCode;
-
-		private Builder(String clientId, Authentication principal) {
-			this.clientId = clientId;
-			this.principal = principal;
-		}
-
-		/**
-		 * Sets the authorization URI.
-		 *
-		 * @param authorizationUri the authorization URI
-		 * @return the {@link Builder}
-		 */
-		public Builder authorizationUri(String authorizationUri) {
-			this.authorizationUri = authorizationUri;
-			return this;
-		}
-
-		/**
-		 * Sets the redirect uri.
-		 *
-		 * @param redirectUri the redirect uri
-		 * @return the {@link Builder}
-		 */
-		public Builder redirectUri(String redirectUri) {
-			this.redirectUri = redirectUri;
-			return this;
-		}
-
-		/**
-		 * Sets the requested (or authorized) scope(s).
-		 *
-		 * @param scopes the requested (or authorized) scope(s)
-		 * @return the {@link Builder}
-		 */
-		public Builder scopes(Set<String> scopes) {
-			if (scopes != null) {
-				this.scopes = new HashSet<>(scopes);
-			}
-			return this;
-		}
-
-		/**
-		 * Sets the state.
-		 *
-		 * @param state the state
-		 * @return the {@link Builder}
-		 */
-		public Builder state(String state) {
-			this.state = state;
-			return this;
-		}
-
-		/**
-		 * Sets the additional parameters.
-		 *
-		 * @param additionalParameters the additional parameters
-		 * @return the {@link Builder}
-		 */
-		public Builder additionalParameters(Map<String, Object> additionalParameters) {
-			if (additionalParameters != null) {
-				this.additionalParameters = new HashMap<>(additionalParameters);
-			}
-			return this;
-		}
-
-		/**
-		 * Set to {@code true} if authorization consent is required, {@code false} otherwise.
-		 *
-		 * @param consentRequired {@code true} if authorization consent is required, {@code false} otherwise
-		 * @return the {@link Builder}
-		 */
-		public Builder consentRequired(boolean consentRequired) {
-			this.consentRequired = consentRequired;
-			return this;
-		}
-
-		/**
-		 * Set to {@code true} if this {@code Authentication} represents an authorization consent request, {@code false} otherwise.
-		 *
-		 * @param consent {@code true} if this {@code Authentication} represents an authorization consent request, {@code false} otherwise
-		 * @return the {@link Builder}
-		 */
-		public Builder consent(boolean consent) {
-			this.consent = consent;
-			return this;
-		}
-
-		/**
-		 * Sets the {@link OAuth2AuthorizationCode}.
-		 *
-		 * @param authorizationCode the {@link OAuth2AuthorizationCode}
-		 * @return the {@link Builder}
-		 */
-		public Builder authorizationCode(OAuth2AuthorizationCode authorizationCode) {
-			this.authorizationCode = authorizationCode;
-			return this;
-		}
-
-		/**
-		 * Builds a new {@link OAuth2AuthorizationCodeRequestAuthenticationToken}.
-		 *
-		 * @return the {@link OAuth2AuthorizationCodeRequestAuthenticationToken}
-		 */
-		public OAuth2AuthorizationCodeRequestAuthenticationToken build() {
-			Assert.hasText(this.authorizationUri, "authorizationUri cannot be empty");
-			if (this.consent) {
-				Assert.hasText(this.state, "state cannot be empty");
-			}
-
-			OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-					new OAuth2AuthorizationCodeRequestAuthenticationToken();
-
-			authentication.authorizationUri = this.authorizationUri;
-			authentication.clientId = this.clientId;
-			authentication.principal = this.principal;
-			authentication.redirectUri = this.redirectUri;
-			authentication.scopes = Collections.unmodifiableSet(
-					!CollectionUtils.isEmpty(this.scopes) ?
-							this.scopes :
-							Collections.emptySet());
-			authentication.state = this.state;
-			authentication.additionalParameters = Collections.unmodifiableMap(
-					!CollectionUtils.isEmpty(this.additionalParameters) ?
-							this.additionalParameters :
-							Collections.emptyMap());
-			authentication.consentRequired = this.consentRequired;
-			authentication.consent = this.consent;
-			authentication.authorizationCode = this.authorizationCode;
-			if (this.authorizationCode != null || this.consentRequired) {
-				authentication.setAuthenticated(true);
-			}
-
-			return authentication;
-		}
-
-	}
-
 }

+ 11 - 26
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationValidator.java

@@ -189,38 +189,23 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationValidator impleme
 			OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
 			RegisteredClient registeredClient) {
 
-		boolean redirectOnError = true;
+		String redirectUri = StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri()) ?
+				authorizationCodeRequestAuthentication.getRedirectUri() :
+				registeredClient.getRedirectUris().iterator().next();
 		if (error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_REQUEST) &&
 				parameterName.equals(OAuth2ParameterNames.REDIRECT_URI)) {
-			redirectOnError = false;
+			redirectUri = null;		// Prevent redirects
 		}
 
-		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult = authorizationCodeRequestAuthentication;
-
-		if (redirectOnError && !StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) {
-			String redirectUri = registeredClient.getRedirectUris().iterator().next();
-			authorizationCodeRequestAuthenticationResult = from(authorizationCodeRequestAuthentication)
-					.redirectUri(redirectUri)
-					.build();
-		} else if (!redirectOnError && StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) {
-			authorizationCodeRequestAuthenticationResult = from(authorizationCodeRequestAuthentication)
-					.redirectUri(null)		// Prevent redirects
-					.build();
-		}
-
-		authorizationCodeRequestAuthenticationResult.setAuthenticated(authorizationCodeRequestAuthentication.isAuthenticated());
+		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult =
+				new OAuth2AuthorizationCodeRequestAuthenticationToken(
+						authorizationCodeRequestAuthentication.getAuthorizationUri(), authorizationCodeRequestAuthentication.getClientId(),
+						(Authentication) authorizationCodeRequestAuthentication.getPrincipal(), redirectUri,
+						authorizationCodeRequestAuthentication.getState(), authorizationCodeRequestAuthentication.getScopes(),
+						authorizationCodeRequestAuthentication.getAdditionalParameters());
+		authorizationCodeRequestAuthenticationResult.setAuthenticated(true);
 
 		throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, authorizationCodeRequestAuthenticationResult);
 	}
 
-	private static OAuth2AuthorizationCodeRequestAuthenticationToken.Builder from(OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication) {
-		return OAuth2AuthorizationCodeRequestAuthenticationToken.with(authorizationCodeRequestAuthentication.getClientId(), (Authentication) authorizationCodeRequestAuthentication.getPrincipal())
-				.authorizationUri(authorizationCodeRequestAuthentication.getAuthorizationUri())
-				.redirectUri(authorizationCodeRequestAuthentication.getRedirectUri())
-				.scopes(authorizationCodeRequestAuthentication.getScopes())
-				.state(authorizationCodeRequestAuthentication.getState())
-				.additionalParameters(authorizationCodeRequestAuthentication.getAdditionalParameters())
-				.authorizationCode(authorizationCodeRequestAuthentication.getAuthorizationCode());
-	}
-
 }

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

@@ -36,7 +36,7 @@ import org.springframework.util.Assert;
  * @since 0.2.1
  * @see OAuth2AuthenticationContext
  * @see OAuth2AuthorizationConsent
- * @see OAuth2AuthorizationCodeRequestAuthenticationProvider#setAuthorizationConsentCustomizer(Consumer)
+ * @see OAuth2AuthorizationConsentAuthenticationProvider#setAuthorizationConsentCustomizer(Consumer)
  */
 public final class OAuth2AuthorizationConsentAuthenticationContext implements OAuth2AuthenticationContext {
 	private final Map<Object, Object> context;
@@ -95,12 +95,12 @@ public final class OAuth2AuthorizationConsentAuthenticationContext implements OA
 	}
 
 	/**
-	 * Constructs a new {@link Builder} with the provided {@link OAuth2AuthorizationCodeRequestAuthenticationToken}.
+	 * Constructs a new {@link Builder} with the provided {@link OAuth2AuthorizationConsentAuthenticationToken}.
 	 *
-	 * @param authentication the {@link OAuth2AuthorizationCodeRequestAuthenticationToken}
+	 * @param authentication the {@link OAuth2AuthorizationConsentAuthenticationToken}
 	 * @return the {@link Builder}
 	 */
-	public static Builder with(OAuth2AuthorizationCodeRequestAuthenticationToken authentication) {
+	public static Builder with(OAuth2AuthorizationConsentAuthenticationToken authentication) {
 		return new Builder(authentication);
 	}
 
@@ -109,7 +109,7 @@ public final class OAuth2AuthorizationConsentAuthenticationContext implements OA
 	 */
 	public static final class Builder extends AbstractBuilder<OAuth2AuthorizationConsentAuthenticationContext, Builder> {
 
-		private Builder(OAuth2AuthorizationCodeRequestAuthenticationToken authentication) {
+		private Builder(OAuth2AuthorizationConsentAuthenticationToken authentication) {
 			super(authentication);
 		}
 

+ 316 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationProvider.java

@@ -0,0 +1,316 @@
+/*
+ * Copyright 2020-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.server.authorization.authentication;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.function.Consumer;
+
+import org.springframework.security.authentication.AnonymousAuthenticationToken;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.GrantedAuthority;
+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.OAuth2AuthorizationRequest;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.core.oidc.OidcScopes;
+import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
+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.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
+import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
+import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
+import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * An {@link AuthenticationProvider} implementation for the OAuth 2.0 Authorization Consent
+ * used in the Authorization Code Grant.
+ *
+ * @author Joe Grandja
+ * @since 0.4.0
+ * @see OAuth2AuthorizationConsentAuthenticationToken
+ * @see OAuth2AuthorizationConsent
+ * @see OAuth2AuthorizationCodeRequestAuthenticationProvider
+ * @see RegisteredClientRepository
+ * @see OAuth2AuthorizationService
+ * @see OAuth2AuthorizationConsentService
+ */
+public final class OAuth2AuthorizationConsentAuthenticationProvider implements AuthenticationProvider {
+	private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1";
+	private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
+	private final RegisteredClientRepository registeredClientRepository;
+	private final OAuth2AuthorizationService authorizationService;
+	private final OAuth2AuthorizationConsentService authorizationConsentService;
+	private OAuth2TokenGenerator<OAuth2AuthorizationCode> authorizationCodeGenerator = new OAuth2AuthorizationCodeGenerator();
+	private Consumer<OAuth2AuthorizationConsentAuthenticationContext> authorizationConsentCustomizer;
+
+	/**
+	 * Constructs an {@code OAuth2AuthorizationConsentAuthenticationProvider} using the provided parameters.
+	 *
+	 * @param registeredClientRepository the repository of registered clients
+	 * @param authorizationService the authorization service
+	 * @param authorizationConsentService the authorization consent service
+	 */
+	public OAuth2AuthorizationConsentAuthenticationProvider(RegisteredClientRepository registeredClientRepository,
+			OAuth2AuthorizationService authorizationService, OAuth2AuthorizationConsentService authorizationConsentService) {
+		Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
+		Assert.notNull(authorizationService, "authorizationService cannot be null");
+		Assert.notNull(authorizationConsentService, "authorizationConsentService cannot be null");
+		this.registeredClientRepository = registeredClientRepository;
+		this.authorizationService = authorizationService;
+		this.authorizationConsentService = authorizationConsentService;
+	}
+
+	@Override
+	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+		OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthentication =
+				(OAuth2AuthorizationConsentAuthenticationToken) authentication;
+
+		OAuth2Authorization authorization = this.authorizationService.findByToken(
+				authorizationConsentAuthentication.getState(), STATE_TOKEN_TYPE);
+		if (authorization == null) {
+			throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE,
+					authorizationConsentAuthentication, null, null);
+		}
+
+		// The 'in-flight' authorization must be associated to the current principal
+		Authentication principal = (Authentication) authorizationConsentAuthentication.getPrincipal();
+		if (!isPrincipalAuthenticated(principal) || !principal.getName().equals(authorization.getPrincipalName())) {
+			throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE,
+					authorizationConsentAuthentication, null, null);
+		}
+
+		RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(
+				authorizationConsentAuthentication.getClientId());
+		if (registeredClient == null || !registeredClient.getId().equals(authorization.getRegisteredClientId())) {
+			throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID,
+					authorizationConsentAuthentication, registeredClient, null);
+		}
+
+		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
+		Set<String> requestedScopes = authorizationRequest.getScopes();
+		Set<String> authorizedScopes = new HashSet<>(authorizationConsentAuthentication.getScopes());
+		if (!requestedScopes.containsAll(authorizedScopes)) {
+			throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE,
+					authorizationConsentAuthentication, registeredClient, authorizationRequest);
+		}
+
+		OAuth2AuthorizationConsent currentAuthorizationConsent = this.authorizationConsentService.findById(
+				authorization.getRegisteredClientId(), authorization.getPrincipalName());
+		Set<String> currentAuthorizedScopes = currentAuthorizationConsent != null ?
+				currentAuthorizationConsent.getScopes() : Collections.emptySet();
+
+		if (!currentAuthorizedScopes.isEmpty()) {
+			for (String requestedScope : requestedScopes) {
+				if (currentAuthorizedScopes.contains(requestedScope)) {
+					authorizedScopes.add(requestedScope);
+				}
+			}
+		}
+
+		if (!authorizedScopes.isEmpty() && requestedScopes.contains(OidcScopes.OPENID)) {
+			// 'openid' scope is auto-approved as it does not require consent
+			authorizedScopes.add(OidcScopes.OPENID);
+		}
+
+		OAuth2AuthorizationConsent.Builder authorizationConsentBuilder;
+		if (currentAuthorizationConsent != null) {
+			authorizationConsentBuilder = OAuth2AuthorizationConsent.from(currentAuthorizationConsent);
+		} else {
+			authorizationConsentBuilder = OAuth2AuthorizationConsent.withId(
+					authorization.getRegisteredClientId(), authorization.getPrincipalName());
+		}
+		authorizedScopes.forEach(authorizationConsentBuilder::scope);
+
+		if (this.authorizationConsentCustomizer != null) {
+			// @formatter:off
+			OAuth2AuthorizationConsentAuthenticationContext authorizationConsentAuthenticationContext =
+					OAuth2AuthorizationConsentAuthenticationContext.with(authorizationConsentAuthentication)
+							.authorizationConsent(authorizationConsentBuilder)
+							.registeredClient(registeredClient)
+							.authorization(authorization)
+							.authorizationRequest(authorizationRequest)
+							.build();
+			// @formatter:on
+			this.authorizationConsentCustomizer.accept(authorizationConsentAuthenticationContext);
+		}
+
+		Set<GrantedAuthority> authorities = new HashSet<>();
+		authorizationConsentBuilder.authorities(authorities::addAll);
+
+		if (authorities.isEmpty()) {
+			// Authorization consent denied (or revoked)
+			if (currentAuthorizationConsent != null) {
+				this.authorizationConsentService.remove(currentAuthorizationConsent);
+			}
+			this.authorizationService.remove(authorization);
+			throwError(OAuth2ErrorCodes.ACCESS_DENIED, OAuth2ParameterNames.CLIENT_ID,
+					authorizationConsentAuthentication, registeredClient, authorizationRequest);
+		}
+
+		OAuth2AuthorizationConsent authorizationConsent = authorizationConsentBuilder.build();
+		if (!authorizationConsent.equals(currentAuthorizationConsent)) {
+			this.authorizationConsentService.save(authorizationConsent);
+		}
+
+		OAuth2TokenContext tokenContext = createAuthorizationCodeTokenContext(
+				authorizationConsentAuthentication, registeredClient, authorization, authorizedScopes);
+		OAuth2AuthorizationCode authorizationCode = this.authorizationCodeGenerator.generate(tokenContext);
+		if (authorizationCode == null) {
+			OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
+					"The token generator failed to generate the authorization code.", ERROR_URI);
+			throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, null);
+		}
+
+		OAuth2Authorization updatedAuthorization = OAuth2Authorization.from(authorization)
+				.authorizedScopes(authorizedScopes)
+				.token(authorizationCode)
+				.attributes(attrs -> {
+					attrs.remove(OAuth2ParameterNames.STATE);
+				})
+				.build();
+		this.authorizationService.save(updatedAuthorization);
+
+		String redirectUri = authorizationRequest.getRedirectUri();
+		if (!StringUtils.hasText(redirectUri)) {
+			redirectUri = registeredClient.getRedirectUris().iterator().next();
+		}
+
+		return new OAuth2AuthorizationCodeRequestAuthenticationToken(
+				authorizationRequest.getAuthorizationUri(), registeredClient.getClientId(), principal, authorizationCode,
+				redirectUri, authorizationRequest.getState(), authorizedScopes);
+	}
+
+	@Override
+	public boolean supports(Class<?> authentication) {
+		return OAuth2AuthorizationConsentAuthenticationToken.class.isAssignableFrom(authentication);
+	}
+
+	/**
+	 * Sets the {@link OAuth2TokenGenerator} that generates the {@link OAuth2AuthorizationCode}.
+	 *
+	 * @param authorizationCodeGenerator the {@link OAuth2TokenGenerator} that generates the {@link OAuth2AuthorizationCode}
+	 */
+	public void setAuthorizationCodeGenerator(OAuth2TokenGenerator<OAuth2AuthorizationCode> authorizationCodeGenerator) {
+		Assert.notNull(authorizationCodeGenerator, "authorizationCodeGenerator cannot be null");
+		this.authorizationCodeGenerator = authorizationCodeGenerator;
+	}
+
+	/**
+	 * Sets the {@code Consumer} providing access to the {@link OAuth2AuthorizationConsentAuthenticationContext}
+	 * containing an {@link OAuth2AuthorizationConsent.Builder} and additional context information.
+	 *
+	 * <p>
+	 * The following context attributes are available:
+	 * <ul>
+	 * <li>The {@link OAuth2AuthorizationConsent.Builder} used to build the authorization consent
+	 * prior to {@link OAuth2AuthorizationConsentService#save(OAuth2AuthorizationConsent)}.</li>
+	 * <li>The {@link Authentication} of type
+	 * {@link OAuth2AuthorizationConsentAuthenticationToken}.</li>
+	 * <li>The {@link RegisteredClient} associated with the authorization request.</li>
+	 * <li>The {@link OAuth2Authorization} associated with the state token presented in the
+	 * authorization consent request.</li>
+	 * <li>The {@link OAuth2AuthorizationRequest} associated with the authorization consent request.</li>
+	 * </ul>
+	 *
+	 * @param authorizationConsentCustomizer the {@code Consumer} providing access to the
+	 * {@link OAuth2AuthorizationConsentAuthenticationContext} containing an {@link OAuth2AuthorizationConsent.Builder}
+	 */
+	public void setAuthorizationConsentCustomizer(Consumer<OAuth2AuthorizationConsentAuthenticationContext> authorizationConsentCustomizer) {
+		Assert.notNull(authorizationConsentCustomizer, "authorizationConsentCustomizer cannot be null");
+		this.authorizationConsentCustomizer = authorizationConsentCustomizer;
+	}
+
+	private static OAuth2TokenContext createAuthorizationCodeTokenContext(
+			OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthentication,
+			RegisteredClient registeredClient, OAuth2Authorization authorization, Set<String> authorizedScopes) {
+
+		// @formatter:off
+		return DefaultOAuth2TokenContext.builder()
+				.registeredClient(registeredClient)
+				.principal((Authentication) authorizationConsentAuthentication.getPrincipal())
+				.authorization(authorization)
+				.authorizationServerContext(AuthorizationServerContextHolder.getContext())
+				.tokenType(new OAuth2TokenType(OAuth2ParameterNames.CODE))
+				.authorizedScopes(authorizedScopes)
+				.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
+				.authorizationGrant(authorizationConsentAuthentication)
+				.build();
+		// @formatter:on
+	}
+
+	private static boolean isPrincipalAuthenticated(Authentication principal) {
+		return principal != null &&
+				!AnonymousAuthenticationToken.class.isAssignableFrom(principal.getClass()) &&
+				principal.isAuthenticated();
+	}
+
+	private static void throwError(String errorCode, String parameterName,
+			OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthentication,
+			RegisteredClient registeredClient, OAuth2AuthorizationRequest authorizationRequest) {
+		OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, ERROR_URI);
+		throwError(error, parameterName, authorizationConsentAuthentication, registeredClient, authorizationRequest);
+	}
+
+	private static void throwError(OAuth2Error error, String parameterName,
+			OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthentication,
+			RegisteredClient registeredClient, OAuth2AuthorizationRequest authorizationRequest) {
+
+		String redirectUri = resolveRedirectUri(authorizationRequest, registeredClient);
+		if (error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_REQUEST) &&
+				(parameterName.equals(OAuth2ParameterNames.CLIENT_ID) ||
+						parameterName.equals(OAuth2ParameterNames.STATE))) {
+			redirectUri = null;		// Prevent redirects
+		}
+
+		String state = authorizationRequest != null ?
+				authorizationRequest.getState() :
+				authorizationConsentAuthentication.getState();
+		Set<String> requestedScopes = authorizationRequest != null ?
+				authorizationRequest.getScopes() :
+				authorizationConsentAuthentication.getScopes();
+
+		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult =
+				new OAuth2AuthorizationCodeRequestAuthenticationToken(
+						authorizationConsentAuthentication.getAuthorizationUri(), authorizationConsentAuthentication.getClientId(),
+						(Authentication) authorizationConsentAuthentication.getPrincipal(), redirectUri,
+						state, requestedScopes, null);
+
+		throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, authorizationCodeRequestAuthenticationResult);
+	}
+
+	private static String resolveRedirectUri(OAuth2AuthorizationRequest authorizationRequest, RegisteredClient registeredClient) {
+		if (authorizationRequest != null && StringUtils.hasText(authorizationRequest.getRedirectUri())) {
+			return authorizationRequest.getRedirectUri();
+		}
+		if (registeredClient != null) {
+			return registeredClient.getRedirectUris().iterator().next();
+		}
+		return null;
+	}
+
+}

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

@@ -0,0 +1,135 @@
+/*
+ * Copyright 2020-2022 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} implementation for the OAuth 2.0 Authorization Consent
+ * used in the Authorization Code Grant.
+ *
+ * @author Joe Grandja
+ * @since 0.4.0
+ * @see OAuth2AuthorizationConsentAuthenticationProvider
+ * @see OAuth2AuthorizationCodeRequestAuthenticationProvider
+ */
+public class OAuth2AuthorizationConsentAuthenticationToken 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 state;
+	private final Set<String> scopes;
+	private final Map<String, Object> additionalParameters;
+
+	/**
+	 * Constructs an {@code OAuth2AuthorizationConsentAuthenticationToken} using the provided parameters.
+	 *
+	 * @param authorizationUri the authorization URI
+	 * @param clientId the client identifier
+	 * @param principal the {@code Principal} (Resource Owner)
+	 * @param state the state
+	 * @param scopes the requested (or authorized) scope(s)
+	 * @param additionalParameters the additional parameters
+	 */
+	public OAuth2AuthorizationConsentAuthenticationToken(String authorizationUri, String clientId, Authentication principal,
+			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");
+		Assert.hasText(state, "state cannot be empty");
+		this.authorizationUri = authorizationUri;
+		this.clientId = clientId;
+		this.principal = principal;
+		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());
+		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 state.
+	 *
+	 * @return the state
+	 */
+	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;
+	}
+
+}

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

@@ -31,10 +31,13 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResp
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationProvider;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationProvider;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationToken;
 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.authentication.DelegatingAuthenticationConverter;
 import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeRequestAuthenticationConverter;
+import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationConsentAuthenticationConverter;
 import org.springframework.security.web.authentication.AuthenticationConverter;
 import org.springframework.security.web.authentication.AuthenticationFailureHandler;
 import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
@@ -72,7 +75,8 @@ public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2C
 
 	/**
 	 * Adds an {@link AuthenticationConverter} used when attempting to extract an Authorization Request (or Consent) from {@link HttpServletRequest}
-	 * to an instance of {@link OAuth2AuthorizationCodeRequestAuthenticationToken} used for authenticating the request.
+	 * to an instance of {@link OAuth2AuthorizationCodeRequestAuthenticationToken} or {@link OAuth2AuthorizationConsentAuthenticationToken}
+	 * used for authenticating the request.
 	 *
 	 * @param authorizationRequestConverter an {@link AuthenticationConverter} used when attempting to extract an Authorization Request (or Consent) from {@link HttpServletRequest}
 	 * @return the {@link OAuth2AuthorizationEndpointConfigurer} for further configuration
@@ -170,7 +174,7 @@ public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2C
 	 *
 	 * <ul>
 	 * <li>It must be an HTTP POST</li>
-	 * <li>It must be submitted to {@link AuthorizationServerSettings#getAuthorizationEndpoint()} ()}</li>
+	 * <li>It must be submitted to {@link AuthorizationServerSettings#getAuthorizationEndpoint()}</li>
 	 * <li>It must include the received {@code client_id} as an HTTP parameter</li>
 	 * <li>It must include the received {@code state} as an HTTP parameter</li>
 	 * <li>It must include the list of {@code scope}s the {@code Resource Owner}
@@ -242,6 +246,7 @@ public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2C
 		List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
 
 		authenticationConverters.add(new OAuth2AuthorizationCodeRequestAuthenticationConverter());
+		authenticationConverters.add(new OAuth2AuthorizationConsentAuthenticationConverter());
 
 		return authenticationConverters;
 	}
@@ -256,6 +261,13 @@ public final class OAuth2AuthorizationEndpointConfigurer extends AbstractOAuth2C
 						OAuth2ConfigurerUtils.getAuthorizationConsentService(httpSecurity));
 		authenticationProviders.add(authorizationCodeRequestAuthenticationProvider);
 
+		OAuth2AuthorizationConsentAuthenticationProvider authorizationConsentAuthenticationProvider =
+				new OAuth2AuthorizationConsentAuthenticationProvider(
+						OAuth2ConfigurerUtils.getRegisteredClientRepository(httpSecurity),
+						OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity),
+						OAuth2ConfigurerUtils.getAuthorizationConsentService(httpSecurity));
+		authenticationProviders.add(authorizationConsentAuthenticationProvider);
+
 		return authenticationProviders;
 	}
 

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

@@ -23,8 +23,12 @@ import java.util.Map;
 import org.springframework.security.config.Customizer;
 import org.springframework.security.config.annotation.ObjectPostProcessor;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+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.web.util.matcher.OrRequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.web.util.UriComponentsBuilder;
 
 /**
  * Configurer for OpenID Connect 1.0 support.
@@ -102,6 +106,25 @@ public final class OidcConfigurer extends AbstractOAuth2Configurer {
 
 	@Override
 	void configure(HttpSecurity httpSecurity) {
+		OidcClientRegistrationEndpointConfigurer clientRegistrationEndpointConfigurer =
+				getConfigurer(OidcClientRegistrationEndpointConfigurer.class);
+		if (clientRegistrationEndpointConfigurer != null) {
+			OidcProviderConfigurationEndpointConfigurer providerConfigurationEndpointConfigurer =
+					getConfigurer(OidcProviderConfigurationEndpointConfigurer.class);
+
+			providerConfigurationEndpointConfigurer
+					.addDefaultProviderConfigurationCustomizer((builder) -> {
+						AuthorizationServerContext authorizationServerContext = AuthorizationServerContextHolder.getContext();
+						String issuer = authorizationServerContext.getIssuer();
+						AuthorizationServerSettings authorizationServerSettings = authorizationServerContext.getAuthorizationServerSettings();
+
+						String clientRegistrationEndpoint = UriComponentsBuilder.fromUriString(issuer)
+								.path(authorizationServerSettings.getOidcClientRegistrationEndpoint()).build().toUriString();
+
+						builder.clientRegistrationEndpoint(clientRegistrationEndpoint);
+					});
+		}
+
 		this.configurers.values().forEach(configurer -> configurer.configure(httpSecurity));
 	}
 

+ 31 - 18
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilter.java

@@ -17,6 +17,7 @@ package org.springframework.security.oauth2.server.authorization.web;
 
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Set;
 
@@ -28,6 +29,7 @@ import jakarta.servlet.http.HttpServletResponse;
 import org.springframework.http.HttpMethod;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.authentication.AuthenticationDetailsSource;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.core.Authentication;
@@ -40,7 +42,11 @@ 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.OAuth2AuthorizationCodeRequestAuthenticationProvider;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationProvider;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter;
 import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeRequestAuthenticationConverter;
+import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationConsentAuthenticationConverter;
 import org.springframework.security.web.DefaultRedirectStrategy;
 import org.springframework.security.web.RedirectStrategy;
 import org.springframework.security.web.authentication.AuthenticationConverter;
@@ -61,7 +67,7 @@ import org.springframework.web.util.UriComponentsBuilder;
 
 /**
  * A {@code Filter} for the OAuth 2.0 Authorization Code Grant,
- * which handles the processing of the OAuth 2.0 Authorization Request (and Consent).
+ * which handles the processing of the OAuth 2.0 Authorization Request and Consent.
  *
  * @author Joe Grandja
  * @author Paurav Munshi
@@ -71,6 +77,7 @@ import org.springframework.web.util.UriComponentsBuilder;
  * @since 0.0.1
  * @see AuthenticationManager
  * @see OAuth2AuthorizationCodeRequestAuthenticationProvider
+ * @see OAuth2AuthorizationConsentAuthenticationProvider
  * @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1">Section 4.1 Authorization Code Grant</a>
  * @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1">Section 4.1.1 Authorization Request</a>
  * @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2">Section 4.1.2 Authorization Response</a>
@@ -110,7 +117,10 @@ public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilte
 		Assert.hasText(authorizationEndpointUri, "authorizationEndpointUri cannot be empty");
 		this.authenticationManager = authenticationManager;
 		this.authorizationEndpointMatcher = createDefaultRequestMatcher(authorizationEndpointUri);
-		this.authenticationConverter = new OAuth2AuthorizationCodeRequestAuthenticationConverter();
+		this.authenticationConverter = new DelegatingAuthenticationConverter(
+				Arrays.asList(
+						new OAuth2AuthorizationCodeRequestAuthenticationConverter(),
+						new OAuth2AuthorizationConsentAuthenticationConverter()));
 	}
 
 	private static RequestMatcher createDefaultRequestMatcher(String authorizationEndpointUri) {
@@ -145,14 +155,14 @@ public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilte
 		}
 
 		try {
-			OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
-					(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationConverter.convert(request);
-			authorizationCodeRequestAuthentication.setDetails(this.authenticationDetailsSource.buildDetails(request));
-
-			OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult =
-					(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationManager.authenticate(authorizationCodeRequestAuthentication);
+			Authentication authentication = this.authenticationConverter.convert(request);
+			if (authentication instanceof AbstractAuthenticationToken) {
+				((AbstractAuthenticationToken) authentication)
+						.setDetails(this.authenticationDetailsSource.buildDetails(request));
+			}
+			Authentication authenticationResult = this.authenticationManager.authenticate(authentication);
 
-			if (!authorizationCodeRequestAuthenticationResult.isAuthenticated()) {
+			if (!authenticationResult.isAuthenticated()) {
 				// If the Principal (Resource Owner) is not authenticated then
 				// pass through the chain with the expectation that the authentication process
 				// will commence via AuthenticationEntryPoint
@@ -160,13 +170,15 @@ public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilte
 				return;
 			}
 
-			if (authorizationCodeRequestAuthenticationResult.isConsentRequired()) {
-				sendAuthorizationConsent(request, response, authorizationCodeRequestAuthentication, authorizationCodeRequestAuthenticationResult);
+			if (authenticationResult instanceof OAuth2AuthorizationConsentAuthenticationToken) {
+				sendAuthorizationConsent(request, response,
+						(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication,
+						(OAuth2AuthorizationConsentAuthenticationToken) authenticationResult);
 				return;
 			}
 
 			this.authenticationSuccessHandler.onAuthenticationSuccess(
-					request, response, authorizationCodeRequestAuthenticationResult);
+					request, response, authenticationResult);
 
 		} catch (OAuth2AuthenticationException ex) {
 			this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
@@ -186,7 +198,8 @@ public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilte
 
 	/**
 	 * Sets the {@link AuthenticationConverter} used when attempting to extract an Authorization Request (or Consent) from {@link HttpServletRequest}
-	 * to an instance of {@link OAuth2AuthorizationCodeRequestAuthenticationToken} used for authenticating the request.
+	 * to an instance of {@link OAuth2AuthorizationCodeRequestAuthenticationToken} or {@link OAuth2AuthorizationConsentAuthenticationToken}
+	 * used for authenticating the request.
 	 *
 	 * @param authenticationConverter the {@link AuthenticationConverter} used when attempting to extract an Authorization Request (or Consent) from {@link HttpServletRequest}
 	 */
@@ -229,13 +242,13 @@ public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilte
 
 	private void sendAuthorizationConsent(HttpServletRequest request, HttpServletResponse response,
 			OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
-			OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult) throws IOException {
+			OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthentication) throws IOException {
 
-		String clientId = authorizationCodeRequestAuthenticationResult.getClientId();
-		Authentication principal = (Authentication) authorizationCodeRequestAuthenticationResult.getPrincipal();
+		String clientId = authorizationConsentAuthentication.getClientId();
+		Authentication principal = (Authentication) authorizationConsentAuthentication.getPrincipal();
 		Set<String> requestedScopes = authorizationCodeRequestAuthentication.getScopes();
-		Set<String> authorizedScopes = authorizationCodeRequestAuthenticationResult.getScopes();
-		String state = authorizationCodeRequestAuthenticationResult.getState();
+		Set<String> authorizedScopes = authorizationConsentAuthentication.getScopes();
+		String state = authorizationConsentAuthentication.getState();
 
 		if (hasConsentUri()) {
 			String redirectUri = UriComponentsBuilder.fromUriString(resolveConsentUri(request))

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

@@ -43,7 +43,7 @@ import org.springframework.util.MultiValueMap;
 import org.springframework.util.StringUtils;
 
 /**
- * Attempts to extract an Authorization Request (or Consent) from {@link HttpServletRequest}
+ * 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 request.
  *
@@ -62,20 +62,19 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationConverter impleme
 
 	@Override
 	public Authentication convert(HttpServletRequest request) {
+		if (!"GET".equals(request.getMethod()) && !OIDC_REQUEST_MATCHER.matches(request)) {
+			return null;
+		}
+
 		MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
 
-		boolean authorizationRequest = false;
-		if ("GET".equals(request.getMethod()) || OIDC_REQUEST_MATCHER.matches(request)) {
-			authorizationRequest = true;
-
-			// response_type (REQUIRED)
-			String responseType = request.getParameter(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);
-			}
+		// response_type (REQUIRED)
+		String responseType = request.getParameter(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();
@@ -101,37 +100,21 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationConverter impleme
 
 		// scope (OPTIONAL)
 		Set<String> scopes = null;
-		if (authorizationRequest) {
-			String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
-			if (StringUtils.hasText(scope) &&
-					parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
-				throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE);
-			}
-			if (StringUtils.hasText(scope)) {
-				scopes = new HashSet<>(
-						Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
-			}
-		} else {
-			// Consent request
-			if (parameters.containsKey(OAuth2ParameterNames.SCOPE)) {
-				scopes = new HashSet<>(parameters.get(OAuth2ParameterNames.SCOPE));
-			}
+		String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
+		if (StringUtils.hasText(scope) &&
+				parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
+			throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE);
+		}
+		if (StringUtils.hasText(scope)) {
+			scopes = new HashSet<>(
+					Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
 		}
 
-		// state
-		// RECOMMENDED for Authorization Request
+		// state (RECOMMENDED)
 		String state = parameters.getFirst(OAuth2ParameterNames.STATE);
-		if (authorizationRequest) {
-			if (StringUtils.hasText(state) &&
-					parameters.get(OAuth2ParameterNames.STATE).size() != 1) {
-				throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE);
-			}
-		} else {
-			// REQUIRED for Authorization Consent Request
-			if (!StringUtils.hasText(state) ||
-					parameters.get(OAuth2ParameterNames.STATE).size() != 1) {
-				throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE);
-			}
+		if (StringUtils.hasText(state) &&
+				parameters.get(OAuth2ParameterNames.STATE).size() != 1) {
+			throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE);
 		}
 
 		// code_challenge (REQUIRED for public clients) - RFC 7636 (PKCE)
@@ -159,14 +142,8 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationConverter impleme
 			}
 		});
 
-		return OAuth2AuthorizationCodeRequestAuthenticationToken.with(clientId, principal)
-				.authorizationUri(authorizationUri)
-				.redirectUri(redirectUri)
-				.scopes(scopes)
-				.state(state)
-				.additionalParameters(additionalParameters)
-				.consent(!authorizationRequest)
-				.build();
+		return new OAuth2AuthorizationCodeRequestAuthenticationToken(authorizationUri, clientId, principal,
+				redirectUri, state, scopes, additionalParameters);
 	}
 
 	private static RequestMatcher createOidcRequestMatcher() {

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

@@ -0,0 +1,109 @@
+/*
+ * Copyright 2020-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.server.authorization.web.authentication;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.springframework.security.authentication.AnonymousAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter;
+import org.springframework.security.web.authentication.AuthenticationConverter;
+import org.springframework.util.MultiValueMap;
+import org.springframework.util.StringUtils;
+
+/**
+ * Attempts to extract an Authorization Consent from {@link HttpServletRequest}
+ * for the OAuth 2.0 Authorization Code Grant and then converts it to
+ * an {@link OAuth2AuthorizationConsentAuthenticationToken} used for authenticating the request.
+ *
+ * @author Joe Grandja
+ * @since 0.4.0
+ * @see AuthenticationConverter
+ * @see OAuth2AuthorizationConsentAuthenticationToken
+ * @see OAuth2AuthorizationEndpointFilter
+ */
+public final class OAuth2AuthorizationConsentAuthenticationConverter implements AuthenticationConverter {
+	private static final String DEFAULT_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1";
+	private static final Authentication ANONYMOUS_AUTHENTICATION = new AnonymousAuthenticationToken(
+			"anonymous", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
+
+	@Override
+	public Authentication convert(HttpServletRequest request) {
+		if (!"POST".equals(request.getMethod()) ||
+				request.getParameter(OAuth2ParameterNames.RESPONSE_TYPE) != null) {
+			return null;
+		}
+
+		MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
+
+		String authorizationUri = request.getRequestURL().toString();
+
+		// client_id (REQUIRED)
+		String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID);
+		if (!StringUtils.hasText(clientId) ||
+				parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {
+			throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID);
+		}
+
+		Authentication principal = SecurityContextHolder.getContext().getAuthentication();
+		if (principal == null) {
+			principal = ANONYMOUS_AUTHENTICATION;
+		}
+
+		// state (REQUIRED)
+		String state = parameters.getFirst(OAuth2ParameterNames.STATE);
+		if (!StringUtils.hasText(state) ||
+				parameters.get(OAuth2ParameterNames.STATE).size() != 1) {
+			throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE);
+		}
+
+		// scope (OPTIONAL)
+		Set<String> scopes = null;
+		if (parameters.containsKey(OAuth2ParameterNames.SCOPE)) {
+			scopes = new HashSet<>(parameters.get(OAuth2ParameterNames.SCOPE));
+		}
+
+		Map<String, Object> additionalParameters = new HashMap<>();
+		parameters.forEach((key, value) -> {
+			if (!key.equals(OAuth2ParameterNames.CLIENT_ID) &&
+					!key.equals(OAuth2ParameterNames.STATE) &&
+					!key.equals(OAuth2ParameterNames.SCOPE)) {
+				additionalParameters.put(key, value.get(0));
+			}
+		});
+
+		return new OAuth2AuthorizationConsentAuthenticationToken(authorizationUri, clientId, principal,
+				state, scopes, additionalParameters);
+	}
+
+	private static void throwError(String errorCode, String parameterName) {
+		OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, DEFAULT_ERROR_URI);
+		throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, null);
+	}
+
+}

+ 68 - 498
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProviderTests.java

@@ -18,7 +18,6 @@ package org.springframework.security.oauth2.server.authorization.authentication;
 import java.security.Principal;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.Consumer;
@@ -42,8 +41,6 @@ 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;
@@ -58,7 +55,6 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -69,7 +65,8 @@ import static org.mockito.Mockito.when;
  * @author Steve Riesenberg
  */
 public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
-	private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
+	private static final String AUTHORIZATION_URI = "https://provider.com/oauth2/authorize";
+	private static final String STATE = "state";
 	private RegisteredClientRepository registeredClientRepository;
 	private OAuth2AuthorizationService authorizationService;
 	private OAuth2AuthorizationConsentService authorizationConsentService;
@@ -132,19 +129,13 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
 				.hasMessage("authenticationValidator cannot be null");
 	}
 
-	@Test
-	public void setAuthorizationConsentCustomizerWhenNullThenThrowIllegalArgumentException() {
-		assertThatThrownBy(() -> this.authenticationProvider.setAuthorizationConsentCustomizer(null))
-				.isInstanceOf(IllegalArgumentException.class)
-				.hasMessage("authorizationConsentCustomizer cannot be null");
-	}
-
 	@Test
 	public void authenticateWhenInvalidClientIdThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
 		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
 		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-				authorizationCodeRequestAuthentication(registeredClient, this.principal)
-						.build();
+				new OAuth2AuthorizationCodeRequestAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes(), null);
 		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
 				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
 				.satisfies(ex ->
@@ -160,9 +151,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
 		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
 				.thenReturn(registeredClient);
 		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-				authorizationCodeRequestAuthentication(registeredClient, this.principal)
-						.redirectUri("https:///invalid")
-						.build();
+				new OAuth2AuthorizationCodeRequestAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						"https:///invalid", STATE, registeredClient.getScopes(), null);
 		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
 				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
 				.satisfies(ex ->
@@ -178,9 +169,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
 		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
 				.thenReturn(registeredClient);
 		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-				authorizationCodeRequestAuthentication(registeredClient, this.principal)
-						.redirectUri("https://example.com#fragment")
-						.build();
+				new OAuth2AuthorizationCodeRequestAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						"https://example.com#fragment", STATE, registeredClient.getScopes(), null);
 		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
 				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
 				.satisfies(ex ->
@@ -196,9 +187,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
 		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
 				.thenReturn(registeredClient);
 		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-				authorizationCodeRequestAuthentication(registeredClient, this.principal)
-						.redirectUri("https://localhost:5000")
-						.build();
+				new OAuth2AuthorizationCodeRequestAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						"https://localhost:5000", STATE, registeredClient.getScopes(), null);
 		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
 				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
 				.satisfies(ex ->
@@ -216,9 +207,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
 		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
 				.thenReturn(registeredClient);
 		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-				authorizationCodeRequestAuthentication(registeredClient, this.principal)
-						.redirectUri("https://invalid-example.com")
-						.build();
+				new OAuth2AuthorizationCodeRequestAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						"https://invalid-example.com", STATE, registeredClient.getScopes(), null);
 		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
 				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
 				.satisfies(ex ->
@@ -236,9 +227,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
 		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
 				.thenReturn(registeredClient);
 		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-				authorizationCodeRequestAuthentication(registeredClient, this.principal)
-						.redirectUri("https://127.0.0.1:5000")
-						.build();
+				new OAuth2AuthorizationCodeRequestAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						"https://127.0.0.1:5000", STATE, registeredClient.getScopes(), null);
 
 		OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult =
 				(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication);
@@ -255,9 +246,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
 		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
 				.thenReturn(registeredClient);
 		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-				authorizationCodeRequestAuthentication(registeredClient, this.principal)
-						.redirectUri("https://[::1]:5000")
-						.build();
+				new OAuth2AuthorizationCodeRequestAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						"https://[::1]:5000", STATE, registeredClient.getScopes(), null);
 
 		OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult =
 				(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication);
@@ -271,9 +262,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
 		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
 				.thenReturn(registeredClient);
 		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-				authorizationCodeRequestAuthentication(registeredClient, this.principal)
-						.redirectUri(null)
-						.build();
+				new OAuth2AuthorizationCodeRequestAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						null, STATE, registeredClient.getScopes(), null);
 		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
 				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
 				.satisfies(ex ->
@@ -291,9 +282,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
 		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
 				.thenReturn(registeredClient);
 		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-				authorizationCodeRequestAuthentication(registeredClient, this.principal)
-						.redirectUri(null)
-						.build();
+				new OAuth2AuthorizationCodeRequestAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						null, STATE, registeredClient.getScopes(), null);
 		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
 				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
 				.satisfies(ex ->
@@ -311,8 +302,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
 		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
 				.thenReturn(registeredClient);
 		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-				authorizationCodeRequestAuthentication(registeredClient, this.principal)
-						.build();
+				new OAuth2AuthorizationCodeRequestAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes(), null);
 		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
 				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
 				.satisfies(ex ->
@@ -328,9 +320,10 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
 		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
 				.thenReturn(registeredClient);
 		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-				authorizationCodeRequestAuthentication(registeredClient, this.principal)
-						.scopes(Collections.singleton("invalid-scope"))
-						.build();
+				new OAuth2AuthorizationCodeRequestAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						registeredClient.getRedirectUris().iterator().next(), STATE,
+						Collections.singleton("invalid-scope"), null);
 		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
 				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
 				.satisfies(ex ->
@@ -347,8 +340,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
 		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
 				.thenReturn(registeredClient);
 		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-				authorizationCodeRequestAuthentication(registeredClient, this.principal)
-						.build();
+				new OAuth2AuthorizationCodeRequestAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes(), null);
 		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
 				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
 				.satisfies(ex ->
@@ -366,9 +360,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
 		additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, "code-challenge");
 		additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "unsupported");
 		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-				authorizationCodeRequestAuthentication(registeredClient, this.principal)
-						.additionalParameters(additionalParameters)
-						.build();
+				new OAuth2AuthorizationCodeRequestAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes(), additionalParameters);
 		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
 				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
 				.satisfies(ex ->
@@ -386,9 +380,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
 		Map<String, Object> additionalParameters = new HashMap<>();
 		additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, "code-challenge");
 		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-				authorizationCodeRequestAuthentication(registeredClient, this.principal)
-						.additionalParameters(additionalParameters)
-						.build();
+				new OAuth2AuthorizationCodeRequestAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes(), additionalParameters);
 		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
 				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
 				.satisfies(ex ->
@@ -405,8 +399,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
 		this.principal.setAuthenticated(false);
 
 		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-				authorizationCodeRequestAuthentication(registeredClient, this.principal)
-						.build();
+				new OAuth2AuthorizationCodeRequestAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes(), null);
 
 		OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult =
 				(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication);
@@ -424,11 +419,12 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
 				.thenReturn(registeredClient);
 
 		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-				authorizationCodeRequestAuthentication(registeredClient, this.principal)
-						.build();
+				new OAuth2AuthorizationCodeRequestAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes(), null);
 
-		OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult =
-				(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication);
+		OAuth2AuthorizationConsentAuthenticationToken authenticationResult =
+				(OAuth2AuthorizationConsentAuthenticationToken) this.authenticationProvider.authenticate(authentication);
 
 		ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
 		verify(this.authorizationService).save(authorizationCaptor.capture());
@@ -457,8 +453,6 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
 		assertThat(authenticationResult.getAuthorizationUri()).isEqualTo(authorizationRequest.getAuthorizationUri());
 		assertThat(authenticationResult.getScopes()).isEmpty();
 		assertThat(authenticationResult.getState()).isEqualTo(state);
-		assertThat(authenticationResult.isConsentRequired()).isTrue();
-		assertThat(authenticationResult.getAuthorizationCode()).isNull();
 		assertThat(authenticationResult.isAuthenticated()).isTrue();
 	}
 
@@ -475,8 +469,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
 				.thenReturn(registeredClient);
 
 		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-				authorizationCodeRequestAuthentication(registeredClient, this.principal)
-						.build();
+				new OAuth2AuthorizationCodeRequestAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes(), null);
 
 		OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult =
 				(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication);
@@ -500,8 +495,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
 				.thenReturn(previousAuthorizationConsent);
 
 		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-				authorizationCodeRequestAuthentication(registeredClient, this.principal)
-						.build();
+				new OAuth2AuthorizationCodeRequestAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes(), null);
 
 		OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult =
 				(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication);
@@ -519,9 +515,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
 		additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, "code-challenge");
 		additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
 		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-				authorizationCodeRequestAuthentication(registeredClient, this.principal)
-						.additionalParameters(additionalParameters)
-						.build();
+				new OAuth2AuthorizationCodeRequestAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes(), additionalParameters);
 
 		OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult =
 				(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication);
@@ -540,8 +536,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
 		this.authenticationProvider.setAuthorizationCodeGenerator(authorizationCodeGenerator);
 
 		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-				authorizationCodeRequestAuthentication(registeredClient, this.principal)
-						.build();
+				new OAuth2AuthorizationCodeRequestAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes(), null);
 
 		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
 				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
@@ -563,8 +560,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
 		this.authenticationProvider.setAuthenticationValidator(authenticationValidator);
 
 		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-				authorizationCodeRequestAuthentication(registeredClient, this.principal)
-						.build();
+				new OAuth2AuthorizationCodeRequestAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes(), null);
 
 		OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult =
 				(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication);
@@ -611,410 +609,6 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
 		assertThat(authenticationResult.isAuthenticated()).isTrue();
 	}
 
-	@Test
-	public void authenticateWhenConsentRequestInvalidStateThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
-		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
-				.build();
-		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-				authorizationConsentRequestAuthentication(registeredClient, this.principal)
-						.build();
-		when(this.authorizationService.findByToken(eq(authentication.getState()), eq(STATE_TOKEN_TYPE)))
-				.thenReturn(null);
-
-		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
-				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
-				.satisfies(ex ->
-						assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
-								OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE, null)
-				);
-	}
-
-	@Test
-	public void authenticateWhenConsentRequestPrincipalNotAuthenticatedThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
-		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
-				.build();
-		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
-				.principalName(this.principal.getName())
-				.build();
-		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-				authorizationConsentRequestAuthentication(registeredClient, this.principal)
-						.build();
-		when(this.authorizationService.findByToken(eq(authentication.getState()), eq(STATE_TOKEN_TYPE)))
-				.thenReturn(authorization);
-		this.principal.setAuthenticated(false);
-
-		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
-				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
-				.satisfies(ex ->
-						assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
-								OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE, null)
-				);
-	}
-
-	@Test
-	public void authenticateWhenConsentRequestInvalidPrincipalThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
-		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
-				.build();
-		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
-				.principalName(this.principal.getName().concat("-other"))
-				.build();
-		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-				authorizationConsentRequestAuthentication(registeredClient, this.principal)
-						.build();
-		when(this.authorizationService.findByToken(eq(authentication.getState()), eq(STATE_TOKEN_TYPE)))
-				.thenReturn(authorization);
-
-		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
-				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
-				.satisfies(ex ->
-						assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
-								OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE, null)
-				);
-	}
-
-	@Test
-	public void authenticateWhenConsentRequestInvalidClientIdThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
-		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
-				.build();
-		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
-				.principalName(this.principal.getName())
-				.build();
-		when(this.authorizationService.findByToken(eq("state"), eq(STATE_TOKEN_TYPE)))
-				.thenReturn(authorization);
-		RegisteredClient otherRegisteredClient = TestRegisteredClients.registeredClient2()
-				.build();
-		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-				authorizationConsentRequestAuthentication(otherRegisteredClient, this.principal)
-						.state("state")
-						.build();
-
-		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
-				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
-				.satisfies(ex ->
-						assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
-								OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID, null)
-				);
-	}
-
-	@Test
-	public void authenticateWhenConsentRequestDoesNotMatchClientThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
-		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
-				.build();
-		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
-				.thenReturn(registeredClient);
-		RegisteredClient otherRegisteredClient = TestRegisteredClients.registeredClient2()
-				.build();
-		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(otherRegisteredClient)
-				.principalName(this.principal.getName())
-				.build();
-		when(this.authorizationService.findByToken(eq("state"), eq(STATE_TOKEN_TYPE)))
-				.thenReturn(authorization);
-		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-				authorizationConsentRequestAuthentication(registeredClient, this.principal)
-						.state("state")
-						.build();
-
-		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
-				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
-				.satisfies(ex ->
-						assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
-								OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID, null)
-				);
-	}
-
-	@Test
-	public void authenticateWhenConsentRequestScopeNotRequestedThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
-		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
-				.build();
-		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
-				.thenReturn(registeredClient);
-		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
-				.principalName(this.principal.getName())
-				.build();
-		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
-		Set<String> authorizedScopes = new HashSet<>(authorizationRequest.getScopes());
-		authorizedScopes.add("scope-not-requested");
-		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-				authorizationConsentRequestAuthentication(registeredClient, this.principal)
-						.scopes(authorizedScopes)
-						.build();
-		when(this.authorizationService.findByToken(eq(authentication.getState()), eq(STATE_TOKEN_TYPE)))
-				.thenReturn(authorization);
-
-		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
-				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
-				.satisfies(ex ->
-						assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
-								OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE, authorizationRequest.getRedirectUri())
-				);
-	}
-
-	@Test
-	public void authenticateWhenConsentRequestNotApprovedThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
-		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
-				.build();
-		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
-				.thenReturn(registeredClient);
-		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
-				.principalName(this.principal.getName())
-				.build();
-		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-				authorizationConsentRequestAuthentication(registeredClient, this.principal)
-						.scopes(new HashSet<>())	// No scopes approved
-						.build();
-		when(this.authorizationService.findByToken(eq(authentication.getState()), eq(STATE_TOKEN_TYPE)))
-				.thenReturn(authorization);
-
-		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
-
-		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
-				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
-				.satisfies(ex ->
-						assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
-								OAuth2ErrorCodes.ACCESS_DENIED, OAuth2ParameterNames.CLIENT_ID, authorizationRequest.getRedirectUri())
-				);
-
-		verify(this.authorizationService).remove(eq(authorization));
-	}
-
-	@Test
-	public void authenticateWhenConsentRequestApproveAllThenReturnAuthorizationCode() {
-		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
-				.build();
-		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
-				.thenReturn(registeredClient);
-		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
-				.principalName(this.principal.getName())
-				.build();
-		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
-		Set<String> authorizedScopes = authorizationRequest.getScopes();
-		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-				authorizationConsentRequestAuthentication(registeredClient, this.principal)
-						.scopes(authorizedScopes)		// Approve all scopes
-						.build();
-		when(this.authorizationService.findByToken(eq(authentication.getState()), eq(STATE_TOKEN_TYPE)))
-				.thenReturn(authorization);
-
-		OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult =
-				(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication);
-
-		assertAuthorizationConsentRequestWithAuthorizationCodeResult(registeredClient, authorization, authenticationResult);
-	}
-
-	@Test
-	public void authenticateWhenCustomAuthorizationConsentCustomizerThenUsed() {
-		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
-				.build();
-		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
-				.thenReturn(registeredClient);
-		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
-				.principalName(this.principal.getName())
-				.build();
-		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
-		Set<String> authorizedScopes = authorizationRequest.getScopes();
-		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-				authorizationConsentRequestAuthentication(registeredClient, this.principal)
-						.scopes(authorizedScopes)		// Approve all scopes
-						.build();
-		when(this.authorizationService.findByToken(eq(authentication.getState()), eq(STATE_TOKEN_TYPE)))
-				.thenReturn(authorization);
-
-		@SuppressWarnings("unchecked")
-		Consumer<OAuth2AuthorizationConsentAuthenticationContext> authorizationConsentCustomizer = mock(Consumer.class);
-		this.authenticationProvider.setAuthorizationConsentCustomizer(authorizationConsentCustomizer);
-
-		OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult =
-				(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication);
-
-		assertAuthorizationConsentRequestWithAuthorizationCodeResult(registeredClient, authorization, authenticationResult);
-
-		ArgumentCaptor<OAuth2AuthorizationConsentAuthenticationContext> authenticationContextCaptor =
-				ArgumentCaptor.forClass(OAuth2AuthorizationConsentAuthenticationContext.class);
-		verify(authorizationConsentCustomizer).accept(authenticationContextCaptor.capture());
-
-		OAuth2AuthorizationConsentAuthenticationContext authenticationContext = authenticationContextCaptor.getValue();
-		assertThat(authenticationContext.<Authentication>getAuthentication()).isEqualTo(authentication);
-		assertThat(authenticationContext.getAuthorizationConsent()).isNotNull();
-		assertThat(authenticationContext.getRegisteredClient()).isEqualTo(registeredClient);
-		assertThat(authenticationContext.getAuthorization()).isEqualTo(authorization);
-		assertThat(authenticationContext.getAuthorizationRequest()).isEqualTo(authorizationRequest);
-	}
-
-	private void assertAuthorizationConsentRequestWithAuthorizationCodeResult(
-			RegisteredClient registeredClient,
-			OAuth2Authorization authorization,
-			OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult) {
-		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
-		Set<String> authorizedScopes = authorizationRequest.getScopes();
-
-		ArgumentCaptor<OAuth2AuthorizationConsent> authorizationConsentCaptor = ArgumentCaptor.forClass(OAuth2AuthorizationConsent.class);
-		verify(this.authorizationConsentService).save(authorizationConsentCaptor.capture());
-		OAuth2AuthorizationConsent authorizationConsent = authorizationConsentCaptor.getValue();
-
-		assertThat(authorizationConsent.getRegisteredClientId()).isEqualTo(authorization.getRegisteredClientId());
-		assertThat(authorizationConsent.getPrincipalName()).isEqualTo(authorization.getPrincipalName());
-		assertThat(authorizationConsent.getAuthorities()).hasSize(authorizedScopes.size());
-		assertThat(authorizationConsent.getScopes()).containsExactlyInAnyOrderElementsOf(authorizedScopes);
-
-		ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
-		verify(this.authorizationService).save(authorizationCaptor.capture());
-		OAuth2Authorization updatedAuthorization = authorizationCaptor.getValue();
-
-		assertThat(updatedAuthorization.getRegisteredClientId()).isEqualTo(authorization.getRegisteredClientId());
-		assertThat(updatedAuthorization.getPrincipalName()).isEqualTo(authorization.getPrincipalName());
-		assertThat(updatedAuthorization.getAuthorizationGrantType()).isEqualTo(authorization.getAuthorizationGrantType());
-		assertThat(updatedAuthorization.<Authentication>getAttribute(Principal.class.getName()))
-				.isEqualTo(authorization.<Authentication>getAttribute(Principal.class.getName()));
-		assertThat(updatedAuthorization.<OAuth2AuthorizationRequest>getAttribute(OAuth2AuthorizationRequest.class.getName()))
-				.isEqualTo(authorizationRequest);
-		OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = updatedAuthorization.getToken(OAuth2AuthorizationCode.class);
-		assertThat(authorizationCode).isNotNull();
-		assertThat(updatedAuthorization.<String>getAttribute(OAuth2ParameterNames.STATE)).isNull();
-		assertThat(updatedAuthorization.getAuthorizedScopes()).isEqualTo(authorizedScopes);
-
-		assertThat(authenticationResult.getClientId()).isEqualTo(registeredClient.getClientId());
-		assertThat(authenticationResult.getPrincipal()).isEqualTo(this.principal);
-		assertThat(authenticationResult.getAuthorizationUri()).isEqualTo(authorizationRequest.getAuthorizationUri());
-		assertThat(authenticationResult.getRedirectUri()).isEqualTo(authorizationRequest.getRedirectUri());
-		assertThat(authenticationResult.getScopes()).isEqualTo(authorizedScopes);
-		assertThat(authenticationResult.getState()).isEqualTo(authorizationRequest.getState());
-		assertThat(authenticationResult.getAuthorizationCode()).isEqualTo(authorizationCode.getToken());
-		assertThat(authenticationResult.isAuthenticated()).isTrue();
-	}
-
-	@Test
-	public void authenticateWhenConsentRequestApproveNoneAndRevokePreviouslyApprovedThenAuthorizationConsentRemoved() {
-		String previouslyApprovedScope = "message.read";
-		String requestedScope = "message.write";
-		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
-				.scopes(scopes -> {
-					scopes.clear();
-					scopes.add(previouslyApprovedScope);
-					scopes.add(requestedScope);
-				})
-				.build();
-		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
-				.thenReturn(registeredClient);
-		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
-				.principalName(this.principal.getName())
-				.build();
-		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
-		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-				authorizationConsentRequestAuthentication(registeredClient, this.principal)
-						.scopes(new HashSet<>())	// No scopes approved
-						.build();
-		when(this.authorizationService.findByToken(eq(authentication.getState()), eq(STATE_TOKEN_TYPE)))
-				.thenReturn(authorization);
-		OAuth2AuthorizationConsent previousAuthorizationConsent =
-				OAuth2AuthorizationConsent.withId(authorization.getRegisteredClientId(), authorization.getPrincipalName())
-						.scope(previouslyApprovedScope)
-						.build();
-		when(this.authorizationConsentService.findById(eq(authorization.getRegisteredClientId()), eq(authorization.getPrincipalName())))
-				.thenReturn(previousAuthorizationConsent);
-
-		// Revoke all (including previously approved)
-		this.authenticationProvider.setAuthorizationConsentCustomizer((authorizationConsentContext) ->
-				authorizationConsentContext.getAuthorizationConsent().authorities(Set::clear));
-
-		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
-				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
-				.satisfies(ex ->
-						assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
-								OAuth2ErrorCodes.ACCESS_DENIED, OAuth2ParameterNames.CLIENT_ID, authorizationRequest.getRedirectUri())
-				);
-
-		verify(this.authorizationConsentService).remove(eq(previousAuthorizationConsent));
-		verify(this.authorizationService).remove(eq(authorization));
-	}
-
-	@Test
-	public void authenticateWhenConsentRequestApproveSomeAndPreviouslyApprovedThenAuthorizationConsentUpdated() {
-		String previouslyApprovedScope = "message.read";
-		String requestedScope = "message.write";
-		String otherPreviouslyApprovedScope = "other.scope";
-		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
-				.scopes(scopes -> {
-					scopes.clear();
-					scopes.add(previouslyApprovedScope);
-					scopes.add(requestedScope);
-				})
-				.build();
-		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
-				.thenReturn(registeredClient);
-		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
-				.principalName(this.principal.getName())
-				.build();
-		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
-		Set<String> requestedScopes = authorizationRequest.getScopes();
-		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-				authorizationConsentRequestAuthentication(registeredClient, this.principal)
-						.scopes(requestedScopes)
-						.build();
-		when(this.authorizationService.findByToken(eq(authentication.getState()), eq(STATE_TOKEN_TYPE)))
-				.thenReturn(authorization);
-		OAuth2AuthorizationConsent previousAuthorizationConsent =
-				OAuth2AuthorizationConsent.withId(authorization.getRegisteredClientId(), authorization.getPrincipalName())
-						.scope(previouslyApprovedScope)
-						.scope(otherPreviouslyApprovedScope)
-						.build();
-		when(this.authorizationConsentService.findById(eq(authorization.getRegisteredClientId()), eq(authorization.getPrincipalName())))
-				.thenReturn(previousAuthorizationConsent);
-
-		OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult =
-				(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication);
-
-		ArgumentCaptor<OAuth2AuthorizationConsent> authorizationConsentCaptor = ArgumentCaptor.forClass(OAuth2AuthorizationConsent.class);
-		verify(this.authorizationConsentService).save(authorizationConsentCaptor.capture());
-		OAuth2AuthorizationConsent updatedAuthorizationConsent = authorizationConsentCaptor.getValue();
-
-		assertThat(updatedAuthorizationConsent.getRegisteredClientId()).isEqualTo(previousAuthorizationConsent.getRegisteredClientId());
-		assertThat(updatedAuthorizationConsent.getPrincipalName()).isEqualTo(previousAuthorizationConsent.getPrincipalName());
-		assertThat(updatedAuthorizationConsent.getScopes()).containsExactlyInAnyOrder(
-				previouslyApprovedScope, otherPreviouslyApprovedScope, requestedScope);
-
-		ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
-		verify(this.authorizationService).save(authorizationCaptor.capture());
-		OAuth2Authorization updatedAuthorization = authorizationCaptor.getValue();
-		assertThat(updatedAuthorization.getAuthorizedScopes()).isEqualTo(requestedScopes);
-		assertThat(authenticationResult.getScopes()).isEqualTo(requestedScopes);
-	}
-
-	@Test
-	public void authenticateWhenConsentRequestApproveNoneAndPreviouslyApprovedThenAuthorizationConsentNotUpdated() {
-		String previouslyApprovedScope = "message.read";
-		String requestedScope = "message.write";
-		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
-				.scopes(scopes -> {
-					scopes.clear();
-					scopes.add(previouslyApprovedScope);
-					scopes.add(requestedScope);
-				})
-				.build();
-		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
-				.thenReturn(registeredClient);
-		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
-				.principalName(this.principal.getName())
-				.build();
-		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-				authorizationConsentRequestAuthentication(registeredClient, this.principal)
-						.scopes(new HashSet<>())	// No scopes approved
-						.build();
-		when(this.authorizationService.findByToken(eq(authentication.getState()), eq(STATE_TOKEN_TYPE)))
-				.thenReturn(authorization);
-		OAuth2AuthorizationConsent previousAuthorizationConsent =
-				OAuth2AuthorizationConsent.withId(authorization.getRegisteredClientId(), authorization.getPrincipalName())
-						.scope(previouslyApprovedScope)
-						.build();
-		when(this.authorizationConsentService.findById(eq(authorization.getRegisteredClientId()), eq(authorization.getPrincipalName())))
-				.thenReturn(previousAuthorizationConsent);
-
-		OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult =
-				(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication);
-
-		verify(this.authorizationConsentService, never()).save(any());
-		assertThat(authenticationResult.getScopes()).isEqualTo(Collections.singleton(previouslyApprovedScope));
-	}
-
 	private static void assertAuthenticationException(OAuth2AuthorizationCodeRequestAuthenticationException authenticationException,
 			String errorCode, String parameterName, String redirectUri) {
 
@@ -1025,30 +619,6 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
 		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
 				authenticationException.getAuthorizationCodeRequestAuthentication();
 		assertThat(authorizationCodeRequestAuthentication.getRedirectUri()).isEqualTo(redirectUri);
-
-		// gh-595
-		if (OAuth2ErrorCodes.ACCESS_DENIED.equals(errorCode)) {
-			assertThat(authorizationCodeRequestAuthentication.isConsent()).isFalse();
-			assertThat(authorizationCodeRequestAuthentication.isConsentRequired()).isFalse();
-		}
-	}
-
-	private static OAuth2AuthorizationCodeRequestAuthenticationToken.Builder authorizationCodeRequestAuthentication(
-			RegisteredClient registeredClient, Authentication principal) {
-		return OAuth2AuthorizationCodeRequestAuthenticationToken.with(registeredClient.getClientId(), principal)
-				.authorizationUri("https://provider.com/oauth2/authorize")
-				.redirectUri(registeredClient.getRedirectUris().iterator().next())
-				.scopes(registeredClient.getScopes())
-				.state("state");
-	}
-
-	private static OAuth2AuthorizationCodeRequestAuthenticationToken.Builder authorizationConsentRequestAuthentication(
-			RegisteredClient registeredClient, Authentication principal) {
-		return OAuth2AuthorizationCodeRequestAuthenticationToken.with(registeredClient.getClientId(), principal)
-				.authorizationUri("https://provider.com/oauth2/authorize")
-				.scopes(registeredClient.getScopes())
-				.state("state")
-				.consent(true);
 	}
 
 }

+ 28 - 99
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationTokenTests.java

@@ -38,61 +38,57 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
  */
 public class OAuth2AuthorizationCodeRequestAuthenticationTokenTests {
 	private static final String AUTHORIZATION_URI = "https://provider.com/oauth2/authorize";
-	private static final String STATE = "state";
 	private static final RegisteredClient REGISTERED_CLIENT = TestRegisteredClients.registeredClient().build();
 	private static final TestingAuthenticationToken PRINCIPAL = new TestingAuthenticationToken("principalName", "password");
 	private static final OAuth2AuthorizationCode AUTHORIZATION_CODE =
 			new OAuth2AuthorizationCode("code", Instant.now(), Instant.now().plus(5, ChronoUnit.MINUTES));
 
 	@Test
-	public void withWhenClientIdNullThenThrowIllegalArgumentException() {
-		assertThatThrownBy(() -> OAuth2AuthorizationCodeRequestAuthenticationToken.with(null, PRINCIPAL))
+	public void constructorWhenAuthorizationUriNotProvidedThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() ->
+				new OAuth2AuthorizationCodeRequestAuthenticationToken(null, REGISTERED_CLIENT.getClientId(), PRINCIPAL,
+						null, null, (Set<String>) null, null))
 				.isInstanceOf(IllegalArgumentException.class)
-				.hasMessage("clientId cannot be empty");
+				.hasMessage("authorizationUri cannot be empty");
 	}
 
 	@Test
-	public void withWhenPrincipalNullThenThrowIllegalArgumentException() {
-		assertThatThrownBy(() -> OAuth2AuthorizationCodeRequestAuthenticationToken.with(REGISTERED_CLIENT.getClientId(), null))
+	public void constructorWhenClientIdNotProvidedThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() ->
+				new OAuth2AuthorizationCodeRequestAuthenticationToken(AUTHORIZATION_URI, null, PRINCIPAL,
+						null, null, (Set<String>) null, null))
 				.isInstanceOf(IllegalArgumentException.class)
-				.hasMessage("principal cannot be null");
+				.hasMessage("clientId cannot be empty");
 	}
 
 	@Test
-	public void buildWhenAuthorizationUriNotProvidedThenThrowIllegalArgumentException() {
+	public void constructorWhenPrincipalNotProvidedThenThrowIllegalArgumentException() {
 		assertThatThrownBy(() ->
-				OAuth2AuthorizationCodeRequestAuthenticationToken.with(REGISTERED_CLIENT.getClientId(), PRINCIPAL)
-						.build())
+				new OAuth2AuthorizationCodeRequestAuthenticationToken(AUTHORIZATION_URI, REGISTERED_CLIENT.getClientId(), null,
+						null, null, (Set<String>) null, null))
 				.isInstanceOf(IllegalArgumentException.class)
-				.hasMessage("authorizationUri cannot be empty");
+				.hasMessage("principal cannot be null");
 	}
 
 	@Test
-	public void buildWhenStateNotProvidedThenThrowIllegalArgumentException() {
+	public void constructorWhenAuthorizationCodeNotProvidedThenThrowIllegalArgumentException() {
 		assertThatThrownBy(() ->
-				OAuth2AuthorizationCodeRequestAuthenticationToken.with(REGISTERED_CLIENT.getClientId(), PRINCIPAL)
-						.authorizationUri(AUTHORIZATION_URI)
-						.consent(true)
-						.build())
+				new OAuth2AuthorizationCodeRequestAuthenticationToken(AUTHORIZATION_URI, REGISTERED_CLIENT.getClientId(), PRINCIPAL,
+						null, null, null, (Set<String>) null))
 				.isInstanceOf(IllegalArgumentException.class)
-				.hasMessage("state cannot be empty");
+				.hasMessage("authorizationCode cannot be null");
 	}
 
 	@Test
-	public void buildWhenAuthorizationCodeRequestThenValuesAreSet() {
+	public void constructorWhenAuthorizationRequestThenValuesAreSet() {
 		String clientId = REGISTERED_CLIENT.getClientId();
 		String redirectUri = REGISTERED_CLIENT.getRedirectUris().iterator().next();
+		String state = "state";
 		Set<String> requestedScopes = REGISTERED_CLIENT.getScopes();
 		Map<String, Object> additionalParameters = Collections.singletonMap("param1", "value1");
 
-		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-				OAuth2AuthorizationCodeRequestAuthenticationToken.with(clientId, PRINCIPAL)
-						.authorizationUri(AUTHORIZATION_URI)
-						.redirectUri(redirectUri)
-						.scopes(requestedScopes)
-						.state(STATE)
-						.additionalParameters(additionalParameters)
-						.build();
+		OAuth2AuthorizationCodeRequestAuthenticationToken authentication = new OAuth2AuthorizationCodeRequestAuthenticationToken(
+				AUTHORIZATION_URI, clientId, PRINCIPAL, redirectUri, state, requestedScopes, additionalParameters);
 
 		assertThat(authentication.getPrincipal()).isEqualTo(PRINCIPAL);
 		assertThat(authentication.getCredentials()).isEqualTo("");
@@ -100,87 +96,22 @@ public class OAuth2AuthorizationCodeRequestAuthenticationTokenTests {
 		assertThat(authentication.getAuthorizationUri()).isEqualTo(AUTHORIZATION_URI);
 		assertThat(authentication.getClientId()).isEqualTo(clientId);
 		assertThat(authentication.getRedirectUri()).isEqualTo(redirectUri);
+		assertThat(authentication.getState()).isEqualTo(state);
 		assertThat(authentication.getScopes()).containsExactlyInAnyOrderElementsOf(requestedScopes);
-		assertThat(authentication.getState()).isEqualTo(STATE);
-		assertThat(authentication.getAdditionalParameters()).containsExactlyInAnyOrderEntriesOf(additionalParameters);
-		assertThat(authentication.isConsentRequired()).isFalse();
-		assertThat(authentication.isConsent()).isFalse();
-		assertThat(authentication.getAuthorizationCode()).isNull();
-		assertThat(authentication.isAuthenticated()).isFalse();
-	}
-
-	@Test
-	public void buildWhenAuthorizationConsentRequiredThenValuesAreSet() {
-		String clientId = REGISTERED_CLIENT.getClientId();
-		Set<String> authorizedScopes = REGISTERED_CLIENT.getScopes();
-
-		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-				OAuth2AuthorizationCodeRequestAuthenticationToken.with(clientId, PRINCIPAL)
-						.authorizationUri(AUTHORIZATION_URI)
-						.scopes(authorizedScopes)
-						.state(STATE)
-						.consentRequired(true)
-						.build();
-
-		assertThat(authentication.getPrincipal()).isEqualTo(PRINCIPAL);
-		assertThat(authentication.getCredentials()).isEqualTo("");
-		assertThat(authentication.getAuthorities()).isEmpty();
-		assertThat(authentication.getAuthorizationUri()).isEqualTo(AUTHORIZATION_URI);
-		assertThat(authentication.getClientId()).isEqualTo(clientId);
-		assertThat(authentication.getRedirectUri()).isNull();
-		assertThat(authentication.getScopes()).containsExactlyInAnyOrderElementsOf(authorizedScopes);
-		assertThat(authentication.getState()).isEqualTo(STATE);
-		assertThat(authentication.getAdditionalParameters()).isEmpty();
-		assertThat(authentication.isConsentRequired()).isTrue();
-		assertThat(authentication.isConsent()).isFalse();
-		assertThat(authentication.getAuthorizationCode()).isNull();
-		assertThat(authentication.isAuthenticated()).isTrue();
-	}
-
-	@Test
-	public void buildWhenAuthorizationConsentRequestThenValuesAreSet() {
-		String clientId = REGISTERED_CLIENT.getClientId();
-		Set<String> authorizedScopes = REGISTERED_CLIENT.getScopes();
-		Map<String, Object> additionalParameters = Collections.singletonMap("param1", "value1");
-
-		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-				OAuth2AuthorizationCodeRequestAuthenticationToken.with(clientId, PRINCIPAL)
-						.authorizationUri(AUTHORIZATION_URI)
-						.scopes(authorizedScopes)
-						.state(STATE)
-						.additionalParameters(additionalParameters)
-						.consent(true)
-						.build();
-
-		assertThat(authentication.getPrincipal()).isEqualTo(PRINCIPAL);
-		assertThat(authentication.getCredentials()).isEqualTo("");
-		assertThat(authentication.getAuthorities()).isEmpty();
-		assertThat(authentication.getAuthorizationUri()).isEqualTo(AUTHORIZATION_URI);
-		assertThat(authentication.getClientId()).isEqualTo(clientId);
-		assertThat(authentication.getRedirectUri()).isNull();
-		assertThat(authentication.getScopes()).containsExactlyInAnyOrderElementsOf(authorizedScopes);
-		assertThat(authentication.getState()).isEqualTo(STATE);
 		assertThat(authentication.getAdditionalParameters()).containsExactlyInAnyOrderEntriesOf(additionalParameters);
-		assertThat(authentication.isConsentRequired()).isFalse();
-		assertThat(authentication.isConsent()).isTrue();
 		assertThat(authentication.getAuthorizationCode()).isNull();
 		assertThat(authentication.isAuthenticated()).isFalse();
 	}
 
 	@Test
-	public void buildWhenAuthorizationResponseThenValuesAreSet() {
+	public void constructorWhenAuthorizationResponseThenValuesAreSet() {
 		String clientId = REGISTERED_CLIENT.getClientId();
 		String redirectUri = REGISTERED_CLIENT.getRedirectUris().iterator().next();
+		String state = "state";
 		Set<String> authorizedScopes = REGISTERED_CLIENT.getScopes();
 
-		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
-				OAuth2AuthorizationCodeRequestAuthenticationToken.with(clientId, PRINCIPAL)
-						.authorizationUri(AUTHORIZATION_URI)
-						.redirectUri(redirectUri)
-						.scopes(authorizedScopes)
-						.state(STATE)
-						.authorizationCode(AUTHORIZATION_CODE)
-						.build();
+		OAuth2AuthorizationCodeRequestAuthenticationToken authentication = new OAuth2AuthorizationCodeRequestAuthenticationToken(
+				AUTHORIZATION_URI, clientId, PRINCIPAL, AUTHORIZATION_CODE, redirectUri, state, authorizedScopes);
 
 		assertThat(authentication.getPrincipal()).isEqualTo(PRINCIPAL);
 		assertThat(authentication.getCredentials()).isEqualTo("");
@@ -188,11 +119,9 @@ public class OAuth2AuthorizationCodeRequestAuthenticationTokenTests {
 		assertThat(authentication.getAuthorizationUri()).isEqualTo(AUTHORIZATION_URI);
 		assertThat(authentication.getClientId()).isEqualTo(clientId);
 		assertThat(authentication.getRedirectUri()).isEqualTo(redirectUri);
+		assertThat(authentication.getState()).isEqualTo(state);
 		assertThat(authentication.getScopes()).containsExactlyInAnyOrderElementsOf(authorizedScopes);
-		assertThat(authentication.getState()).isEqualTo(STATE);
 		assertThat(authentication.getAdditionalParameters()).isEmpty();
-		assertThat(authentication.isConsentRequired()).isFalse();
-		assertThat(authentication.isConsent()).isFalse();
 		assertThat(authentication.getAuthorizationCode()).isEqualTo(AUTHORIZATION_CODE);
 		assertThat(authentication.isAuthenticated()).isTrue();
 	}

+ 9 - 9
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationContextTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-2021 the original author or authors.
+ * Copyright 2020-2022 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,10 +42,10 @@ public class OAuth2AuthorizationConsentAuthenticationContextTests {
 	private final Authentication principal = this.authorization.getAttribute(Principal.class.getName());
 	private final OAuth2AuthorizationRequest authorizationRequest = this.authorization.getAttribute(
 			OAuth2AuthorizationRequest.class.getName());
-	private final OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
-			OAuth2AuthorizationCodeRequestAuthenticationToken.with(this.registeredClient.getClientId(), this.principal)
-					.authorizationUri(this.authorizationRequest.getAuthorizationUri())
-					.build();
+	private final OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthentication =
+			new OAuth2AuthorizationConsentAuthenticationToken(
+					this.authorizationRequest.getAuthorizationUri(), this.registeredClient.getClientId(),
+					this.principal, "state", null, null);
 	private final OAuth2AuthorizationConsent.Builder authorizationConsentBuilder =
 			OAuth2AuthorizationConsent.withId(this.authorization.getRegisteredClientId(), this.authorization.getPrincipalName());
 
@@ -59,7 +59,7 @@ public class OAuth2AuthorizationConsentAuthenticationContextTests {
 	@Test
 	public void setWhenValueNullThenThrowIllegalArgumentException() {
 		OAuth2AuthorizationConsentAuthenticationContext.Builder builder =
-				OAuth2AuthorizationConsentAuthenticationContext.with(this.authorizationCodeRequestAuthentication);
+				OAuth2AuthorizationConsentAuthenticationContext.with(this.authorizationConsentAuthentication);
 
 		assertThatThrownBy(() -> builder.authorizationConsent(null))
 				.isInstanceOf(IllegalArgumentException.class);
@@ -76,7 +76,7 @@ public class OAuth2AuthorizationConsentAuthenticationContextTests {
 	@Test
 	public void buildWhenRequiredValueNullThenThrowIllegalArgumentException() {
 		OAuth2AuthorizationConsentAuthenticationContext.Builder builder =
-				OAuth2AuthorizationConsentAuthenticationContext.with(this.authorizationCodeRequestAuthentication);
+				OAuth2AuthorizationConsentAuthenticationContext.with(this.authorizationConsentAuthentication);
 
 		assertThatThrownBy(builder::build)
 				.isInstanceOf(IllegalArgumentException.class)
@@ -104,7 +104,7 @@ public class OAuth2AuthorizationConsentAuthenticationContextTests {
 	@Test
 	public void buildWhenAllValuesProvidedThenAllValuesAreSet() {
 		OAuth2AuthorizationConsentAuthenticationContext context =
-				OAuth2AuthorizationConsentAuthenticationContext.with(this.authorizationCodeRequestAuthentication)
+				OAuth2AuthorizationConsentAuthenticationContext.with(this.authorizationConsentAuthentication)
 						.authorizationConsent(this.authorizationConsentBuilder)
 						.registeredClient(this.registeredClient)
 						.authorization(this.authorization)
@@ -113,7 +113,7 @@ public class OAuth2AuthorizationConsentAuthenticationContextTests {
 						.context(ctx -> ctx.put("custom-key-2", "custom-value-2"))
 						.build();
 
-		assertThat(context.<Authentication>getAuthentication()).isEqualTo(this.authorizationCodeRequestAuthentication);
+		assertThat(context.<Authentication>getAuthentication()).isEqualTo(this.authorizationConsentAuthentication);
 		assertThat(context.getAuthorizationConsent()).isEqualTo(this.authorizationConsentBuilder);
 		assertThat(context.getRegisteredClient()).isEqualTo(this.registeredClient);
 		assertThat(context.getAuthorization()).isEqualTo(this.authorization);

+ 548 - 0
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationConsentAuthenticationProviderTests.java

@@ -0,0 +1,548 @@
+/*
+ * Copyright 2020-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.server.authorization.authentication;
+
+import java.security.Principal;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.function.Consumer;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.core.Authentication;
+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.server.authorization.OAuth2Authorization;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
+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;
+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 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.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests for {@link OAuth2AuthorizationConsentAuthenticationProvider}.
+ *
+ * @author Joe Grandja
+ * @author Steve Riesenberg
+ */
+public class OAuth2AuthorizationConsentAuthenticationProviderTests {
+	private static final String AUTHORIZATION_URI = "https://provider.com/oauth2/authorize";
+	private static final String STATE = "state";
+	private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
+	private RegisteredClientRepository registeredClientRepository;
+	private OAuth2AuthorizationService authorizationService;
+	private OAuth2AuthorizationConsentService authorizationConsentService;
+	private OAuth2AuthorizationConsentAuthenticationProvider authenticationProvider;
+	private TestingAuthenticationToken principal;
+
+	@Before
+	public void setUp() {
+		this.registeredClientRepository = mock(RegisteredClientRepository.class);
+		this.authorizationService = mock(OAuth2AuthorizationService.class);
+		this.authorizationConsentService = mock(OAuth2AuthorizationConsentService.class);
+		this.authenticationProvider = new OAuth2AuthorizationConsentAuthenticationProvider(
+				this.registeredClientRepository, this.authorizationService, this.authorizationConsentService);
+		this.principal = new TestingAuthenticationToken("principalName", "password");
+		this.principal.setAuthenticated(true);
+		AuthorizationServerSettings authorizationServerSettings = AuthorizationServerSettings.builder().issuer("https://provider.com").build();
+		AuthorizationServerContextHolder.setContext(new TestAuthorizationServerContext(authorizationServerSettings, null));
+	}
+
+	@Test
+	public void constructorWhenRegisteredClientRepositoryNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> new OAuth2AuthorizationConsentAuthenticationProvider(
+				null, this.authorizationService, this.authorizationConsentService))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("registeredClientRepository cannot be null");
+	}
+
+	@Test
+	public void constructorWhenAuthorizationServiceNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> new OAuth2AuthorizationConsentAuthenticationProvider(
+				this.registeredClientRepository, null, this.authorizationConsentService))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("authorizationService cannot be null");
+	}
+
+	@Test
+	public void constructorWhenAuthorizationConsentServiceNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> new OAuth2AuthorizationConsentAuthenticationProvider(
+				this.registeredClientRepository, this.authorizationService, null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("authorizationConsentService cannot be null");
+	}
+
+	@Test
+	public void supportsWhenTypeOAuth2AuthorizationConsentAuthenticationTokenThenReturnTrue() {
+		assertThat(this.authenticationProvider.supports(OAuth2AuthorizationConsentAuthenticationToken.class)).isTrue();
+	}
+
+	@Test
+	public void setAuthorizationCodeGeneratorWhenNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> this.authenticationProvider.setAuthorizationCodeGenerator(null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("authorizationCodeGenerator cannot be null");
+	}
+
+	@Test
+	public void setAuthorizationConsentCustomizerWhenNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> this.authenticationProvider.setAuthorizationConsentCustomizer(null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("authorizationConsentCustomizer cannot be null");
+	}
+
+	@Test
+	public void authenticateWhenInvalidStateThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.build();
+		OAuth2AuthorizationConsentAuthenticationToken authentication =
+				new OAuth2AuthorizationConsentAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						STATE, registeredClient.getScopes(), null);
+		when(this.authorizationService.findByToken(eq(authentication.getState()), eq(STATE_TOKEN_TYPE)))
+				.thenReturn(null);
+
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
+				.satisfies(ex ->
+						assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
+								OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE, null)
+				);
+	}
+
+	@Test
+	public void authenticateWhenPrincipalNotAuthenticatedThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.build();
+		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
+				.principalName(this.principal.getName())
+				.build();
+		OAuth2AuthorizationConsentAuthenticationToken authentication =
+				new OAuth2AuthorizationConsentAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						STATE, registeredClient.getScopes(), null);
+		when(this.authorizationService.findByToken(eq(authentication.getState()), eq(STATE_TOKEN_TYPE)))
+				.thenReturn(authorization);
+		this.principal.setAuthenticated(false);
+
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
+				.satisfies(ex ->
+						assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
+								OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE, null)
+				);
+	}
+
+	@Test
+	public void authenticateWhenInvalidPrincipalThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.build();
+		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
+				.principalName(this.principal.getName().concat("-other"))
+				.build();
+		OAuth2AuthorizationConsentAuthenticationToken authentication =
+				new OAuth2AuthorizationConsentAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						STATE, registeredClient.getScopes(), null);
+		when(this.authorizationService.findByToken(eq(authentication.getState()), eq(STATE_TOKEN_TYPE)))
+				.thenReturn(authorization);
+
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
+				.satisfies(ex ->
+						assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
+								OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE, null)
+				);
+	}
+
+	@Test
+	public void authenticateWhenInvalidClientIdThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.build();
+		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
+				.principalName(this.principal.getName())
+				.build();
+		when(this.authorizationService.findByToken(eq("state"), eq(STATE_TOKEN_TYPE)))
+				.thenReturn(authorization);
+		RegisteredClient otherRegisteredClient = TestRegisteredClients.registeredClient2()
+				.build();
+		OAuth2AuthorizationConsentAuthenticationToken authentication =
+				new OAuth2AuthorizationConsentAuthenticationToken(
+						AUTHORIZATION_URI, otherRegisteredClient.getClientId(), principal,
+						STATE, registeredClient.getScopes(), null);
+
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
+				.satisfies(ex ->
+						assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
+								OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID, null)
+				);
+	}
+
+	@Test
+	public void authenticateWhenDoesNotMatchClientThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+		RegisteredClient otherRegisteredClient = TestRegisteredClients.registeredClient2()
+				.build();
+		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(otherRegisteredClient)
+				.principalName(this.principal.getName())
+				.build();
+		when(this.authorizationService.findByToken(eq("state"), eq(STATE_TOKEN_TYPE)))
+				.thenReturn(authorization);
+		OAuth2AuthorizationConsentAuthenticationToken authentication =
+				new OAuth2AuthorizationConsentAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						STATE, registeredClient.getScopes(), null);
+
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
+				.satisfies(ex ->
+						assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
+								OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID, null)
+				);
+	}
+
+	@Test
+	public void authenticateWhenScopeNotRequestedThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
+				.principalName(this.principal.getName())
+				.build();
+		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
+		Set<String> authorizedScopes = new HashSet<>(authorizationRequest.getScopes());
+		authorizedScopes.add("scope-not-requested");
+		OAuth2AuthorizationConsentAuthenticationToken authentication =
+				new OAuth2AuthorizationConsentAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						STATE, authorizedScopes, null);
+		when(this.authorizationService.findByToken(eq(authentication.getState()), eq(STATE_TOKEN_TYPE)))
+				.thenReturn(authorization);
+
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
+				.satisfies(ex ->
+						assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
+								OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE, authorizationRequest.getRedirectUri())
+				);
+	}
+
+	@Test
+	public void authenticateWhenNotApprovedThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
+				.principalName(this.principal.getName())
+				.build();
+		OAuth2AuthorizationConsentAuthenticationToken authentication =
+				new OAuth2AuthorizationConsentAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						STATE, new HashSet<>(), null);		// No scopes approved
+		when(this.authorizationService.findByToken(eq(authentication.getState()), eq(STATE_TOKEN_TYPE)))
+				.thenReturn(authorization);
+
+		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
+
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
+				.satisfies(ex ->
+						assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
+								OAuth2ErrorCodes.ACCESS_DENIED, OAuth2ParameterNames.CLIENT_ID, authorizationRequest.getRedirectUri())
+				);
+
+		verify(this.authorizationService).remove(eq(authorization));
+	}
+
+	@Test
+	public void authenticateWhenApproveAllThenReturnAuthorizationCode() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
+				.principalName(this.principal.getName())
+				.build();
+		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
+		Set<String> authorizedScopes = authorizationRequest.getScopes();
+		OAuth2AuthorizationConsentAuthenticationToken authentication =
+				new OAuth2AuthorizationConsentAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						STATE, authorizedScopes, null);		// Approve all scopes
+		when(this.authorizationService.findByToken(eq(authentication.getState()), eq(STATE_TOKEN_TYPE)))
+				.thenReturn(authorization);
+
+		OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult =
+				(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication);
+
+		assertAuthorizationConsentRequestWithAuthorizationCodeResult(registeredClient, authorization, authenticationResult);
+	}
+
+	@Test
+	public void authenticateWhenCustomAuthorizationConsentCustomizerThenUsed() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
+				.principalName(this.principal.getName())
+				.build();
+		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
+		Set<String> authorizedScopes = authorizationRequest.getScopes();
+		OAuth2AuthorizationConsentAuthenticationToken authentication =
+				new OAuth2AuthorizationConsentAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						STATE, authorizedScopes, null);		// Approve all scopes
+		when(this.authorizationService.findByToken(eq(authentication.getState()), eq(STATE_TOKEN_TYPE)))
+				.thenReturn(authorization);
+
+		@SuppressWarnings("unchecked")
+		Consumer<OAuth2AuthorizationConsentAuthenticationContext> authorizationConsentCustomizer = mock(Consumer.class);
+		this.authenticationProvider.setAuthorizationConsentCustomizer(authorizationConsentCustomizer);
+
+		OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult =
+				(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication);
+
+		assertAuthorizationConsentRequestWithAuthorizationCodeResult(registeredClient, authorization, authenticationResult);
+
+		ArgumentCaptor<OAuth2AuthorizationConsentAuthenticationContext> authenticationContextCaptor =
+				ArgumentCaptor.forClass(OAuth2AuthorizationConsentAuthenticationContext.class);
+		verify(authorizationConsentCustomizer).accept(authenticationContextCaptor.capture());
+
+		OAuth2AuthorizationConsentAuthenticationContext authenticationContext = authenticationContextCaptor.getValue();
+		assertThat(authenticationContext.<Authentication>getAuthentication()).isEqualTo(authentication);
+		assertThat(authenticationContext.getAuthorizationConsent()).isNotNull();
+		assertThat(authenticationContext.getRegisteredClient()).isEqualTo(registeredClient);
+		assertThat(authenticationContext.getAuthorization()).isEqualTo(authorization);
+		assertThat(authenticationContext.getAuthorizationRequest()).isEqualTo(authorizationRequest);
+	}
+
+	private void assertAuthorizationConsentRequestWithAuthorizationCodeResult(
+			RegisteredClient registeredClient,
+			OAuth2Authorization authorization,
+			OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult) {
+		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
+		Set<String> authorizedScopes = authorizationRequest.getScopes();
+
+		ArgumentCaptor<OAuth2AuthorizationConsent> authorizationConsentCaptor = ArgumentCaptor.forClass(OAuth2AuthorizationConsent.class);
+		verify(this.authorizationConsentService).save(authorizationConsentCaptor.capture());
+		OAuth2AuthorizationConsent authorizationConsent = authorizationConsentCaptor.getValue();
+
+		assertThat(authorizationConsent.getRegisteredClientId()).isEqualTo(authorization.getRegisteredClientId());
+		assertThat(authorizationConsent.getPrincipalName()).isEqualTo(authorization.getPrincipalName());
+		assertThat(authorizationConsent.getAuthorities()).hasSize(authorizedScopes.size());
+		assertThat(authorizationConsent.getScopes()).containsExactlyInAnyOrderElementsOf(authorizedScopes);
+
+		ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
+		verify(this.authorizationService).save(authorizationCaptor.capture());
+		OAuth2Authorization updatedAuthorization = authorizationCaptor.getValue();
+
+		assertThat(updatedAuthorization.getRegisteredClientId()).isEqualTo(authorization.getRegisteredClientId());
+		assertThat(updatedAuthorization.getPrincipalName()).isEqualTo(authorization.getPrincipalName());
+		assertThat(updatedAuthorization.getAuthorizationGrantType()).isEqualTo(authorization.getAuthorizationGrantType());
+		assertThat(updatedAuthorization.<Authentication>getAttribute(Principal.class.getName()))
+				.isEqualTo(authorization.<Authentication>getAttribute(Principal.class.getName()));
+		assertThat(updatedAuthorization.<OAuth2AuthorizationRequest>getAttribute(OAuth2AuthorizationRequest.class.getName()))
+				.isEqualTo(authorizationRequest);
+		OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = updatedAuthorization.getToken(OAuth2AuthorizationCode.class);
+		assertThat(authorizationCode).isNotNull();
+		assertThat(updatedAuthorization.<String>getAttribute(OAuth2ParameterNames.STATE)).isNull();
+		assertThat(updatedAuthorization.getAuthorizedScopes()).isEqualTo(authorizedScopes);
+
+		assertThat(authenticationResult.getClientId()).isEqualTo(registeredClient.getClientId());
+		assertThat(authenticationResult.getPrincipal()).isEqualTo(this.principal);
+		assertThat(authenticationResult.getAuthorizationUri()).isEqualTo(authorizationRequest.getAuthorizationUri());
+		assertThat(authenticationResult.getRedirectUri()).isEqualTo(authorizationRequest.getRedirectUri());
+		assertThat(authenticationResult.getScopes()).isEqualTo(authorizedScopes);
+		assertThat(authenticationResult.getState()).isEqualTo(authorizationRequest.getState());
+		assertThat(authenticationResult.getAuthorizationCode()).isEqualTo(authorizationCode.getToken());
+		assertThat(authenticationResult.isAuthenticated()).isTrue();
+	}
+
+	@Test
+	public void authenticateWhenApproveNoneAndRevokePreviouslyApprovedThenAuthorizationConsentRemoved() {
+		String previouslyApprovedScope = "message.read";
+		String requestedScope = "message.write";
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.scopes(scopes -> {
+					scopes.clear();
+					scopes.add(previouslyApprovedScope);
+					scopes.add(requestedScope);
+				})
+				.build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
+				.principalName(this.principal.getName())
+				.build();
+		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
+		OAuth2AuthorizationConsentAuthenticationToken authentication =
+				new OAuth2AuthorizationConsentAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						STATE, new HashSet<>(), null);		// No scopes approved
+		when(this.authorizationService.findByToken(eq(authentication.getState()), eq(STATE_TOKEN_TYPE)))
+				.thenReturn(authorization);
+		OAuth2AuthorizationConsent previousAuthorizationConsent =
+				OAuth2AuthorizationConsent.withId(authorization.getRegisteredClientId(), authorization.getPrincipalName())
+						.scope(previouslyApprovedScope)
+						.build();
+		when(this.authorizationConsentService.findById(eq(authorization.getRegisteredClientId()), eq(authorization.getPrincipalName())))
+				.thenReturn(previousAuthorizationConsent);
+
+		// Revoke all (including previously approved)
+		this.authenticationProvider.setAuthorizationConsentCustomizer((authorizationConsentContext) ->
+				authorizationConsentContext.getAuthorizationConsent().authorities(Set::clear));
+
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
+				.satisfies(ex ->
+						assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
+								OAuth2ErrorCodes.ACCESS_DENIED, OAuth2ParameterNames.CLIENT_ID, authorizationRequest.getRedirectUri())
+				);
+
+		verify(this.authorizationConsentService).remove(eq(previousAuthorizationConsent));
+		verify(this.authorizationService).remove(eq(authorization));
+	}
+
+	@Test
+	public void authenticateWhenApproveSomeAndPreviouslyApprovedThenAuthorizationConsentUpdated() {
+		String previouslyApprovedScope = "message.read";
+		String requestedScope = "message.write";
+		String otherPreviouslyApprovedScope = "other.scope";
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.scopes(scopes -> {
+					scopes.clear();
+					scopes.add(previouslyApprovedScope);
+					scopes.add(requestedScope);
+				})
+				.build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
+				.principalName(this.principal.getName())
+				.build();
+		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
+		Set<String> requestedScopes = authorizationRequest.getScopes();
+		OAuth2AuthorizationConsentAuthenticationToken authentication =
+				new OAuth2AuthorizationConsentAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						STATE, requestedScopes, null);
+		when(this.authorizationService.findByToken(eq(authentication.getState()), eq(STATE_TOKEN_TYPE)))
+				.thenReturn(authorization);
+		OAuth2AuthorizationConsent previousAuthorizationConsent =
+				OAuth2AuthorizationConsent.withId(authorization.getRegisteredClientId(), authorization.getPrincipalName())
+						.scope(previouslyApprovedScope)
+						.scope(otherPreviouslyApprovedScope)
+						.build();
+		when(this.authorizationConsentService.findById(eq(authorization.getRegisteredClientId()), eq(authorization.getPrincipalName())))
+				.thenReturn(previousAuthorizationConsent);
+
+		OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult =
+				(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication);
+
+		ArgumentCaptor<OAuth2AuthorizationConsent> authorizationConsentCaptor = ArgumentCaptor.forClass(OAuth2AuthorizationConsent.class);
+		verify(this.authorizationConsentService).save(authorizationConsentCaptor.capture());
+		OAuth2AuthorizationConsent updatedAuthorizationConsent = authorizationConsentCaptor.getValue();
+
+		assertThat(updatedAuthorizationConsent.getRegisteredClientId()).isEqualTo(previousAuthorizationConsent.getRegisteredClientId());
+		assertThat(updatedAuthorizationConsent.getPrincipalName()).isEqualTo(previousAuthorizationConsent.getPrincipalName());
+		assertThat(updatedAuthorizationConsent.getScopes()).containsExactlyInAnyOrder(
+				previouslyApprovedScope, otherPreviouslyApprovedScope, requestedScope);
+
+		ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
+		verify(this.authorizationService).save(authorizationCaptor.capture());
+		OAuth2Authorization updatedAuthorization = authorizationCaptor.getValue();
+		assertThat(updatedAuthorization.getAuthorizedScopes()).isEqualTo(requestedScopes);
+		assertThat(authenticationResult.getScopes()).isEqualTo(requestedScopes);
+	}
+
+	@Test
+	public void authenticateWhenApproveNoneAndPreviouslyApprovedThenAuthorizationConsentNotUpdated() {
+		String previouslyApprovedScope = "message.read";
+		String requestedScope = "message.write";
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.scopes(scopes -> {
+					scopes.clear();
+					scopes.add(previouslyApprovedScope);
+					scopes.add(requestedScope);
+				})
+				.build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
+				.principalName(this.principal.getName())
+				.build();
+		OAuth2AuthorizationConsentAuthenticationToken authentication =
+				new OAuth2AuthorizationConsentAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						STATE, new HashSet<>(), null);		// No scopes approved
+		when(this.authorizationService.findByToken(eq(authentication.getState()), eq(STATE_TOKEN_TYPE)))
+				.thenReturn(authorization);
+		OAuth2AuthorizationConsent previousAuthorizationConsent =
+				OAuth2AuthorizationConsent.withId(authorization.getRegisteredClientId(), authorization.getPrincipalName())
+						.scope(previouslyApprovedScope)
+						.build();
+		when(this.authorizationConsentService.findById(eq(authorization.getRegisteredClientId()), eq(authorization.getPrincipalName())))
+				.thenReturn(previousAuthorizationConsent);
+
+		OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult =
+				(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication);
+
+		verify(this.authorizationConsentService, never()).save(any());
+		assertThat(authenticationResult.getScopes()).isEqualTo(Collections.singleton(previouslyApprovedScope));
+	}
+
+	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);
+	}
+
+}

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

@@ -90,6 +90,8 @@ import org.springframework.security.oauth2.server.authorization.TestOAuth2Author
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationProvider;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationContext;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationProvider;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationToken;
 import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
 import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository.RegisteredClientParametersMapper;
 import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
@@ -108,6 +110,7 @@ import org.springframework.security.oauth2.server.authorization.token.OAuth2Toke
 import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
 import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
 import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeRequestAuthenticationConverter;
+import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationConsentAuthenticationConverter;
 import org.springframework.security.web.SecurityFilterChain;
 import org.springframework.security.web.authentication.AuthenticationConverter;
 import org.springframework.security.web.authentication.AuthenticationFailureHandler;
@@ -626,13 +629,9 @@ public class OAuth2AuthorizationCodeGrantTests {
 		OAuth2AuthorizationCode authorizationCode = new OAuth2AuthorizationCode(
 				"code", Instant.now(), Instant.now().plus(5, ChronoUnit.MINUTES));
 		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult =
-				OAuth2AuthorizationCodeRequestAuthenticationToken.with(registeredClient.getClientId(), principal)
-						.authorizationUri("https://provider.com/oauth2/authorize")
-						.redirectUri(registeredClient.getRedirectUris().iterator().next())
-						.scopes(registeredClient.getScopes())
-						.state("state")
-						.authorizationCode(authorizationCode)
-						.build();
+				new OAuth2AuthorizationCodeRequestAuthenticationToken(
+						"https://provider.com/oauth2/authorize", registeredClient.getClientId(), principal, authorizationCode,
+						registeredClient.getRedirectUris().iterator().next(), "state", registeredClient.getScopes());
 		when(authorizationRequestConverter.convert(any())).thenReturn(authorizationCodeRequestAuthenticationResult);
 		when(authorizationRequestAuthenticationProvider.supports(eq(OAuth2AuthorizationCodeRequestAuthenticationToken.class))).thenReturn(true);
 		when(authorizationRequestAuthenticationProvider.authenticate(any())).thenReturn(authorizationCodeRequestAuthenticationResult);
@@ -650,7 +649,8 @@ public class OAuth2AuthorizationCodeGrantTests {
 		List<AuthenticationConverter> authenticationConverters = authenticationConvertersCaptor.getValue();
 		assertThat(authenticationConverters).allMatch((converter) ->
 				converter == authorizationRequestConverter ||
-						converter instanceof OAuth2AuthorizationCodeRequestAuthenticationConverter);
+						converter instanceof OAuth2AuthorizationCodeRequestAuthenticationConverter ||
+						converter instanceof OAuth2AuthorizationConsentAuthenticationConverter);
 
 		verify(authorizationRequestAuthenticationProvider).authenticate(eq(authorizationCodeRequestAuthenticationResult));
 
@@ -660,7 +660,8 @@ public class OAuth2AuthorizationCodeGrantTests {
 		List<AuthenticationProvider> authenticationProviders = authenticationProvidersCaptor.getValue();
 		assertThat(authenticationProviders).allMatch((provider) ->
 				provider == authorizationRequestAuthenticationProvider ||
-						provider instanceof OAuth2AuthorizationCodeRequestAuthenticationProvider);
+						provider instanceof OAuth2AuthorizationCodeRequestAuthenticationProvider ||
+						provider instanceof OAuth2AuthorizationConsentAuthenticationProvider);
 
 		verify(authorizationResponseHandler).onAuthenticationSuccess(any(), any(), eq(authorizationCodeRequestAuthenticationResult));
 	}
@@ -933,7 +934,7 @@ public class OAuth2AuthorizationCodeGrantTests {
 					new OAuth2AuthorizationServerConfigurer();
 			authorizationServerConfigurer
 					.authorizationEndpoint(authorizationEndpoint ->
-							authorizationEndpoint.authenticationProvider(createProvider()));
+							authorizationEndpoint.authenticationProviders(configureAuthenticationProviders()));
 			RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
 
 			http
@@ -965,15 +966,14 @@ public class OAuth2AuthorizationCodeGrantTests {
 			};
 		}
 
-		private AuthenticationProvider createProvider() {
-			OAuth2AuthorizationCodeRequestAuthenticationProvider authorizationCodeRequestAuthenticationProvider =
-					new OAuth2AuthorizationCodeRequestAuthenticationProvider(
-							this.registeredClientRepository,
-							this.authorizationService,
-							this.authorizationConsentService);
-			authorizationCodeRequestAuthenticationProvider.setAuthorizationConsentCustomizer(new AuthorizationConsentCustomizer());
-
-			return authorizationCodeRequestAuthenticationProvider;
+		private Consumer<List<AuthenticationProvider>> configureAuthenticationProviders() {
+			return (authenticationProviders) ->
+				authenticationProviders.forEach((authenticationProvider) -> {
+					if (authenticationProvider instanceof OAuth2AuthorizationConsentAuthenticationProvider) {
+						((OAuth2AuthorizationConsentAuthenticationProvider) authenticationProvider)
+								.setAuthorizationConsentCustomizer(new AuthorizationConsentCustomizer());
+					}
+				});
 		}
 
 		static class AuthorizationConsentCustomizer implements Consumer<OAuth2AuthorizationConsentAuthenticationContext> {
@@ -982,10 +982,10 @@ public class OAuth2AuthorizationCodeGrantTests {
 			public void accept(OAuth2AuthorizationConsentAuthenticationContext authorizationConsentAuthenticationContext) {
 				OAuth2AuthorizationConsent.Builder authorizationConsentBuilder =
 						authorizationConsentAuthenticationContext.getAuthorizationConsent();
-				OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
+				OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthentication =
 						authorizationConsentAuthenticationContext.getAuthentication();
 				Map<String, Object> additionalParameters =
-						authorizationCodeRequestAuthentication.getAdditionalParameters();
+						authorizationConsentAuthentication.getAdditionalParameters();
 				RegisteredClient registeredClient = authorizationConsentAuthenticationContext.getRegisteredClient();
 				ClientSettings clientSettings = registeredClient.getClientSettings();
 

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

@@ -0,0 +1,347 @@
+/*
+ * Copyright 2020-2022 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.function.Consumer;
+
+import org.junit.Rule;
+import org.junit.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
+import org.springframework.security.oauth2.core.oidc.OidcScopes;
+import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationServerMetadataClaimNames;
+import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
+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;
+import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
+import org.springframework.security.oauth2.server.authorization.oidc.OidcProviderConfiguration;
+import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
+import org.springframework.security.oauth2.server.authorization.test.SpringTestRule;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.ResultMatcher;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.hamcrest.CoreMatchers.hasItems;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * Integration tests for the OpenID Connect 1.0 Provider Configuration endpoint.
+ *
+ * @author Sahariar Alam Khandoker
+ * @author Joe Grandja
+ * @author Daniel Garnier-Moiroux
+ */
+public class OidcProviderConfigurationTests {
+	private static final String DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI = "/.well-known/openid-configuration";
+	private static final String ISSUER_URL = "https://example.com/issuer1";
+
+	@Rule
+	public final SpringTestRule spring = new SpringTestRule();
+
+	@Autowired
+	private AuthorizationServerSettings authorizationServerSettings;
+
+	@Autowired
+	private MockMvc mvc;
+
+	@Test
+	public void requestWhenConfigurationRequestAndIssuerSetThenReturnDefaultConfigurationResponse() throws Exception {
+		this.spring.register(AuthorizationServerConfiguration.class).autowire();
+
+		this.mvc.perform(get(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI))
+				.andExpect(status().is2xxSuccessful())
+				.andExpectAll(defaultConfigurationMatchers());
+	}
+
+	// gh-632
+	@Test
+	public void requestWhenConfigurationRequestAndUserAuthenticatedThenReturnConfigurationResponse() throws Exception {
+		this.spring.register(AuthorizationServerConfiguration.class).autowire();
+
+		this.mvc.perform(get(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI)
+				.with(user("user")))
+				.andExpect(status().is2xxSuccessful())
+				.andExpectAll(defaultConfigurationMatchers());
+	}
+
+	// gh-616
+	@Test
+	public void requestWhenConfigurationRequestAndConfigurationCustomizerSetThenReturnCustomConfigurationResponse() throws Exception {
+		this.spring.register(AuthorizationServerConfigurationWithProviderConfigurationCustomizer.class).autowire();
+
+		this.mvc.perform(get(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI))
+				.andExpect(status().is2xxSuccessful())
+				.andExpect(jsonPath(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED,
+						hasItems(OidcScopes.OPENID, OidcScopes.PROFILE, OidcScopes.EMAIL)));
+	}
+
+	@Test
+	public void requestWhenConfigurationRequestAndClientRegistrationEnabledThenConfigurationResponseIncludesRegistrationEndpoint() throws Exception {
+		this.spring.register(AuthorizationServerConfigurationWithClientRegistrationEnabled.class).autowire();
+
+		this.mvc.perform(get(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI))
+				.andExpect(status().is2xxSuccessful())
+				.andExpectAll(defaultConfigurationMatchers())
+				.andExpect(jsonPath("$.registration_endpoint").value(ISSUER_URL.concat(this.authorizationServerSettings.getOidcClientRegistrationEndpoint())));
+	}
+
+	private ResultMatcher[] defaultConfigurationMatchers() {
+		// @formatter:off
+		return new ResultMatcher[] {
+				jsonPath("issuer").value(ISSUER_URL),
+				jsonPath("authorization_endpoint").value(ISSUER_URL.concat(this.authorizationServerSettings.getAuthorizationEndpoint())),
+				jsonPath("token_endpoint").value(ISSUER_URL.concat(this.authorizationServerSettings.getTokenEndpoint())),
+				jsonPath("$.token_endpoint_auth_methods_supported[0]").value(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()),
+				jsonPath("$.token_endpoint_auth_methods_supported[1]").value(ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue()),
+				jsonPath("$.token_endpoint_auth_methods_supported[2]").value(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue()),
+				jsonPath("$.token_endpoint_auth_methods_supported[3]").value(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue()),
+				jsonPath("jwks_uri").value(ISSUER_URL.concat(this.authorizationServerSettings.getJwkSetEndpoint())),
+				jsonPath("userinfo_endpoint").value(ISSUER_URL.concat(this.authorizationServerSettings.getOidcUserInfoEndpoint())),
+				jsonPath("response_types_supported").value(OAuth2AuthorizationResponseType.CODE.getValue()),
+				jsonPath("$.grant_types_supported[0]").value(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()),
+				jsonPath("$.grant_types_supported[1]").value(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()),
+				jsonPath("$.grant_types_supported[2]").value(AuthorizationGrantType.REFRESH_TOKEN.getValue()),
+				jsonPath("revocation_endpoint").value(ISSUER_URL.concat(this.authorizationServerSettings.getTokenRevocationEndpoint())),
+				jsonPath("$.revocation_endpoint_auth_methods_supported[0]").value(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()),
+				jsonPath("$.revocation_endpoint_auth_methods_supported[1]").value(ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue()),
+				jsonPath("$.revocation_endpoint_auth_methods_supported[2]").value(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue()),
+				jsonPath("$.revocation_endpoint_auth_methods_supported[3]").value(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue()),
+				jsonPath("introspection_endpoint").value(ISSUER_URL.concat(this.authorizationServerSettings.getTokenIntrospectionEndpoint())),
+				jsonPath("$.introspection_endpoint_auth_methods_supported[0]").value(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()),
+				jsonPath("$.introspection_endpoint_auth_methods_supported[1]").value(ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue()),
+				jsonPath("$.introspection_endpoint_auth_methods_supported[2]").value(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue()),
+				jsonPath("$.introspection_endpoint_auth_methods_supported[3]").value(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue()),
+				jsonPath("subject_types_supported").value("public"),
+				jsonPath("id_token_signing_alg_values_supported").value(SignatureAlgorithm.RS256.getName()),
+				jsonPath("scopes_supported").value(OidcScopes.OPENID)
+		};
+		// @formatter:on
+	}
+
+	@Test
+	public void loadContextWhenIssuerNotValidUrlThenThrowException() {
+		assertThatThrownBy(
+				() -> this.spring.register(AuthorizationServerConfigurationWithInvalidIssuerUrl.class).autowire()
+		);
+	}
+
+	@Test
+	public void loadContextWhenIssuerNotValidUriThenThrowException() {
+		assertThatThrownBy(
+				() -> this.spring.register(AuthorizationServerConfigurationWithInvalidIssuerUri.class).autowire()
+		);
+	}
+
+	@Test
+	public void loadContextWhenIssuerWithQueryThenThrowException() {
+		assertThatThrownBy(
+				() -> this.spring.register(AuthorizationServerConfigurationWithIssuerQuery.class).autowire()
+		);
+	}
+
+	@Test
+	public void loadContextWhenIssuerWithFragmentThenThrowException() {
+		assertThatThrownBy(
+				() -> this.spring.register(AuthorizationServerConfigurationWithIssuerFragment.class).autowire()
+		);
+	}
+
+	@Test
+	public void loadContextWhenIssuerWithQueryAndFragmentThenThrowException() {
+		assertThatThrownBy(
+				() -> this.spring.register(AuthorizationServerConfigurationWithIssuerQueryAndFragment.class).autowire()
+		);
+	}
+
+	@Test
+	public void loadContextWhenIssuerWithEmptyQueryThenThrowException() {
+		assertThatThrownBy(
+				() -> this.spring.register(AuthorizationServerConfigurationWithIssuerEmptyQuery.class).autowire()
+		);
+	}
+
+	@Test
+	public void loadContextWhenIssuerWithEmptyFragmentThenThrowException() {
+		assertThatThrownBy(
+				() -> this.spring.register(AuthorizationServerConfigurationWithIssuerEmptyFragment.class).autowire()
+		);
+	}
+
+	@EnableWebSecurity
+	@Import(OAuth2AuthorizationServerConfiguration.class)
+	static class AuthorizationServerConfiguration {
+
+		@Bean
+		RegisteredClientRepository registeredClientRepository() {
+			RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+			return new InMemoryRegisteredClientRepository(registeredClient);
+		}
+
+		@Bean
+		AuthorizationServerSettings authorizationServerSettings() {
+			return AuthorizationServerSettings.builder()
+					.issuer(ISSUER_URL)
+					.build();
+		}
+
+	}
+
+	@Configuration
+	@EnableWebSecurity
+	static class AuthorizationServerConfigurationWithProviderConfigurationCustomizer extends AuthorizationServerConfiguration {
+
+		// @formatter:off
+		@Bean
+		public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
+			OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
+					new OAuth2AuthorizationServerConfigurer();
+			http.apply(authorizationServerConfigurer);
+
+			authorizationServerConfigurer
+					.oidc(oidc ->
+							oidc.providerConfigurationEndpoint(providerConfigurationEndpoint ->
+									providerConfigurationEndpoint
+											.providerConfigurationCustomizer(providerConfigurationCustomizer())));
+
+			RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
+
+			http
+					.requestMatcher(endpointsMatcher)
+					.authorizeRequests(authorizeRequests ->
+							authorizeRequests.anyRequest().authenticated()
+					)
+					.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher));
+
+			return http.build();
+		}
+		// @formatter:on
+
+		private Consumer<OidcProviderConfiguration.Builder> providerConfigurationCustomizer() {
+			return (providerConfiguration) ->
+					providerConfiguration.scope(OidcScopes.PROFILE).scope(OidcScopes.EMAIL);
+		}
+
+	}
+
+	@Configuration
+	@EnableWebSecurity
+	static class AuthorizationServerConfigurationWithClientRegistrationEnabled extends AuthorizationServerConfiguration {
+
+		// @formatter:off
+		@Bean
+		SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
+			OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
+					new OAuth2AuthorizationServerConfigurer();
+			http.apply(authorizationServerConfigurer);
+
+			authorizationServerConfigurer
+					.oidc(oidc ->
+							oidc.clientRegistrationEndpoint(Customizer.withDefaults())
+					);
+
+			return http.build();
+		}
+		// @formatter:on
+
+	}
+
+	@EnableWebSecurity
+	@Import(OAuth2AuthorizationServerConfiguration.class)
+	static class AuthorizationServerConfigurationWithInvalidIssuerUrl extends AuthorizationServerConfiguration {
+
+		@Bean
+		AuthorizationServerSettings authorizationServerSettings() {
+			return AuthorizationServerSettings.builder().issuer("urn:example").build();
+		}
+	}
+
+	@EnableWebSecurity
+	@Import(OAuth2AuthorizationServerConfiguration.class)
+	static class AuthorizationServerConfigurationWithInvalidIssuerUri extends AuthorizationServerConfiguration {
+
+		@Bean
+		AuthorizationServerSettings authorizationServerSettings() {
+			return AuthorizationServerSettings.builder().issuer("https://not a valid uri").build();
+		}
+	}
+
+	@EnableWebSecurity
+	@Import(OAuth2AuthorizationServerConfiguration.class)
+	static class AuthorizationServerConfigurationWithIssuerQuery extends AuthorizationServerConfiguration {
+
+		@Bean
+		AuthorizationServerSettings authorizationServerSettings() {
+			return AuthorizationServerSettings.builder().issuer(ISSUER_URL + "?param=value").build();
+		}
+	}
+
+	@EnableWebSecurity
+	@Import(OAuth2AuthorizationServerConfiguration.class)
+	static class AuthorizationServerConfigurationWithIssuerFragment extends AuthorizationServerConfiguration {
+
+		@Bean
+		AuthorizationServerSettings authorizationServerSettings() {
+			return AuthorizationServerSettings.builder().issuer(ISSUER_URL + "#fragment").build();
+		}
+	}
+
+	@EnableWebSecurity
+	@Import(OAuth2AuthorizationServerConfiguration.class)
+	static class AuthorizationServerConfigurationWithIssuerQueryAndFragment extends AuthorizationServerConfiguration {
+
+		@Bean
+		AuthorizationServerSettings authorizationServerSettings() {
+			return AuthorizationServerSettings.builder().issuer(ISSUER_URL + "?param=value#fragment").build();
+		}
+	}
+
+	@EnableWebSecurity
+	@Import(OAuth2AuthorizationServerConfiguration.class)
+	static class AuthorizationServerConfigurationWithIssuerEmptyQuery extends AuthorizationServerConfiguration {
+
+		@Bean
+		AuthorizationServerSettings authorizationServerSettings() {
+			return AuthorizationServerSettings.builder().issuer(ISSUER_URL + "?").build();
+		}
+	}
+
+	@EnableWebSecurity
+	@Import(OAuth2AuthorizationServerConfiguration.class)
+	static class AuthorizationServerConfigurationWithIssuerEmptyFragment extends AuthorizationServerConfiguration {
+
+		@Bean
+		AuthorizationServerSettings authorizationServerSettings() {
+			return AuthorizationServerSettings.builder().issuer(ISSUER_URL + "#").build();
+		}
+	}
+
+}

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

@@ -24,7 +24,6 @@ import java.util.Base64;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
-import java.util.function.Consumer;
 
 import com.nimbusds.jose.jwk.JWKSet;
 import com.nimbusds.jose.jwk.source.JWKSource;
@@ -37,7 +36,6 @@ import org.junit.Test;
 
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Import;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.HttpStatus;
@@ -71,7 +69,6 @@ import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
 import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
 import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
-import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationServerMetadataClaimNames;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
 import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
 import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
@@ -82,8 +79,6 @@ import org.springframework.security.oauth2.server.authorization.client.Registere
 import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
 import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
 import org.springframework.security.oauth2.server.authorization.jackson2.TestingAuthenticationTokenMixin;
-import org.springframework.security.oauth2.server.authorization.oidc.OidcProviderConfiguration;
-import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
 import org.springframework.security.oauth2.server.authorization.test.SpringTestRule;
 import org.springframework.security.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator;
 import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
@@ -103,9 +98,7 @@ import org.springframework.web.util.UriComponents;
 import org.springframework.web.util.UriComponentsBuilder;
 
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.hamcrest.CoreMatchers.containsString;
-import static org.hamcrest.CoreMatchers.hasItems;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
@@ -126,8 +119,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
 public class OidcTests {
 	private static final String DEFAULT_AUTHORIZATION_ENDPOINT_URI = "/oauth2/authorize";
 	private static final String DEFAULT_TOKEN_ENDPOINT_URI = "/oauth2/token";
-	private static final String DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI = "/.well-known/openid-configuration";
-	private static final String ISSUER_URL = "https://example.com/issuer1";
 	private static final String AUTHORITIES_CLAIM = "authorities";
 	private static final OAuth2TokenType AUTHORIZATION_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.CODE);
 	private static EmbeddedDatabase db;
@@ -182,85 +173,6 @@ public class OidcTests {
 		db.shutdown();
 	}
 
-	@Test
-	public void requestWhenConfigurationRequestAndIssuerSetThenReturnConfigurationResponse() throws Exception {
-		this.spring.register(AuthorizationServerConfigurationWithIssuer.class).autowire();
-
-		this.mvc.perform(get(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI))
-				.andExpect(status().is2xxSuccessful())
-				.andExpect(jsonPath("issuer").value(ISSUER_URL));
-	}
-
-	// gh-632
-	@Test
-	public void requestWhenConfigurationRequestAndUserAuthenticatedThenReturnConfigurationResponse() throws Exception {
-		this.spring.register(AuthorizationServerConfiguration.class).autowire();
-
-		this.mvc.perform(get(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI)
-				.with(user("user")))
-				.andExpect(status().is2xxSuccessful());
-	}
-
-	// gh-616
-	@Test
-	public void requestWhenConfigurationRequestAndConfigurationCustomizerSetThenReturnCustomConfigurationResponse() throws Exception {
-		this.spring.register(AuthorizationServerConfigurationWithProviderConfigurationCustomizer.class).autowire();
-
-		this.mvc.perform(get(DEFAULT_OIDC_PROVIDER_CONFIGURATION_ENDPOINT_URI))
-				.andExpect(status().is2xxSuccessful())
-				.andExpect(jsonPath(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED,
-						hasItems(OidcScopes.OPENID, OidcScopes.PROFILE, OidcScopes.EMAIL)));
-	}
-
-	@Test
-	public void loadContextWhenIssuerNotValidUrlThenThrowException() {
-		assertThatThrownBy(
-				() -> this.spring.register(AuthorizationServerConfigurationWithInvalidIssuerUrl.class).autowire()
-		);
-	}
-
-	@Test
-	public void loadContextWhenIssuerNotValidUriThenThrowException() {
-		assertThatThrownBy(
-				() -> this.spring.register(AuthorizationServerConfigurationWithInvalidIssuerUri.class).autowire()
-		);
-	}
-
-	@Test
-	public void loadContextWhenIssuerWithQueryThenThrowException() {
-		assertThatThrownBy(
-				() -> this.spring.register(AuthorizationServerConfigurationWithIssuerQuery.class).autowire()
-		);
-	}
-
-	@Test
-	public void loadContextWhenIssuerWithFragmentThenThrowException() {
-		assertThatThrownBy(
-				() -> this.spring.register(AuthorizationServerConfigurationWithIssuerFragment.class).autowire()
-		);
-	}
-
-	@Test
-	public void loadContextWhenIssuerWithQueryAndFragmentThenThrowException() {
-		assertThatThrownBy(
-				() -> this.spring.register(AuthorizationServerConfigurationWithIssuerQueryAndFragment.class).autowire()
-		);
-	}
-
-	@Test
-	public void loadContextWhenIssuerWithEmptyQueryThenThrowException() {
-		assertThatThrownBy(
-				() -> this.spring.register(AuthorizationServerConfigurationWithIssuerEmptyQuery.class).autowire()
-		);
-	}
-
-	@Test
-	public void loadContextWhenIssuerWithEmptyFragmentThenThrowException() {
-		assertThatThrownBy(
-				() -> this.spring.register(AuthorizationServerConfigurationWithIssuerEmptyFragment.class).autowire()
-		);
-	}
-
 	@Test
 	public void requestWhenAuthenticationRequestThenTokenResponseIncludesIdToken() throws Exception {
 		this.spring.register(AuthorizationServerConfiguration.class).autowire();
@@ -481,121 +393,4 @@ public class OidcTests {
 
 	}
 
-	@Configuration
-	@EnableWebSecurity
-	static class AuthorizationServerConfigurationWithProviderConfigurationCustomizer extends AuthorizationServerConfiguration {
-
-		// @formatter:off
-		@Bean
-		public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
-			OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
-					new OAuth2AuthorizationServerConfigurer();
-			http.apply(authorizationServerConfigurer);
-
-			authorizationServerConfigurer
-					.oidc(oidc ->
-							oidc.providerConfigurationEndpoint(providerConfigurationEndpoint ->
-									providerConfigurationEndpoint
-											.providerConfigurationCustomizer(providerConfigurationCustomizer())));
-
-			RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
-
-			http
-					.requestMatcher(endpointsMatcher)
-					.authorizeRequests(authorizeRequests ->
-							authorizeRequests.anyRequest().authenticated()
-					)
-					.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher));
-
-			return http.build();
-		}
-		// @formatter:on
-
-		private Consumer<OidcProviderConfiguration.Builder> providerConfigurationCustomizer() {
-			return (providerConfiguration) ->
-					providerConfiguration.scope(OidcScopes.PROFILE).scope(OidcScopes.EMAIL);
-		}
-
-	}
-
-	@EnableWebSecurity
-	@Import(OAuth2AuthorizationServerConfiguration.class)
-	static class AuthorizationServerConfigurationWithIssuer extends AuthorizationServerConfiguration {
-
-		@Bean
-		AuthorizationServerSettings authorizationServerSettings() {
-			return AuthorizationServerSettings.builder().issuer(ISSUER_URL).build();
-		}
-	}
-
-	@EnableWebSecurity
-	@Import(OAuth2AuthorizationServerConfiguration.class)
-	static class AuthorizationServerConfigurationWithInvalidIssuerUrl extends AuthorizationServerConfiguration {
-
-		@Bean
-		AuthorizationServerSettings authorizationServerSettings() {
-			return AuthorizationServerSettings.builder().issuer("urn:example").build();
-		}
-	}
-
-	@EnableWebSecurity
-	@Import(OAuth2AuthorizationServerConfiguration.class)
-	static class AuthorizationServerConfigurationWithInvalidIssuerUri extends AuthorizationServerConfiguration {
-
-		@Bean
-		AuthorizationServerSettings authorizationServerSettings() {
-			return AuthorizationServerSettings.builder().issuer("https://not a valid uri").build();
-		}
-	}
-
-	@EnableWebSecurity
-	@Import(OAuth2AuthorizationServerConfiguration.class)
-	static class AuthorizationServerConfigurationWithIssuerQuery extends AuthorizationServerConfiguration {
-
-		@Bean
-		AuthorizationServerSettings authorizationServerSettings() {
-			return AuthorizationServerSettings.builder().issuer(ISSUER_URL + "?param=value").build();
-		}
-	}
-
-	@EnableWebSecurity
-	@Import(OAuth2AuthorizationServerConfiguration.class)
-	static class AuthorizationServerConfigurationWithIssuerFragment extends AuthorizationServerConfiguration {
-
-		@Bean
-		AuthorizationServerSettings authorizationServerSettings() {
-			return AuthorizationServerSettings.builder().issuer(ISSUER_URL + "#fragment").build();
-		}
-	}
-
-	@EnableWebSecurity
-	@Import(OAuth2AuthorizationServerConfiguration.class)
-	static class AuthorizationServerConfigurationWithIssuerQueryAndFragment extends AuthorizationServerConfiguration {
-
-		@Bean
-		AuthorizationServerSettings authorizationServerSettings() {
-			return AuthorizationServerSettings.builder().issuer(ISSUER_URL + "?param=value#fragment").build();
-		}
-	}
-
-	@EnableWebSecurity
-	@Import(OAuth2AuthorizationServerConfiguration.class)
-	static class AuthorizationServerConfigurationWithIssuerEmptyQuery extends AuthorizationServerConfiguration {
-
-		@Bean
-		AuthorizationServerSettings authorizationServerSettings() {
-			return AuthorizationServerSettings.builder().issuer(ISSUER_URL + "?").build();
-		}
-	}
-
-	@EnableWebSecurity
-	@Import(OAuth2AuthorizationServerConfiguration.class)
-	static class AuthorizationServerConfigurationWithIssuerEmptyFragment extends AuthorizationServerConfiguration {
-
-		@Bean
-		AuthorizationServerSettings authorizationServerSettings() {
-			return AuthorizationServerSettings.builder().issuer(ISSUER_URL + "#").build();
-		}
-	}
-
 }

+ 16 - 0
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderConfigurationTests.java

@@ -61,6 +61,7 @@ public class OidcProviderConfigurationTests {
 				.idTokenSigningAlgorithm("RS256")
 				.userInfoEndpoint("https://example.com/issuer1/userinfo")
 				.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue())
+				.clientRegistrationEndpoint("https://example.com/issuer1/connect/register")
 				.claim("a-claim", "a-value")
 				.build();
 
@@ -75,6 +76,7 @@ public class OidcProviderConfigurationTests {
 		assertThat(providerConfiguration.getIdTokenSigningAlgorithms()).containsExactly("RS256");
 		assertThat(providerConfiguration.getUserInfoEndpoint()).isEqualTo(url("https://example.com/issuer1/userinfo"));
 		assertThat(providerConfiguration.getTokenEndpointAuthenticationMethods()).containsExactly(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue());
+		assertThat(providerConfiguration.getClientRegistrationEndpoint()).isEqualTo(url("https://example.com/issuer1/connect/register"));
 		assertThat(providerConfiguration.<String>getClaim("a-claim")).isEqualTo("a-value");
 	}
 
@@ -115,6 +117,7 @@ public class OidcProviderConfigurationTests {
 		claims.put(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED, Collections.singletonList("public"));
 		claims.put(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED, Collections.singletonList("RS256"));
 		claims.put(OidcProviderMetadataClaimNames.USER_INFO_ENDPOINT, "https://example.com/issuer1/userinfo");
+		claims.put(OidcProviderMetadataClaimNames.REGISTRATION_ENDPOINT, "https://example.com/issuer1/connect/register");
 		claims.put("some-claim", "some-value");
 
 		OidcProviderConfiguration providerConfiguration = OidcProviderConfiguration.withClaims(claims).build();
@@ -130,6 +133,7 @@ public class OidcProviderConfigurationTests {
 		assertThat(providerConfiguration.getIdTokenSigningAlgorithms()).containsExactly("RS256");
 		assertThat(providerConfiguration.getUserInfoEndpoint()).isEqualTo(url("https://example.com/issuer1/userinfo"));
 		assertThat(providerConfiguration.getTokenEndpointAuthenticationMethods()).isNull();
+		assertThat(providerConfiguration.getClientRegistrationEndpoint()).isEqualTo(url("https://example.com/issuer1/connect/register"));
 		assertThat(providerConfiguration.<String>getClaim("some-claim")).isEqualTo("some-value");
 	}
 
@@ -145,6 +149,7 @@ public class OidcProviderConfigurationTests {
 		claims.put(OidcProviderMetadataClaimNames.SUBJECT_TYPES_SUPPORTED, Collections.singletonList("public"));
 		claims.put(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED, Collections.singletonList("RS256"));
 		claims.put(OidcProviderMetadataClaimNames.USER_INFO_ENDPOINT, url("https://example.com/issuer1/userinfo"));
+		claims.put(OidcProviderMetadataClaimNames.REGISTRATION_ENDPOINT, url("https://example.com/issuer1/connect/register"));
 		claims.put("some-claim", "some-value");
 
 		OidcProviderConfiguration providerConfiguration = OidcProviderConfiguration.withClaims(claims).build();
@@ -160,6 +165,7 @@ public class OidcProviderConfigurationTests {
 		assertThat(providerConfiguration.getIdTokenSigningAlgorithms()).containsExactly("RS256");
 		assertThat(providerConfiguration.getUserInfoEndpoint()).isEqualTo(url("https://example.com/issuer1/userinfo"));
 		assertThat(providerConfiguration.getTokenEndpointAuthenticationMethods()).isNull();
+		assertThat(providerConfiguration.getClientRegistrationEndpoint()).isEqualTo(url("https://example.com/issuer1/connect/register"));
 		assertThat(providerConfiguration.<String>getClaim("some-claim")).isEqualTo("some-value");
 	}
 
@@ -396,6 +402,16 @@ public class OidcProviderConfigurationTests {
 				.withMessage("userInfoEndpoint must be a valid URL");
 	}
 
+	@Test
+	public void buildWhenClientRegistrationEndpointNotUrlThenThrowIllegalArgumentException() {
+		OidcProviderConfiguration.Builder builder = this.minimalConfigurationBuilder
+				.claims((claims) -> claims.put(OidcProviderMetadataClaimNames.REGISTRATION_ENDPOINT, "not an url"));
+
+		assertThatIllegalArgumentException()
+				.isThrownBy(builder::build)
+				.withMessage("clientRegistrationEndpoint must be a valid URL");
+	}
+
 	@Test
 	public void responseTypesWhenAddingOrRemovingThenCorrectValues() {
 		OidcProviderConfiguration configuration = this.minimalConfigurationBuilder

+ 45 - 49
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilterTests.java

@@ -40,7 +40,6 @@ 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.Authentication;
 import org.springframework.security.core.context.SecurityContext;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.oauth2.core.OAuth2Error;
@@ -52,6 +51,7 @@ import org.springframework.security.oauth2.core.oidc.OidcScopes;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
 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.OAuth2AuthorizationConsentAuthenticationToken;
 import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
 import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
 import org.springframework.security.web.authentication.AuthenticationConverter;
@@ -82,6 +82,8 @@ import static org.mockito.Mockito.when;
  */
 public class OAuth2AuthorizationEndpointFilterTests {
 	private static final String DEFAULT_AUTHORIZATION_ENDPOINT_URI = "/oauth2/authorize";
+	private static final String AUTHORIZATION_URI = "https://provider.com/oauth2/authorize";
+	private static final String STATE = "state";
 	private static final String REMOTE_ADDRESS = "remote-address";
 	private AuthenticationManager authenticationManager;
 	private OAuth2AuthorizationEndpointFilter filter;
@@ -280,8 +282,9 @@ public class OAuth2AuthorizationEndpointFilterTests {
 	public void doFilterWhenAuthorizationRequestAuthenticationExceptionThenErrorResponse() throws Exception {
 		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
 		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
-				authorizationCodeRequestAuthentication(registeredClient, this.principal)
-						.build();
+				new OAuth2AuthorizationCodeRequestAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes(), null);
 		OAuth2Error error = new OAuth2Error("errorCode", "errorDescription", "errorUri");
 		when(this.authenticationManager.authenticate(any()))
 				.thenThrow(new OAuth2AuthorizationCodeRequestAuthenticationException(error, authorizationCodeRequestAuthentication));
@@ -304,8 +307,9 @@ public class OAuth2AuthorizationEndpointFilterTests {
 	public void doFilterWhenCustomAuthenticationConverterThenUsed() throws Exception {
 		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
 		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
-				authorizationCodeRequestAuthentication(registeredClient, this.principal)
-						.build();
+				new OAuth2AuthorizationCodeRequestAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes(), null);
 
 		AuthenticationConverter authenticationConverter = mock(AuthenticationConverter.class);
 		when(authenticationConverter.convert(any())).thenReturn(authorizationCodeRequestAuthentication);
@@ -329,9 +333,9 @@ public class OAuth2AuthorizationEndpointFilterTests {
 	public void doFilterWhenCustomAuthenticationSuccessHandlerThenUsed() throws Exception {
 		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
 		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult =
-				authorizationCodeRequestAuthentication(registeredClient, this.principal)
-						.authorizationCode(this.authorizationCode)
-						.build();
+				new OAuth2AuthorizationCodeRequestAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal, this.authorizationCode,
+						registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes());
 		authorizationCodeRequestAuthenticationResult.setAuthenticated(true);
 		when(this.authenticationManager.authenticate(any()))
 				.thenReturn(authorizationCodeRequestAuthenticationResult);
@@ -354,8 +358,9 @@ public class OAuth2AuthorizationEndpointFilterTests {
 	public void doFilterWhenCustomAuthenticationFailureHandlerThenUsed() throws Exception {
 		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
 		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
-				authorizationCodeRequestAuthentication(registeredClient, this.principal)
-						.build();
+				new OAuth2AuthorizationCodeRequestAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes(), null);
 		OAuth2Error error = new OAuth2Error("errorCode", "errorDescription", "errorUri");
 		OAuth2AuthorizationCodeRequestAuthenticationException authenticationException =
 				new OAuth2AuthorizationCodeRequestAuthenticationException(error, authorizationCodeRequestAuthentication);
@@ -380,7 +385,9 @@ public class OAuth2AuthorizationEndpointFilterTests {
 	public void doFilterWhenCustomAuthenticationDetailsSourceThenUsed() throws Exception {
 		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
 		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
-				authorizationCodeRequestAuthentication(registeredClient, this.principal).build();
+				new OAuth2AuthorizationCodeRequestAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes(), null);
 		MockHttpServletRequest request = createAuthorizationRequest(registeredClient);
 
 		AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> authenticationDetailsSource =
@@ -407,8 +414,9 @@ public class OAuth2AuthorizationEndpointFilterTests {
 		this.principal.setAuthenticated(false);
 		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
 		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult =
-				authorizationCodeRequestAuthentication(registeredClient, this.principal)
-						.build();
+				new OAuth2AuthorizationCodeRequestAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes(), null);
 		authorizationCodeRequestAuthenticationResult.setAuthenticated(false);
 		when(this.authenticationManager.authenticate(any()))
 				.thenReturn(authorizationCodeRequestAuthenticationResult);
@@ -432,14 +440,13 @@ public class OAuth2AuthorizationEndpointFilterTests {
 					scopes.addAll(requestedScopes);
 				})
 				.build();
-		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult =
-				authorizationCodeRequestAuthentication(registeredClient, this.principal)
-						.scopes(new HashSet<>())	// No scopes previously approved
-						.consentRequired(true)
-						.build();
-		authorizationCodeRequestAuthenticationResult.setAuthenticated(true);
+		OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthenticationResult =
+				new OAuth2AuthorizationConsentAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						STATE, new HashSet<>(), null);	// No scopes previously approved
+		authorizationConsentAuthenticationResult.setAuthenticated(true);
 		when(this.authenticationManager.authenticate(any()))
-				.thenReturn(authorizationCodeRequestAuthenticationResult);
+				.thenReturn(authorizationConsentAuthenticationResult);
 
 		MockHttpServletRequest request = createAuthorizationRequest(registeredClient);
 		MockHttpServletResponse response = new MockHttpServletResponse();
@@ -464,14 +471,13 @@ public class OAuth2AuthorizationEndpointFilterTests {
 					scopes.addAll(requestedScopes);
 				})
 				.build();
-		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult =
-				authorizationCodeRequestAuthentication(registeredClient, this.principal)
-						.scopes(new HashSet<>())	// No scopes previously approved
-						.consentRequired(true)
-						.build();
-		authorizationCodeRequestAuthenticationResult.setAuthenticated(true);
+		OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthenticationResult =
+				new OAuth2AuthorizationConsentAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						STATE, new HashSet<>(), null);	// No scopes previously approved
+		authorizationConsentAuthenticationResult.setAuthenticated(true);
 		when(this.authenticationManager.authenticate(any()))
-				.thenReturn(authorizationCodeRequestAuthenticationResult);
+				.thenReturn(authorizationConsentAuthenticationResult);
 
 		MockHttpServletRequest request = createAuthorizationRequest(registeredClient);
 		MockHttpServletResponse response = new MockHttpServletResponse();
@@ -500,14 +506,13 @@ public class OAuth2AuthorizationEndpointFilterTests {
 					scopes.addAll(requestedScopes);
 				})
 				.build();
-		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult =
-				authorizationCodeRequestAuthentication(registeredClient, this.principal)
-						.scopes(approvedScopes)
-						.consentRequired(true)
-						.build();
-		authorizationCodeRequestAuthenticationResult.setAuthenticated(true);
+		OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthenticationResult =
+				new OAuth2AuthorizationConsentAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal,
+						STATE, approvedScopes, null);
+		authorizationConsentAuthenticationResult.setAuthenticated(true);
 		when(this.authenticationManager.authenticate(any()))
-				.thenReturn(authorizationCodeRequestAuthenticationResult);
+				.thenReturn(authorizationConsentAuthenticationResult);
 
 		MockHttpServletRequest request = createAuthorizationRequest(registeredClient);
 		MockHttpServletResponse response = new MockHttpServletResponse();
@@ -532,9 +537,9 @@ public class OAuth2AuthorizationEndpointFilterTests {
 	public void doFilterWhenAuthorizationRequestAuthenticatedThenAuthorizationResponse() throws Exception {
 		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
 		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult =
-				authorizationCodeRequestAuthentication(registeredClient, this.principal)
-						.authorizationCode(this.authorizationCode)
-						.build();
+				new OAuth2AuthorizationCodeRequestAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal, this.authorizationCode,
+						registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes());
 		authorizationCodeRequestAuthenticationResult.setAuthenticated(true);
 		when(this.authenticationManager.authenticate(any()))
 				.thenReturn(authorizationCodeRequestAuthenticationResult);
@@ -568,9 +573,9 @@ public class OAuth2AuthorizationEndpointFilterTests {
 				})
 				.build();
 		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult =
-				authorizationCodeRequestAuthentication(registeredClient, this.principal)
-						.authorizationCode(this.authorizationCode)
-						.build();
+				new OAuth2AuthorizationCodeRequestAuthenticationToken(
+						AUTHORIZATION_URI, registeredClient.getClientId(), principal, this.authorizationCode,
+						registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes());
 		authorizationCodeRequestAuthenticationResult.setAuthenticated(true);
 		when(this.authenticationManager.authenticate(any()))
 				.thenReturn(authorizationCodeRequestAuthenticationResult);
@@ -647,15 +652,6 @@ public class OAuth2AuthorizationEndpointFilterTests {
 		return request;
 	}
 
-	private static OAuth2AuthorizationCodeRequestAuthenticationToken.Builder authorizationCodeRequestAuthentication(
-			RegisteredClient registeredClient, Authentication principal) {
-		return OAuth2AuthorizationCodeRequestAuthenticationToken.with(registeredClient.getClientId(), principal)
-				.authorizationUri("https://provider.com/oauth2/authorize")
-				.redirectUri(registeredClient.getRedirectUris().iterator().next())
-				.scopes(registeredClient.getScopes())
-				.state("state");
-	}
-
 	private static String scopeCheckbox(String scope) {
 		return MessageFormat.format(
 				"<input class=\"form-check-input\" type=\"checkbox\" name=\"scope\" value=\"{0}\" id=\"{0}\">",