浏览代码

Extract AuthenticationProvider from OAuth2AuthorizationEndpointFilter

Closes gh-340
Joe Grandja 4 年之前
父节点
当前提交
08ba07d676
共有 10 个文件被更改,包括 2330 次插入1600 次删除
  1. 9 3
      oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java
  2. 72 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationCodeRequestAuthenticationException.java
  3. 1 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java
  4. 478 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java
  5. 320 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationToken.java
  6. 236 643
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilter.java
  7. 1 2
      oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationCodeGrantTests.java
  8. 861 0
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProviderTests.java
  9. 200 0
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationTokenTests.java
  10. 152 952
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilterTests.java

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

@@ -32,6 +32,7 @@ import org.springframework.security.config.annotation.web.configurers.ExceptionH
 import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationProvider;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationProvider;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenIntrospectionAuthenticationProvider;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenRevocationAuthenticationProvider;
@@ -221,6 +222,13 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
 		}
 		builder.authenticationProvider(postProcess(clientAuthenticationProvider));
 
+		OAuth2AuthorizationCodeRequestAuthenticationProvider authorizationCodeRequestAuthenticationProvider =
+				new OAuth2AuthorizationCodeRequestAuthenticationProvider(
+						OAuth2ConfigurerUtils.getRegisteredClientRepository(builder),
+						OAuth2ConfigurerUtils.getAuthorizationService(builder),
+						OAuth2ConfigurerUtils.getAuthorizationConsentService(builder));
+		builder.authenticationProvider(postProcess(authorizationCodeRequestAuthenticationProvider));
+
 		OAuth2TokenIntrospectionAuthenticationProvider tokenIntrospectionAuthenticationProvider =
 				new OAuth2TokenIntrospectionAuthenticationProvider(
 						OAuth2ConfigurerUtils.getRegisteredClientRepository(builder),
@@ -285,9 +293,7 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
 
 		OAuth2AuthorizationEndpointFilter authorizationEndpointFilter =
 				new OAuth2AuthorizationEndpointFilter(
-						OAuth2ConfigurerUtils.getRegisteredClientRepository(builder),
-						OAuth2ConfigurerUtils.getAuthorizationService(builder),
-						OAuth2ConfigurerUtils.getAuthorizationConsentService(builder),
+						authenticationManager,
 						providerSettings.authorizationEndpoint());
 		if (StringUtils.hasText(this.consentPage)) {
 			authorizationEndpointFilter.setUserConsentUri(this.consentPage);

+ 72 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationCodeRequestAuthenticationException.java

@@ -0,0 +1,72 @@
+/*
+ * Copyright 2020-2021 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;
+
+import org.springframework.lang.Nullable;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationProvider;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken;
+
+/**
+ * This exception is thrown by {@link OAuth2AuthorizationCodeRequestAuthenticationProvider}
+ * when an attempt to authenticate the OAuth 2.0 Authorization Request (or Consent) fails.
+ *
+ * @author Joe Grandja
+ * @since 0.1.2
+ * @see OAuth2AuthorizationCodeRequestAuthenticationToken
+ * @see OAuth2AuthorizationCodeRequestAuthenticationProvider
+ */
+public class OAuth2AuthorizationCodeRequestAuthenticationException extends OAuth2AuthenticationException {
+	private final OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication;
+
+	/**
+	 * Constructs an {@code OAuth2AuthorizationCodeRequestAuthenticationException} using the provided parameters.
+	 *
+	 * @param error the {@link OAuth2Error OAuth 2.0 Error}
+	 * @param authorizationCodeRequestAuthentication the {@link Authentication} instance of the OAuth 2.0 Authorization Request (or Consent)
+	 */
+	public OAuth2AuthorizationCodeRequestAuthenticationException(OAuth2Error error,
+			@Nullable OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication) {
+		super(error);
+		this.authorizationCodeRequestAuthentication = authorizationCodeRequestAuthentication;
+	}
+
+	/**
+	 * Constructs an {@code OAuth2AuthorizationCodeRequestAuthenticationException} using the provided parameters.
+	 *
+	 * @param error the {@link OAuth2Error OAuth 2.0 Error}
+	 * @param cause the root cause
+	 * @param authorizationCodeRequestAuthentication the {@link Authentication} instance of the OAuth 2.0 Authorization Request (or Consent)
+	 */
+	public OAuth2AuthorizationCodeRequestAuthenticationException(OAuth2Error error, Throwable cause,
+			@Nullable OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication) {
+		super(error, cause);
+		this.authorizationCodeRequestAuthentication = authorizationCodeRequestAuthentication;
+	}
+
+	/**
+	 * Returns the {@link Authentication} instance of the OAuth 2.0 Authorization Request (or Consent), or {@code null} if not available.
+	 *
+	 * @return the {@link OAuth2AuthorizationCodeRequestAuthenticationToken}
+	 */
+	@Nullable
+	public OAuth2AuthorizationCodeRequestAuthenticationToken getAuthorizationCodeRequestAuthentication() {
+		return this.authorizationCodeRequestAuthentication;
+	}
+
+}

+ 1 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java

@@ -61,6 +61,7 @@ import static org.springframework.security.oauth2.server.authorization.authentic
  * @since 0.0.1
  * @see OAuth2AuthorizationCodeAuthenticationToken
  * @see OAuth2AccessTokenAuthenticationToken
+ * @see OAuth2AuthorizationCodeRequestAuthenticationProvider
  * @see OAuth2AuthorizationService
  * @see JwtEncoder
  * @see OAuth2TokenCustomizer

+ 478 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java

@@ -0,0 +1,478 @@
+/*
+ * Copyright 2020-2021 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.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+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.crypto.keygen.Base64StringKeyGenerator;
+import org.springframework.security.crypto.keygen.StringKeyGenerator;
+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.OAuth2TokenType;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
+import org.springframework.security.oauth2.core.oidc.OidcScopes;
+import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCodeRequestAuthenticationException;
+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.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+import org.springframework.web.util.UriComponents;
+import org.springframework.web.util.UriComponentsBuilder;
+
+/**
+ * An {@link AuthenticationProvider} implementation for the OAuth 2.0 Authorization Request (and Consent)
+ * used in the Authorization Code Grant.
+ *
+ * @author Joe Grandja
+ * @since 0.1.2
+ * @see OAuth2AuthorizationCodeRequestAuthenticationToken
+ * @see OAuth2AuthorizationCodeAuthenticationProvider
+ * @see RegisteredClientRepository
+ * @see OAuth2AuthorizationService
+ * @see OAuth2AuthorizationConsentService
+ * @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1">Section 4.1.1 Authorization Request</a>
+ */
+public class OAuth2AuthorizationCodeRequestAuthenticationProvider implements AuthenticationProvider {
+	private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
+	private static final String PKCE_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7636#section-4.4.1";
+	private static final Pattern LOOPBACK_ADDRESS_PATTERN =
+			Pattern.compile("^127(?:\\.[0-9]+){0,2}\\.[0-9]+$|^\\[(?:0*:)*?:?0*1]$");
+	private final RegisteredClientRepository registeredClientRepository;
+	private final OAuth2AuthorizationService authorizationService;
+	private final OAuth2AuthorizationConsentService authorizationConsentService;
+	private final StringKeyGenerator codeGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
+	private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder());
+
+	/**
+	 * Constructs an {@code OAuth2AuthorizationCodeRequestAuthenticationProvider} using the provided parameters.
+	 *
+	 * @param registeredClientRepository the repository of registered clients
+	 * @param authorizationService the authorization service
+	 * @param authorizationConsentService the authorization consent service
+	 */
+	public OAuth2AuthorizationCodeRequestAuthenticationProvider(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 {
+		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
+				(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
+
+		return authorizationCodeRequestAuthentication.isConsent() ?
+				authenticateAuthorizationConsent(authentication) :
+				authenticateAuthorizationRequest(authentication);
+	}
+
+	@Override
+	public boolean supports(Class<?> authentication) {
+		return OAuth2AuthorizationCodeRequestAuthenticationToken.class.isAssignableFrom(authentication);
+	}
+
+	private Authentication authenticateAuthorizationRequest(Authentication authentication) throws AuthenticationException {
+		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
+				(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
+
+		RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(
+				authorizationCodeRequestAuthentication.getClientId());
+		if (registeredClient == null) {
+			throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID,
+					authorizationCodeRequestAuthentication, null);
+		}
+
+		if (StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) {
+			if (!isValidRedirectUri(authorizationCodeRequestAuthentication.getRedirectUri(), registeredClient)) {
+				throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
+						authorizationCodeRequestAuthentication, registeredClient);
+			}
+		} else if (authorizationCodeRequestAuthentication.getScopes().contains(OidcScopes.OPENID) ||
+				registeredClient.getRedirectUris().size() != 1) {
+			// redirect_uri is REQUIRED for OpenID Connect
+			throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
+					authorizationCodeRequestAuthentication, registeredClient);
+		}
+
+		if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.AUTHORIZATION_CODE)) {
+			throwError(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT, OAuth2ParameterNames.CLIENT_ID,
+					authorizationCodeRequestAuthentication, registeredClient);
+		}
+
+		Set<String> requestedScopes = authorizationCodeRequestAuthentication.getScopes();
+		Set<String> allowedScopes = registeredClient.getScopes();
+		if (!requestedScopes.isEmpty() && !allowedScopes.containsAll(requestedScopes)) {
+			throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE,
+					authorizationCodeRequestAuthentication, registeredClient);
+		}
+
+		// code_challenge (REQUIRED for public clients) - RFC 7636 (PKCE)
+		String codeChallenge = (String) authorizationCodeRequestAuthentication.getAdditionalParameters().get(PkceParameterNames.CODE_CHALLENGE);
+		if (StringUtils.hasText(codeChallenge)) {
+			String codeChallengeMethod = (String) authorizationCodeRequestAuthentication.getAdditionalParameters().get(PkceParameterNames.CODE_CHALLENGE_METHOD);
+			if (StringUtils.hasText(codeChallengeMethod)) {
+				if (!"S256".equals(codeChallengeMethod) && !"plain".equals(codeChallengeMethod)) {
+					throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD, PKCE_ERROR_URI,
+							authorizationCodeRequestAuthentication, registeredClient, null);
+				}
+			}
+		} else if (registeredClient.getClientSettings().requireProofKey()) {
+			throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE, PKCE_ERROR_URI,
+					authorizationCodeRequestAuthentication, registeredClient, null);
+		}
+
+		// ---------------
+		// The request is valid - ensure the resource owner is authenticated
+		// ---------------
+
+		Authentication principal = (Authentication) authorizationCodeRequestAuthentication.getPrincipal();
+		if (!isPrincipalAuthenticated(principal)) {
+			// Return the authorization request as-is where isAuthenticated() is false
+			return authorizationCodeRequestAuthentication;
+		}
+
+		OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
+				.authorizationUri(authorizationCodeRequestAuthentication.getAuthorizationUri())
+				.clientId(registeredClient.getClientId())
+				.redirectUri(authorizationCodeRequestAuthentication.getRedirectUri())
+				.scopes(requestedScopes)
+				.state(authorizationCodeRequestAuthentication.getState())
+				.additionalParameters(authorizationCodeRequestAuthentication.getAdditionalParameters())
+				.build();
+
+		OAuth2AuthorizationConsent currentAuthorizationConsent = this.authorizationConsentService.findById(
+				registeredClient.getId(), principal.getName());
+
+		if (requireAuthorizationConsent(registeredClient, authorizationRequest, currentAuthorizationConsent)) {
+			String state = this.stateGenerator.generateKey();
+			OAuth2Authorization authorization = authorizationBuilder(registeredClient, principal, authorizationRequest)
+					.attribute(OAuth2ParameterNames.STATE, state)
+					.build();
+			this.authorizationService.save(authorization);
+
+			// TODO Need to remove 'in-flight' authorization if consent step is not completed (e.g. approved or cancelled)
+
+			Set<String> currentAuthorizedScopes = currentAuthorizationConsent != null ?
+					currentAuthorizationConsent.getScopes() : null;
+
+			return OAuth2AuthorizationCodeRequestAuthenticationToken.with(registeredClient.getClientId(), principal)
+					.authorizationUri(authorizationRequest.getAuthorizationUri())
+					.scopes(currentAuthorizedScopes)
+					.state(state)
+					.consentRequired(true)
+					.build();
+		}
+
+		OAuth2AuthorizationCode authorizationCode = createAuthorizationCode();
+		OAuth2Authorization authorization = authorizationBuilder(registeredClient, principal, authorizationRequest)
+				.token(authorizationCode)
+				.attribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizationRequest.getScopes())
+				.build();
+		this.authorizationService.save(authorization);
+
+//		TODO security checks for code parameter
+//		The authorization code MUST expire shortly after it is issued to mitigate the risk of leaks.
+//		A maximum authorization code lifetime of 10 minutes is RECOMMENDED.
+//		The client MUST NOT use the authorization code more than once.
+//		If an authorization code is used more than once, the authorization server MUST deny the request
+//		and SHOULD revoke (when possible) all tokens previously issued based on that authorization code.
+//		The authorization code is bound to the client identifier and redirection URI.
+
+		String redirectUri = authorizationRequest.getRedirectUri();
+		if (!StringUtils.hasText(redirectUri)) {
+			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();
+	}
+
+	private OAuth2AuthorizationCode createAuthorizationCode() {
+		Instant issuedAt = Instant.now();
+		Instant expiresAt = issuedAt.plus(5, ChronoUnit.MINUTES);		// TODO Allow configuration for authorization code time-to-live
+		return new OAuth2AuthorizationCode(this.codeGenerator.generateKey(), issuedAt, expiresAt);
+	}
+
+	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 (authorizedScopes.isEmpty() && currentAuthorizedScopes.isEmpty()) {
+			// Authorization consent denied
+			this.authorizationService.remove(authorization);
+			throwError(OAuth2ErrorCodes.ACCESS_DENIED, OAuth2ParameterNames.CLIENT_ID,
+					authorizationCodeRequestAuthentication, registeredClient, authorizationRequest);
+		}
+
+		if (requestedScopes.contains(OidcScopes.OPENID)) {
+			// 'openid' scope is auto-approved as it does not require consent
+			authorizedScopes.add(OidcScopes.OPENID);
+		}
+
+		if (!currentAuthorizedScopes.isEmpty()) {
+			for (String requestedScope : requestedScopes) {
+				if (currentAuthorizedScopes.contains(requestedScope)) {
+					authorizedScopes.add(requestedScope);
+				}
+			}
+		}
+
+		if (!authorizedScopes.isEmpty() && !authorizedScopes.equals(currentAuthorizedScopes)) {
+			OAuth2AuthorizationConsent.Builder authorizationConsentBuilder;
+			if (currentAuthorizationConsent != null) {
+				authorizationConsentBuilder = OAuth2AuthorizationConsent.from(currentAuthorizationConsent);
+			} else {
+				authorizationConsentBuilder = OAuth2AuthorizationConsent.withId(
+						authorization.getRegisteredClientId(), authorization.getPrincipalName());
+			}
+			authorizedScopes.forEach(authorizationConsentBuilder::scope);
+			OAuth2AuthorizationConsent authorizationConsent = authorizationConsentBuilder.build();
+			this.authorizationConsentService.save(authorizationConsent);
+		}
+
+		OAuth2AuthorizationCode authorizationCode = createAuthorizationCode();
+
+		OAuth2Authorization updatedAuthorization = OAuth2Authorization.from(authorization)
+				.token(authorizationCode)
+				.attributes(attrs -> {
+					attrs.remove(OAuth2ParameterNames.STATE);
+					attrs.put(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizedScopes);
+				})
+				.build();
+		this.authorizationService.save(updatedAuthorization);
+
+		String redirectUri = authorizationRequest.getRedirectUri();
+		if (!StringUtils.hasText(redirectUri)) {
+			redirectUri = registeredClient.getRedirectUris().iterator().next();
+		}
+
+		return OAuth2AuthorizationCodeRequestAuthenticationToken.with(registeredClient.getClientId(), principal)
+				.authorizationUri(authorizationRequest.getAuthorizationUri())
+				.redirectUri(redirectUri)
+				.scopes(authorizedScopes)
+				.state(authorizationRequest.getState())
+				.authorizationCode(authorizationCode)
+				.build();
+	}
+
+	private static OAuth2Authorization.Builder authorizationBuilder(RegisteredClient registeredClient, Authentication principal,
+			OAuth2AuthorizationRequest authorizationRequest) {
+		return OAuth2Authorization.withRegisteredClient(registeredClient)
+				.principalName(principal.getName())
+				.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
+				.attribute(Principal.class.getName(), principal)
+				.attribute(OAuth2AuthorizationRequest.class.getName(), authorizationRequest);
+	}
+
+	private static boolean requireAuthorizationConsent(RegisteredClient registeredClient,
+			OAuth2AuthorizationRequest authorizationRequest, OAuth2AuthorizationConsent authorizationConsent) {
+
+		if (!registeredClient.getClientSettings().requireUserConsent()) {
+			return false;
+		}
+		// 'openid' scope does not require consent
+		if (authorizationRequest.getScopes().contains(OidcScopes.OPENID) &&
+				authorizationRequest.getScopes().size() == 1) {
+			return false;
+		}
+
+		if (authorizationConsent != null &&
+				authorizationConsent.getScopes().containsAll(authorizationRequest.getScopes())) {
+			return false;
+		}
+
+		return true;
+	}
+
+	private static boolean isValidRedirectUri(String requestedRedirectUri, RegisteredClient registeredClient) {
+		UriComponents requestedRedirect;
+		try {
+			requestedRedirect = UriComponentsBuilder.fromUriString(requestedRedirectUri).build();
+			if (requestedRedirect.getFragment() != null) {
+				return false;
+			}
+		} catch (Exception ex) {
+			return false;
+		}
+
+		String requestedRedirectHost = requestedRedirect.getHost();
+		if (requestedRedirectHost == null || requestedRedirectHost.equals("localhost")) {
+			// As per https://tools.ietf.org/html/draft-ietf-oauth-v2-1-01#section-9.7.1
+			// While redirect URIs using localhost (i.e.,
+			// "http://localhost:{port}/{path}") function similarly to loopback IP
+			// redirects described in Section 10.3.3, the use of "localhost" is NOT RECOMMENDED.
+			return false;
+		}
+		if (!LOOPBACK_ADDRESS_PATTERN.matcher(requestedRedirectHost).matches()) {
+			// As per https://tools.ietf.org/html/draft-ietf-oauth-v2-1-01#section-9.7
+			// When comparing client redirect URIs against pre-registered URIs,
+			// authorization servers MUST utilize exact string matching.
+			return registeredClient.getRedirectUris().contains(requestedRedirectUri);
+		}
+
+		// As per https://tools.ietf.org/html/draft-ietf-oauth-v2-1-01#section-10.3.3
+		// The authorization server MUST allow any port to be specified at the
+		// time of the request for loopback IP redirect URIs, to accommodate
+		// clients that obtain an available ephemeral port from the operating
+		// system at the time of the request.
+		for (String registeredRedirectUri : registeredClient.getRedirectUris()) {
+			UriComponentsBuilder registeredRedirect = UriComponentsBuilder.fromUriString(registeredRedirectUri);
+			registeredRedirect.port(requestedRedirect.getPort());
+			if (registeredRedirect.build().toString().equals(requestedRedirect.toString())) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	private static boolean isPrincipalAuthenticated(Authentication principal) {
+		return principal != null &&
+				!AnonymousAuthenticationToken.class.isAssignableFrom(principal.getClass()) &&
+				principal.isAuthenticated();
+	}
+
+	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, "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1",
+				authorizationCodeRequestAuthentication, registeredClient, authorizationRequest);
+	}
+
+	private static void throwError(String errorCode, String parameterName, String errorUri,
+			OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
+			RegisteredClient registeredClient, OAuth2AuthorizationRequest authorizationRequest) {
+
+		boolean redirectOnError = true;
+		if (errorCode.equals(OAuth2ErrorCodes.INVALID_REQUEST) &&
+				(parameterName.equals(OAuth2ParameterNames.CLIENT_ID) ||
+						parameterName.equals(OAuth2ParameterNames.REDIRECT_URI) ||
+						parameterName.equals(OAuth2ParameterNames.STATE))) {
+			redirectOnError = false;
+		}
+
+		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();
+			authorizationCodeRequestAuthenticationResult.setAuthenticated(authorizationCodeRequestAuthentication.isAuthenticated());
+		} else if (!redirectOnError && StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) {
+			authorizationCodeRequestAuthenticationResult = from(authorizationCodeRequestAuthentication)
+					.redirectUri(null)		// Prevent redirects
+					.build();
+			authorizationCodeRequestAuthenticationResult.setAuthenticated(authorizationCodeRequestAuthentication.isAuthenticated());
+		}
+
+		OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, errorUri);
+		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;
+	}
+
+	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())
+				.consentRequired(authorizationCodeRequestAuthentication.isConsentRequired())
+				.consent(authorizationCodeRequestAuthentication.isConsent())
+				.authorizationCode(authorizationCodeRequestAuthentication.getAuthorizationCode());
+	}
+
+}

+ 320 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationToken.java

@@ -0,0 +1,320 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.server.authorization.authentication;
+
+import java.io.Serializable;
+import java.util.Collections;
+import 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.core.Version;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * An {@link Authentication} implementation for the OAuth 2.0 Authorization Request (and Consent)
+ * used in the Authorization Code Grant.
+ *
+ * @author Joe Grandja
+ * @since 0.1.2
+ * @see OAuth2AuthorizationCodeRequestAuthenticationProvider
+ */
+public final class OAuth2AuthorizationCodeRequestAuthenticationToken extends AbstractAuthenticationToken {
+	private static final long serialVersionUID = Version.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 OAuth2AuthorizationCodeRequestAuthenticationToken() {
+		super(Collections.emptyList());
+	}
+
+	@Override
+	public Object getPrincipal() {
+		return this.principal;
+	}
+
+	@Override
+	public Object getCredentials() {
+		return "";
+	}
+
+	/**
+	 * Returns the authorization URI.
+	 *
+	 * @return the authorization URI
+	 */
+	public String getAuthorizationUri() {
+		return this.authorizationUri;
+	}
+
+	/**
+	 * Returns the client identifier.
+	 *
+	 * @return the client identifier
+	 */
+	public String getClientId() {
+		return this.clientId;
+	}
+
+	/**
+	 * Returns the redirect uri.
+	 *
+	 * @return the redirect uri
+	 */
+	@Nullable
+	public String getRedirectUri() {
+		return this.redirectUri;
+	}
+
+	/**
+	 * Returns the 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.
+	 *
+	 * @return the state
+	 */
+	@Nullable
+	public String getState() {
+		return this.state;
+	}
+
+	/**
+	 * 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.
+	 *
+	 * @return {@code true} if authorization consent is required, {@code false} otherwise
+	 */
+	public boolean isConsentRequired() {
+		return this.consentRequired;
+	}
+
+	/**
+	 * Returns {@code true} if this {@code Authentication} represents an authorization consent request,
+	 * {@code false} otherwise.
+	 *
+	 * @return {@code true} if this {@code Authentication} represents an authorization consent request, {@code false} otherwise
+	 */
+	public boolean isConsent() {
+		return this.consent;
+	}
+
+	/**
+	 * Returns the {@link OAuth2AuthorizationCode}.
+	 *
+	 * @return the {@link OAuth2AuthorizationCode}
+	 */
+	@Nullable
+	public OAuth2AuthorizationCode getAuthorizationCode() {
+		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 = Version.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;
+		}
+
+	}
+
+}

文件差异内容过多而无法显示
+ 236 - 643
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilter.java


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

@@ -410,7 +410,6 @@ public class OAuth2AuthorizationCodeGrantTests {
 				.param(OAuth2ParameterNames.SCOPE, "message.read")
 				.param(OAuth2ParameterNames.SCOPE, "message.write")
 				.param(OAuth2ParameterNames.STATE, "state")
-				.param("consent_action", "approve")
 				.with(user("user")))
 				.andExpect(status().is3xxRedirection())
 				.andReturn();
@@ -455,7 +454,7 @@ public class OAuth2AuthorizationCodeGrantTests {
 				.andExpect(status().is3xxRedirection())
 				.andReturn();
 		String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
-		assertThat(redirectedUrl).matches("/oauth2/consent\\?scope=.+&client_id=.+&state=.+");
+		assertThat(redirectedUrl).matches("http://localhost/oauth2/consent\\?scope=.+&client_id=.+&state=.+");
 
 		String locationHeader = URLDecoder.decode(redirectedUrl, StandardCharsets.UTF_8.name());
 		UriComponents uriComponents = UriComponentsBuilder.fromUriString(locationHeader).build();

+ 861 - 0
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProviderTests.java

@@ -0,0 +1,861 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.server.authorization.authentication;
+
+import java.security.Principal;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+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.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.core.OAuth2TokenType;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
+import org.springframework.security.oauth2.core.oidc.OidcScopes;
+import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCodeRequestAuthenticationException;
+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.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 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 OAuth2AuthorizationCodeRequestAuthenticationProvider}.
+ *
+ * @author Joe Grandja
+ */
+public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
+	private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
+	private RegisteredClientRepository registeredClientRepository;
+	private OAuth2AuthorizationService authorizationService;
+	private OAuth2AuthorizationConsentService authorizationConsentService;
+	private OAuth2AuthorizationCodeRequestAuthenticationProvider 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 OAuth2AuthorizationCodeRequestAuthenticationProvider(
+				this.registeredClientRepository, this.authorizationService, this.authorizationConsentService);
+		this.principal = new TestingAuthenticationToken("principalName", "password");
+		this.principal.setAuthenticated(true);
+	}
+
+	@Test
+	public void constructorWhenRegisteredClientRepositoryNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> new OAuth2AuthorizationCodeRequestAuthenticationProvider(
+				null, this.authorizationService, this.authorizationConsentService))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("registeredClientRepository cannot be null");
+	}
+
+	@Test
+	public void constructorWhenAuthorizationServiceNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> new OAuth2AuthorizationCodeRequestAuthenticationProvider(
+				this.registeredClientRepository, null, this.authorizationConsentService))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("authorizationService cannot be null");
+	}
+
+	@Test
+	public void constructorWhenAuthorizationConsentServiceNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> new OAuth2AuthorizationCodeRequestAuthenticationProvider(
+				this.registeredClientRepository, this.authorizationService, null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("authorizationConsentService cannot be null");
+	}
+
+	@Test
+	public void supportsWhenTypeOAuth2AuthorizationCodeRequestAuthenticationTokenThenReturnTrue() {
+		assertThat(this.authenticationProvider.supports(OAuth2AuthorizationCodeRequestAuthenticationToken.class)).isTrue();
+	}
+
+	@Test
+	public void authenticateWhenInvalidClientIdThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
+				authorizationCodeRequestAuthentication(registeredClient, this.principal)
+						.build();
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
+				.satisfies(ex ->
+						assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
+								OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID, null)
+				);
+	}
+
+	// gh-243
+	@Test
+	public void authenticateWhenInvalidRedirectUriHostThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
+				authorizationCodeRequestAuthentication(registeredClient, this.principal)
+						.redirectUri("https:///invalid")
+						.build();
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
+				.satisfies(ex ->
+						assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
+								OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, null)
+				);
+	}
+
+	// gh-243
+	@Test
+	public void authenticateWhenInvalidRedirectUriFragmentThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
+				authorizationCodeRequestAuthentication(registeredClient, this.principal)
+						.redirectUri("https://example.com#fragment")
+						.build();
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
+				.satisfies(ex ->
+						assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
+								OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, null)
+				);
+	}
+
+	// gh-243
+	@Test
+	public void authenticateWhenRedirectUriLocalhostThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
+				authorizationCodeRequestAuthentication(registeredClient, this.principal)
+						.redirectUri("https://localhost:5000")
+						.build();
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
+				.satisfies(ex ->
+						assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
+								OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, null)
+				);
+	}
+
+	@Test
+	public void authenticateWhenUnregisteredRedirectUriThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
+				authorizationCodeRequestAuthentication(registeredClient, this.principal)
+						.redirectUri("https://invalid-example.com")
+						.build();
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
+				.satisfies(ex ->
+						assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
+								OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, null)
+				);
+	}
+
+	// gh-243
+	@Test
+	public void authenticateWhenRedirectUriIPv4LoopbackAndDifferentPortThenReturnAuthorizationCode() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.redirectUri("https://127.0.0.1:8080")
+				.build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
+				authorizationCodeRequestAuthentication(registeredClient, this.principal)
+						.redirectUri("https://127.0.0.1:5000")
+						.build();
+
+		OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult =
+				(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication);
+
+		assertAuthorizationCodeRequestWithAuthorizationCodeResult(registeredClient, authentication, authenticationResult);
+	}
+
+	// gh-243
+	@Test
+	public void authenticateWhenRedirectUriIPv6LoopbackAndDifferentPortThenReturnAuthorizationCode() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.redirectUri("https://[::1]:8080")
+				.build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
+				authorizationCodeRequestAuthentication(registeredClient, this.principal)
+						.redirectUri("https://[::1]:5000")
+						.build();
+
+		OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult =
+				(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication);
+
+		assertAuthorizationCodeRequestWithAuthorizationCodeResult(registeredClient, authentication, authenticationResult);
+	}
+
+	@Test
+	public void authenticateWhenMissingRedirectUriAndMultipleRegisteredThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().redirectUri("https://example2.com").build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
+				authorizationCodeRequestAuthentication(registeredClient, this.principal)
+						.redirectUri(null)
+						.build();
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
+				.satisfies(ex ->
+						assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
+								OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, null)
+				);
+	}
+
+	@Test
+	public void authenticateWhenAuthenticationRequestMissingRedirectUriThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
+		// redirect_uri is REQUIRED for OpenID Connect requests
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.scope(OidcScopes.OPENID)
+				.build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
+				authorizationCodeRequestAuthentication(registeredClient, this.principal)
+						.redirectUri(null)
+						.build();
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
+				.satisfies(ex ->
+						assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
+								OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, null)
+				);
+	}
+
+	@Test
+	public void authenticateWhenClientNotAuthorizedToRequestCodeThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.authorizationGrantTypes(Set::clear)
+				.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
+				.build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
+				authorizationCodeRequestAuthentication(registeredClient, this.principal)
+						.build();
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
+				.satisfies(ex ->
+						assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
+								OAuth2ErrorCodes.UNAUTHORIZED_CLIENT, OAuth2ParameterNames.CLIENT_ID, authentication.getRedirectUri())
+				);
+	}
+
+	@Test
+	public void authenticateWhenInvalidScopeThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
+				authorizationCodeRequestAuthentication(registeredClient, this.principal)
+						.scopes(Collections.singleton("invalid-scope"))
+						.build();
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
+				.satisfies(ex ->
+						assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
+								OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE, authentication.getRedirectUri())
+				);
+	}
+
+	@Test
+	public void authenticateWhenPkceRequiredAndMissingCodeChallengeThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.clientSettings(clientSettings -> clientSettings.requireProofKey(true))
+				.build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
+				authorizationCodeRequestAuthentication(registeredClient, this.principal)
+						.build();
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
+				.satisfies(ex ->
+						assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
+								OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE, authentication.getRedirectUri())
+				);
+	}
+
+	@Test
+	public void authenticateWhenPkceUnsupportedCodeChallengeMethodThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+		Map<String, Object> additionalParameters = new HashMap<>();
+		additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, "code-challenge");
+		additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "unsupported");
+		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
+				authorizationCodeRequestAuthentication(registeredClient, this.principal)
+						.additionalParameters(additionalParameters)
+						.build();
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+				.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
+				.satisfies(ex ->
+						assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
+								OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD, authentication.getRedirectUri())
+				);
+	}
+
+	@Test
+	public void authenticateWhenPrincipalNotAuthenticatedThenReturnAuthorizationCodeRequest() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+		this.principal.setAuthenticated(false);
+
+		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
+				authorizationCodeRequestAuthentication(registeredClient, this.principal)
+						.build();
+
+		OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult =
+				(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication);
+
+		assertThat(authenticationResult).isSameAs(authentication);
+		assertThat(authenticationResult.isAuthenticated()).isFalse();
+	}
+
+	@Test
+	public void authenticateWhenRequireAuthorizationConsentThenReturnAuthorizationConsent() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.clientSettings(clientSettings -> clientSettings.requireUserConsent(true))
+				.build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+
+		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
+				authorizationCodeRequestAuthentication(registeredClient, this.principal)
+						.build();
+
+		OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult =
+				(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication);
+
+		ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
+		verify(this.authorizationService).save(authorizationCaptor.capture());
+		OAuth2Authorization authorization = authorizationCaptor.getValue();
+
+		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
+		assertThat(authorizationRequest.getGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
+		assertThat(authorizationRequest.getResponseType()).isEqualTo(OAuth2AuthorizationResponseType.CODE);
+		assertThat(authorizationRequest.getAuthorizationUri()).isEqualTo(authentication.getAuthorizationUri());
+		assertThat(authorizationRequest.getClientId()).isEqualTo(registeredClient.getClientId());
+		assertThat(authorizationRequest.getRedirectUri()).isEqualTo(authentication.getRedirectUri());
+		assertThat(authorizationRequest.getScopes()).isEqualTo(authentication.getScopes());
+		assertThat(authorizationRequest.getState()).isEqualTo(authentication.getState());
+		assertThat(authorizationRequest.getAdditionalParameters()).isEqualTo(authentication.getAdditionalParameters());
+
+		assertThat(authorization.getRegisteredClientId()).isEqualTo(registeredClient.getId());
+		assertThat(authorization.getPrincipalName()).isEqualTo(this.principal.getName());
+		assertThat(authorization.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
+		assertThat(authorization.<Authentication>getAttribute(Principal.class.getName())).isEqualTo(this.principal);
+		String state = authorization.getAttribute(OAuth2ParameterNames.STATE);
+		assertThat(state).isNotNull();
+		assertThat(state).isNotEqualTo(authentication.getState());
+
+		assertThat(authenticationResult.getClientId()).isEqualTo(registeredClient.getClientId());
+		assertThat(authenticationResult.getPrincipal()).isEqualTo(this.principal);
+		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();
+	}
+
+	@Test
+	public void authenticateWhenRequireAuthorizationConsentAndOnlyOpenidScopeRequestedThenAuthorizationConsentNotRequired() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.clientSettings(clientSettings -> clientSettings.requireUserConsent(true))
+				.scopes(scopes -> {
+					scopes.clear();
+					scopes.add(OidcScopes.OPENID);
+				})
+				.build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+
+		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
+				authorizationCodeRequestAuthentication(registeredClient, this.principal)
+						.build();
+
+		OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult =
+				(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication);
+
+		assertAuthorizationCodeRequestWithAuthorizationCodeResult(registeredClient, authentication, authenticationResult);
+	}
+
+	@Test
+	public void authenticateWhenRequireAuthorizationConsentAndAllPreviouslyApprovedThenAuthorizationConsentNotRequired() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.clientSettings(clientSettings -> clientSettings.requireUserConsent(true))
+				.build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+
+		OAuth2AuthorizationConsent.Builder builder =
+				OAuth2AuthorizationConsent.withId(registeredClient.getId(), this.principal.getName());
+		registeredClient.getScopes().forEach(builder::scope);
+		OAuth2AuthorizationConsent previousAuthorizationConsent = builder.build();
+		when(this.authorizationConsentService.findById(eq(registeredClient.getId()), eq(this.principal.getName())))
+				.thenReturn(previousAuthorizationConsent);
+
+		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
+				authorizationCodeRequestAuthentication(registeredClient, this.principal)
+						.build();
+
+		OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult =
+				(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication);
+
+		assertAuthorizationCodeRequestWithAuthorizationCodeResult(registeredClient, authentication, authenticationResult);
+	}
+
+	@Test
+	public void authenticateWhenAuthorizationCodeRequestValidThenReturnAuthorizationCode() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+
+		Map<String, Object> additionalParameters = new HashMap<>();
+		additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, "code-challenge");
+		additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
+		OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
+				authorizationCodeRequestAuthentication(registeredClient, this.principal)
+						.additionalParameters(additionalParameters)
+						.build();
+
+		OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult =
+				(OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider.authenticate(authentication);
+
+		assertAuthorizationCodeRequestWithAuthorizationCodeResult(registeredClient, authentication, authenticationResult);
+	}
+
+	private void assertAuthorizationCodeRequestWithAuthorizationCodeResult(
+			RegisteredClient registeredClient,
+			OAuth2AuthorizationCodeRequestAuthenticationToken authentication,
+			OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult) {
+
+		ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
+		verify(this.authorizationService).save(authorizationCaptor.capture());
+		OAuth2Authorization authorization = authorizationCaptor.getValue();
+
+		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(OAuth2AuthorizationRequest.class.getName());
+		assertThat(authorizationRequest.getGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
+		assertThat(authorizationRequest.getResponseType()).isEqualTo(OAuth2AuthorizationResponseType.CODE);
+		assertThat(authorizationRequest.getAuthorizationUri()).isEqualTo(authentication.getAuthorizationUri());
+		assertThat(authorizationRequest.getClientId()).isEqualTo(registeredClient.getClientId());
+		assertThat(authorizationRequest.getRedirectUri()).isEqualTo(authentication.getRedirectUri());
+		assertThat(authorizationRequest.getScopes()).isEqualTo(authentication.getScopes());
+		assertThat(authorizationRequest.getState()).isEqualTo(authentication.getState());
+		assertThat(authorizationRequest.getAdditionalParameters()).isEqualTo(authentication.getAdditionalParameters());
+
+		assertThat(authorization.getRegisteredClientId()).isEqualTo(registeredClient.getId());
+		assertThat(authorization.getPrincipalName()).isEqualTo(this.principal.getName());
+		assertThat(authorization.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
+		assertThat(authorization.<Authentication>getAttribute(Principal.class.getName())).isEqualTo(this.principal);
+
+		OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = authorization.getToken(OAuth2AuthorizationCode.class);
+		Set<String> authorizedScopes = authorization.getAttribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME);
+
+		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 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);
+
+		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.<Set<String>>getAttribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME))
+				.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 authenticateWhenConsentRequestWithPreviouslyApprovedThenAuthorizationConsentUpdated() {
+		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.<Set<String>>getAttribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME))
+				.isEqualTo(requestedScopes);
+
+		assertThat(authenticationResult.getScopes()).isEqualTo(requestedScopes);
+	}
+
+	@Test
+	public void authenticateWhenConsentRequestApproveNoneButPreviouslyApprovedThenAuthorizationConsentNotUpdated() {
+		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) {
+
+		OAuth2Error error = authenticationException.getError();
+		assertThat(error.getErrorCode()).isEqualTo(errorCode);
+		assertThat(error.getDescription()).contains(parameterName);
+
+		OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
+				authenticationException.getAuthorizationCodeRequestAuthentication();
+		assertThat(authorizationCodeRequestAuthentication.getRedirectUri()).isEqualTo(redirectUri);
+	}
+
+	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);
+	}
+
+}

+ 200 - 0
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationTokenTests.java

@@ -0,0 +1,200 @@
+/*
+ * Copyright 2020-2021 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.time.temporal.ChronoUnit;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+import org.junit.Test;
+
+import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
+import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
+import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Tests for {@link OAuth2AuthorizationCodeRequestAuthenticationToken}.
+ *
+ * @author Joe Grandja
+ */
+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))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("clientId cannot be empty");
+	}
+
+	@Test
+	public void withWhenPrincipalNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> OAuth2AuthorizationCodeRequestAuthenticationToken.with(REGISTERED_CLIENT.getClientId(), null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("principal cannot be null");
+	}
+
+	@Test
+	public void buildWhenAuthorizationUriNotProvidedThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() ->
+				OAuth2AuthorizationCodeRequestAuthenticationToken.with(REGISTERED_CLIENT.getClientId(), PRINCIPAL)
+						.build())
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("authorizationUri cannot be empty");
+	}
+
+	@Test
+	public void buildWhenStateNotProvidedThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() ->
+				OAuth2AuthorizationCodeRequestAuthenticationToken.with(REGISTERED_CLIENT.getClientId(), PRINCIPAL)
+						.authorizationUri(AUTHORIZATION_URI)
+						.consent(true)
+						.build())
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("state cannot be empty");
+	}
+
+	@Test
+	public void buildWhenAuthorizationCodeRequestThenValuesAreSet() {
+		String clientId = REGISTERED_CLIENT.getClientId();
+		String redirectUri = REGISTERED_CLIENT.getRedirectUris().iterator().next();
+		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();
+
+		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()).isEqualTo(redirectUri);
+		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() {
+		String clientId = REGISTERED_CLIENT.getClientId();
+		String redirectUri = REGISTERED_CLIENT.getRedirectUris().iterator().next();
+		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();
+
+		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()).isEqualTo(redirectUri);
+		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();
+	}
+
+}

文件差异内容过多而无法显示
+ 152 - 952
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilterTests.java


部分文件因为文件数量过多而无法显示