Selaa lähdekoodia

Move PKCE to OAuth2ClientAuthenticationProvider

PR gh-93
Joe Grandja 4 vuotta sitten
vanhempi
commit
5c31fb1b7e
21 muutettua tiedostoa jossa 774 lisäystä ja 533 poistoa
  1. 2 1
      oauth2-authorization-server/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationServerConfigurer.java
  2. 4 59
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java
  3. 1 37
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationToken.java
  4. 99 7
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProvider.java
  5. 29 3
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationToken.java
  6. 13 1
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/ClientSecretBasicAuthenticationConverter.java
  7. 64 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/DelegatingAuthenticationConverter.java
  8. 5 1
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilter.java
  9. 10 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2EndpointUtils.java
  10. 3 31
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilter.java
  11. 72 0
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/PublicClientAuthenticationConverter.java
  12. 2 3
      oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationCodeGrantTests.java
  13. 2 311
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java
  14. 2 30
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationTokenTests.java
  15. 322 11
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProviderTests.java
  16. 18 3
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationTokenTests.java
  17. 1 1
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProviderTests.java
  18. 26 0
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/ClientSecretBasicAuthenticationConverterTests.java
  19. 2 2
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilterTests.java
  20. 0 32
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilterTests.java
  21. 97 0
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/PublicClientAuthenticationConverterTests.java

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

@@ -120,7 +120,8 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
 	public void init(B builder) {
 		OAuth2ClientAuthenticationProvider clientAuthenticationProvider =
 				new OAuth2ClientAuthenticationProvider(
-						getRegisteredClientRepository(builder));
+						getRegisteredClientRepository(builder),
+						getAuthorizationService(builder));
 		builder.authenticationProvider(postProcess(clientAuthenticationProvider));
 
 		NimbusJwsEncoder jwtEncoder = new NimbusJwsEncoder(getKeyManager(builder));

+ 4 - 59
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java

@@ -24,7 +24,6 @@ import org.springframework.security.oauth2.core.OAuth2Error;
 import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
 import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
 import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
-import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
 import org.springframework.security.oauth2.jose.JoseHeader;
 import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
 import org.springframework.security.oauth2.jwt.Jwt;
@@ -42,12 +41,8 @@ import org.springframework.util.StringUtils;
 import java.net.MalformedURLException;
 import java.net.URI;
 import java.net.URL;
-import java.nio.charset.StandardCharsets;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
 import java.time.Instant;
 import java.time.temporal.ChronoUnit;
-import java.util.Base64;
 import java.util.Collections;
 
 /**
@@ -92,22 +87,13 @@ public class OAuth2AuthorizationCodeAuthenticationProvider implements Authentica
 				(OAuth2AuthorizationCodeAuthenticationToken) authentication;
 
 		OAuth2ClientAuthenticationToken clientPrincipal = null;
-		RegisteredClient registeredClient;
 		if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authorizationCodeAuthentication.getPrincipal().getClass())) {
 			clientPrincipal = (OAuth2ClientAuthenticationToken) authorizationCodeAuthentication.getPrincipal();
-			if (!clientPrincipal.isAuthenticated()) {
-				throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
-			}
-			registeredClient = clientPrincipal.getRegisteredClient();
-		} else if (StringUtils.hasText(authorizationCodeAuthentication.getClientId())) {
-			String clientId = authorizationCodeAuthentication.getClientId();
-			registeredClient = this.registeredClientRepository.findByClientId(clientId);
-			if (registeredClient == null) {
-				throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
-			}
-		} else {
+		}
+		if (clientPrincipal == null || !clientPrincipal.isAuthenticated()) {
 			throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
 		}
+		RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
 
 		OAuth2Authorization authorization = this.authorizationService.findByToken(
 				authorizationCodeAuthentication.getCode(), TokenType.AUTHORIZATION_CODE);
@@ -127,26 +113,6 @@ public class OAuth2AuthorizationCodeAuthenticationProvider implements Authentica
 			throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT));
 		}
 
-		// Validate PKCE parameters
-		String codeChallenge = (String) authorizationRequest
-				.getAdditionalParameters()
-				.get(PkceParameterNames.CODE_CHALLENGE);
-		if (StringUtils.hasText(codeChallenge)) {
-			String codeChallengeMethod = (String) authorizationRequest
-					.getAdditionalParameters()
-					.get(PkceParameterNames.CODE_CHALLENGE_METHOD);
-
-			String codeVerifier = (String) authorizationCodeAuthentication
-					.getAdditionalParameters()
-					.get(PkceParameterNames.CODE_VERIFIER);
-
-			if (!pkceCodeVerifierValid(codeVerifier, codeChallenge, codeChallengeMethod)) {
-				throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT));
-			}
-		} else if (registeredClient.getClientSettings().requireProofKey()) {
-			throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT));
-		}
-
 		JoseHeader joseHeader = JoseHeader.withAlgorithm(SignatureAlgorithm.RS256).build();
 
 		// TODO Allow configuration for issuer claim
@@ -179,28 +145,7 @@ public class OAuth2AuthorizationCodeAuthenticationProvider implements Authentica
 				.build();
 		this.authorizationService.save(authorization);
 
-		return clientPrincipal != null ?
-				new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken) :
-				new OAuth2AccessTokenAuthenticationToken(registeredClient, new OAuth2ClientAuthenticationToken(registeredClient), accessToken);
-	}
-
-	private static boolean pkceCodeVerifierValid(String codeVerifier, String codeChallenge, String codeChallengeMethod) {
-		if (!StringUtils.hasText(codeVerifier)) {
-			return false;
-		} else if (!StringUtils.hasText(codeChallengeMethod) || "plain".equals(codeChallengeMethod)) {
-			return  codeVerifier.equals(codeChallenge);
-		} else if ("S256".equals(codeChallengeMethod)) {
-			try {
-				MessageDigest md = MessageDigest.getInstance("SHA-256");
-				byte[] digest = md.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII));
-				String encodedVerifier = Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
-				return encodedVerifier.equals(codeChallenge);
-			} catch (NoSuchAlgorithmException ex) {
-				// It is unlikely that SHA-256 is not available on the server. If it is not available,
-				// there will likely be bigger issues as well. We default to SERVER_ERROR.
-			}
-		}
-		throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR));
+		return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken);
 	}
 
 	@Override

+ 1 - 37
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationToken.java

@@ -39,7 +39,6 @@ public class OAuth2AuthorizationCodeAuthenticationToken extends AbstractAuthenti
 	private static final long serialVersionUID = SpringSecurityCoreVersion2.SERIAL_VERSION_UID;
 	private final String code;
 	private final Authentication clientPrincipal;
-	private final String clientId;
 	private final String redirectUri;
 	private final Map<String, Object> additionalParameters;
 
@@ -58,32 +57,6 @@ public class OAuth2AuthorizationCodeAuthenticationToken extends AbstractAuthenti
 		Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");
 		this.code = code;
 		this.clientPrincipal = clientPrincipal;
-		this.clientId = OAuth2ClientAuthenticationToken.class.isAssignableFrom(this.clientPrincipal.getClass()) ?
-				(String) this.clientPrincipal.getPrincipal() :
-				null;
-		this.redirectUri = redirectUri;
-		this.additionalParameters = Collections.unmodifiableMap(
-				additionalParameters != null ?
-						additionalParameters :
-						Collections.emptyMap());
-	}
-
-	/**
-	 * Constructs an {@code OAuth2AuthorizationCodeAuthenticationToken} using the provided parameters.
-	 *
-	 * @param code the authorization code
-	 * @param clientId the client identifier
-	 * @param redirectUri the redirect uri
-	 * @param additionalParameters the additional parameters
-	 */
-	public OAuth2AuthorizationCodeAuthenticationToken(String code, String clientId,
-			@Nullable String redirectUri, @Nullable Map<String, Object> additionalParameters) {
-		super(Collections.emptyList());
-		Assert.hasText(code, "code cannot be empty");
-		Assert.hasText(clientId, "clientId cannot be empty");
-		this.code = code;
-		this.clientPrincipal = null;
-		this.clientId = clientId;
 		this.redirectUri = redirectUri;
 		this.additionalParameters = Collections.unmodifiableMap(
 				additionalParameters != null ?
@@ -93,7 +66,7 @@ public class OAuth2AuthorizationCodeAuthenticationToken extends AbstractAuthenti
 
 	@Override
 	public Object getPrincipal() {
-		return this.clientPrincipal != null ? this.clientPrincipal : this.clientId;
+		return this.clientPrincipal;
 	}
 
 	@Override
@@ -110,15 +83,6 @@ public class OAuth2AuthorizationCodeAuthenticationToken extends AbstractAuthenti
 		return this.code;
 	}
 
-	/**
-	 * Returns the client identifier
-	 *
-	 * @return the client identifier
-	 */
-	public @Nullable String getClientId() {
-		return this.clientId;
-	}
-
 	/**
 	 * Returns the redirect uri.
 	 *

+ 99 - 7
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProvider.java

@@ -18,49 +18,80 @@ package org.springframework.security.oauth2.server.authorization.authentication;
 import org.springframework.security.authentication.AuthenticationProvider;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
 import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
 import org.springframework.security.oauth2.core.OAuth2Error;
 import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
+import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationAttributeNames;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.TokenType;
 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.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Base64;
+import java.util.Map;
 
 /**
- * An {@link AuthenticationProvider} implementation that validates {@link OAuth2ClientAuthenticationToken}'s.
+ * An {@link AuthenticationProvider} implementation used for authenticating an OAuth 2.0 Client.
  *
  * @author Joe Grandja
  * @author Patryk Kostrzewa
+ * @author Daniel Garnier-Moiroux
  * @since 0.0.1
  * @see AuthenticationProvider
  * @see OAuth2ClientAuthenticationToken
  * @see RegisteredClientRepository
+ * @see OAuth2AuthorizationService
  */
 public class OAuth2ClientAuthenticationProvider implements AuthenticationProvider {
 	private final RegisteredClientRepository registeredClientRepository;
+	private final OAuth2AuthorizationService authorizationService;
 
 	/**
 	 * Constructs an {@code OAuth2ClientAuthenticationProvider} using the provided parameters.
 	 *
 	 * @param registeredClientRepository the repository of registered clients
+	 * @param authorizationService the authorization service
 	 */
-	public OAuth2ClientAuthenticationProvider(RegisteredClientRepository registeredClientRepository) {
+	public OAuth2ClientAuthenticationProvider(RegisteredClientRepository registeredClientRepository,
+			OAuth2AuthorizationService authorizationService) {
 		Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null");
+		Assert.notNull(authorizationService, "authorizationService cannot be null");
 		this.registeredClientRepository = registeredClientRepository;
+		this.authorizationService = authorizationService;
 	}
 
 	@Override
 	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
-		String clientId = authentication.getPrincipal().toString();
+		OAuth2ClientAuthenticationToken clientAuthentication =
+				(OAuth2ClientAuthenticationToken) authentication;
+
+		String clientId = clientAuthentication.getPrincipal().toString();
 		RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
 		if (registeredClient == null) {
-			throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
+			throwInvalidClient();
 		}
 
-		String clientSecret = authentication.getCredentials().toString();
-		if (!registeredClient.getClientSecret().equals(clientSecret)) {
-			throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
+		if (clientAuthentication.getCredentials() != null) {
+			String clientSecret = clientAuthentication.getCredentials().toString();
+			// TODO Use PasswordEncoder.matches()
+			if (!registeredClient.getClientSecret().equals(clientSecret)) {
+				throwInvalidClient();
+			}
 		}
 
+		authenticatePkceIfAvailable(clientAuthentication, registeredClient);
+
 		return new OAuth2ClientAuthenticationToken(registeredClient);
 	}
 
@@ -68,4 +99,65 @@ public class OAuth2ClientAuthenticationProvider implements AuthenticationProvide
 	public boolean supports(Class<?> authentication) {
 		return OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication);
 	}
+
+	private void authenticatePkceIfAvailable(OAuth2ClientAuthenticationToken clientAuthentication,
+			RegisteredClient registeredClient) {
+
+		Map<String, Object> parameters = clientAuthentication.getAdditionalParameters();
+		if (CollectionUtils.isEmpty(parameters) || !authorizationCodeGrant(parameters)) {
+			return;
+		}
+
+		OAuth2Authorization authorization = this.authorizationService.findByToken(
+				(String) parameters.get(OAuth2ParameterNames.CODE),
+				TokenType.AUTHORIZATION_CODE);
+		if (authorization == null) {
+			throwInvalidClient();
+		}
+
+		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
+				OAuth2AuthorizationAttributeNames.AUTHORIZATION_REQUEST);
+
+		String codeChallenge = (String) authorizationRequest.getAdditionalParameters()
+				.get(PkceParameterNames.CODE_CHALLENGE);
+		if (StringUtils.hasText(codeChallenge)) {
+			String codeChallengeMethod = (String) authorizationRequest.getAdditionalParameters()
+					.get(PkceParameterNames.CODE_CHALLENGE_METHOD);
+			String codeVerifier = (String) parameters.get(PkceParameterNames.CODE_VERIFIER);
+			if (!codeVerifierValid(codeVerifier, codeChallenge, codeChallengeMethod)) {
+				throwInvalidClient();
+			}
+		} else if (registeredClient.getClientSettings().requireProofKey()) {
+			throwInvalidClient();
+		}
+	}
+
+	private static boolean authorizationCodeGrant(Map<String, Object> parameters) {
+		return AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(
+				parameters.get(OAuth2ParameterNames.GRANT_TYPE)) &&
+				parameters.get(OAuth2ParameterNames.CODE) != null;
+	}
+
+	private static boolean codeVerifierValid(String codeVerifier, String codeChallenge, String codeChallengeMethod) {
+		if (!StringUtils.hasText(codeVerifier)) {
+			return false;
+		} else if (!StringUtils.hasText(codeChallengeMethod) || "plain".equals(codeChallengeMethod)) {
+			return  codeVerifier.equals(codeChallenge);
+		} else if ("S256".equals(codeChallengeMethod)) {
+			try {
+				MessageDigest md = MessageDigest.getInstance("SHA-256");
+				byte[] digest = md.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII));
+				String encodedVerifier = Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
+				return encodedVerifier.equals(codeChallenge);
+			} catch (NoSuchAlgorithmException ex) {
+				// It is unlikely that SHA-256 is not available on the server. If it is not available,
+				// there will likely be bigger issues as well. We default to SERVER_ERROR.
+			}
+		}
+		throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR));
+	}
+
+	private static void throwInvalidClient() {
+		throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
+	}
 }

+ 29 - 3
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationToken.java

@@ -23,6 +23,7 @@ import org.springframework.security.oauth2.server.authorization.client.Registere
 import org.springframework.util.Assert;
 
 import java.util.Collections;
+import java.util.Map;
 
 /**
  * An {@link Authentication} implementation used for OAuth 2.0 Client Authentication.
@@ -38,6 +39,7 @@ public class OAuth2ClientAuthenticationToken extends AbstractAuthenticationToken
 	private static final long serialVersionUID = SpringSecurityCoreVersion2.SERIAL_VERSION_UID;
 	private String clientId;
 	private String clientSecret;
+	private Map<String, Object> additionalParameters;
 	private RegisteredClient registeredClient;
 
 	/**
@@ -45,13 +47,28 @@ public class OAuth2ClientAuthenticationToken extends AbstractAuthenticationToken
 	 *
 	 * @param clientId the client identifier
 	 * @param clientSecret the client secret
+	 * @param additionalParameters the additional parameters
 	 */
-	public OAuth2ClientAuthenticationToken(String clientId, String clientSecret) {
+	public OAuth2ClientAuthenticationToken(String clientId, String clientSecret,
+			@Nullable Map<String, Object> additionalParameters) {
+		this(clientId, additionalParameters);
+		Assert.hasText(clientSecret, "clientSecret cannot be empty");
+		this.clientSecret = clientSecret;
+	}
+
+	/**
+	 * Constructs an {@code OAuth2ClientAuthenticationToken} using the provided parameters.
+	 *
+	 * @param clientId the client identifier
+	 * @param additionalParameters the additional parameters
+	 */
+	public OAuth2ClientAuthenticationToken(String clientId,
+			@Nullable Map<String, Object> additionalParameters) {
 		super(Collections.emptyList());
 		Assert.hasText(clientId, "clientId cannot be empty");
-		Assert.hasText(clientSecret, "clientSecret cannot be empty");
 		this.clientId = clientId;
-		this.clientSecret = clientSecret;
+		this.additionalParameters = additionalParameters != null ?
+				Collections.unmodifiableMap(additionalParameters) : null;
 	}
 
 	/**
@@ -78,6 +95,15 @@ public class OAuth2ClientAuthenticationToken extends AbstractAuthenticationToken
 		return this.clientSecret;
 	}
 
+	/**
+	 * Returns the additional parameters
+	 *
+	 * @return the additional parameters
+	 */
+	public @Nullable Map<String, Object> getAdditionalParameters() {
+		return this.additionalParameters;
+	}
+
 	/**
 	 * Returns the {@link RegisteredClient registered client}.
 	 *

+ 13 - 1
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/ClientSecretBasicAuthenticationConverter.java

@@ -28,6 +28,9 @@ import javax.servlet.http.HttpServletRequest;
 import java.net.URLDecoder;
 import java.nio.charset.StandardCharsets;
 import java.util.Base64;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
 
 /**
  * Attempts to extract HTTP Basic credentials from {@link HttpServletRequest}
@@ -82,6 +85,15 @@ public class ClientSecretBasicAuthenticationConverter implements AuthenticationC
 			throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST), ex);
 		}
 
-		return new OAuth2ClientAuthenticationToken(clientID, clientSecret);
+		return new OAuth2ClientAuthenticationToken(clientID, clientSecret, extractAdditionalParameters(request));
+	}
+
+	private static Map<String, Object> extractAdditionalParameters(HttpServletRequest request) {
+		Map<String, Object> additionalParameters = Collections.emptyMap();
+		if (OAuth2EndpointUtils.matchesPkceTokenRequest(request)) {
+			// Confidential clients can also leverage PKCE
+			additionalParameters = new HashMap<>(OAuth2EndpointUtils.getParameters(request).toSingleValueMap());
+		}
+		return additionalParameters;
 	}
 }

+ 64 - 0
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/DelegatingAuthenticationConverter.java

@@ -0,0 +1,64 @@
+/*
+ * Copyright 2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.server.authorization.web;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.authentication.AuthenticationConverter;
+import org.springframework.util.Assert;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * An {@link AuthenticationConverter} that simply delegates to it's
+ * internal {@code List} of {@link AuthenticationConverter}(s).
+ * <p>
+ * Each {@link AuthenticationConverter} is given a chance to
+ * {@link AuthenticationConverter#convert(HttpServletRequest)}
+ * with the first {@code non-null} {@link Authentication} being returned.
+ *
+ * @author Joe Grandja
+ * @since 0.0.2
+ * @see AuthenticationConverter
+ */
+public final class DelegatingAuthenticationConverter implements AuthenticationConverter {
+	private final List<AuthenticationConverter> converters;
+
+	/**
+	 * Constructs a {@code DelegatingAuthenticationConverter} using the provided parameters.
+	 *
+	 * @param converters a {@code List} of {@link AuthenticationConverter}(s)
+	 */
+	public DelegatingAuthenticationConverter(List<AuthenticationConverter> converters) {
+		Assert.notEmpty(converters, "converters cannot be empty");
+		this.converters = Collections.unmodifiableList(new LinkedList<>(converters));
+	}
+
+	@Override
+	public Authentication convert(HttpServletRequest request) {
+		Assert.notNull(request, "request cannot be null");
+		// @formatter:off
+		return this.converters.stream()
+				.map(converter -> converter.convert(request))
+				.filter(Objects::nonNull)
+				.findFirst()
+				.orElse(null);
+		// @formatter:on
+	}
+}

+ 5 - 1
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilter.java

@@ -41,6 +41,7 @@ import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
+import java.util.Arrays;
 
 /**
  * A {@code Filter} that processes an authentication request for an OAuth 2.0 Client.
@@ -73,7 +74,10 @@ public class OAuth2ClientAuthenticationFilter extends OncePerRequestFilter {
 		Assert.notNull(requestMatcher, "requestMatcher cannot be null");
 		this.authenticationManager = authenticationManager;
 		this.requestMatcher = requestMatcher;
-		this.authenticationConverter = new ClientSecretBasicAuthenticationConverter();
+		this.authenticationConverter = new DelegatingAuthenticationConverter(
+				Arrays.asList(
+						new ClientSecretBasicAuthenticationConverter(),
+						new PublicClientAuthenticationConverter()));
 		this.authenticationSuccessHandler = this::onAuthenticationSuccess;
 		this.authenticationFailureHandler = this::onAuthenticationFailure;
 	}

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

@@ -15,6 +15,9 @@
  */
 package org.springframework.security.oauth2.server.authorization.web;
 
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
 import org.springframework.util.LinkedMultiValueMap;
 import org.springframework.util.MultiValueMap;
 
@@ -46,4 +49,11 @@ final class OAuth2EndpointUtils {
 		});
 		return parameters;
 	}
+
+	static boolean matchesPkceTokenRequest(HttpServletRequest request) {
+		return AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(
+				request.getParameter(OAuth2ParameterNames.GRANT_TYPE)) &&
+				request.getParameter(OAuth2ParameterNames.CODE) != null &&
+				request.getParameter(PkceParameterNames.CODE_VERIFIER) != null;
+	}
 }

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

@@ -30,13 +30,11 @@ import org.springframework.security.oauth2.core.OAuth2Error;
 import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
 import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
 import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
-import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
 import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
 import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationToken;
-import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationToken;
 import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcher;
@@ -189,12 +187,6 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
 		throw new OAuth2AuthenticationException(error);
 	}
 
-	private static boolean isClientAuthenticated(Authentication clientPrincipal) {
-		return clientPrincipal != null &&
-				OAuth2ClientAuthenticationToken.class.isAssignableFrom(clientPrincipal.getClass()) &&
-				clientPrincipal.isAuthenticated();
-	}
-
 	private static class AuthorizationCodeAuthenticationConverter implements Converter<HttpServletRequest, Authentication> {
 
 		@Override
@@ -205,6 +197,8 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
 				return null;
 			}
 
+			Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
+
 			MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
 
 			// code (REQUIRED)
@@ -222,25 +216,6 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
 				throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI);
 			}
 
-			// client_id (REQUIRED)
-			// Required only if the client did not authenticate
-			String clientId = null;
-			Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
-			if (!isClientAuthenticated(clientPrincipal)) {
-				clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID);
-				if (!StringUtils.hasText(clientId) ||
-						parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {
-					throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID);
-				}
-
-				// code_verifier (REQUIRED for public clients)
-				String codeVerifier = parameters.getFirst(PkceParameterNames.CODE_VERIFIER);
-				if (!StringUtils.hasText(codeVerifier) ||
-						parameters.get(PkceParameterNames.CODE_VERIFIER).size() != 1) {
-					throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_VERIFIER);
-				}
-			}
-
 			Map<String, Object> additionalParameters = parameters
 					.entrySet()
 					.stream()
@@ -250,10 +225,7 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
 							!e.getKey().equals(OAuth2ParameterNames.REDIRECT_URI))
 					.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));
 
-
-			return clientId != null ?
-					new OAuth2AuthorizationCodeAuthenticationToken(code, clientId, redirectUri, additionalParameters) :
-					new OAuth2AuthorizationCodeAuthenticationToken(code, clientPrincipal, redirectUri, additionalParameters);
+			return new OAuth2AuthorizationCodeAuthenticationToken(code, clientPrincipal, redirectUri, additionalParameters);
 		}
 	}
 

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

@@ -0,0 +1,72 @@
+/*
+ * Copyright 2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.server.authorization.web;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
+import org.springframework.security.web.authentication.AuthenticationConverter;
+import org.springframework.util.MultiValueMap;
+import org.springframework.util.StringUtils;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.HashMap;
+
+/**
+ * Attempts to extract the parameters from {@link HttpServletRequest}
+ * used for authenticating public clients using Proof Key for Code Exchange (PKCE).
+ *
+ * @author Joe Grandja
+ * @since 0.0.2
+ * @see AuthenticationConverter
+ * @see OAuth2ClientAuthenticationToken
+ * @see OAuth2ClientAuthenticationFilter
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636">Proof Key for Code Exchange by OAuth Public Clients</a>
+ */
+public class PublicClientAuthenticationConverter implements AuthenticationConverter {
+
+	@Override
+	public Authentication convert(HttpServletRequest request) {
+		if (!OAuth2EndpointUtils.matchesPkceTokenRequest(request)) {
+			return null;
+		}
+
+		MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
+
+		// client_id (REQUIRED for public clients)
+		String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID);
+		if (!StringUtils.hasText(clientId)) {
+			return null;
+		}
+		if (parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {
+			throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST));
+		}
+
+		// code_verifier (REQUIRED)
+		if (parameters.get(PkceParameterNames.CODE_VERIFIER).size() != 1) {
+			throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST));
+		}
+
+		parameters.remove(OAuth2ParameterNames.CLIENT_ID);
+
+		return new OAuth2ClientAuthenticationToken(
+				clientId, new HashMap<>(parameters.toSingleValueMap()));
+	}
+}

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

@@ -207,13 +207,12 @@ public class OAuth2AuthorizationCodeGrantTests {
 		this.mvc.perform(post(OAuth2TokenEndpointFilter.DEFAULT_TOKEN_ENDPOINT_URI)
 				.params(getTokenRequestParameters(registeredClient, authorization))
 				.param(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())
-				.param(PkceParameterNames.CODE_VERIFIER, S256_CODE_VERIFIER)
-				.with(user("user")))	// TODO Remove after PKCE authentication is moved to OAuth2ClientAuthenticationProvider
+				.param(PkceParameterNames.CODE_VERIFIER, S256_CODE_VERIFIER))
 				.andExpect(status().isOk())
 				.andExpect(jsonPath("$.access_token").isNotEmpty());
 
 		verify(registeredClientRepository, times(2)).findByClientId(eq(registeredClient.getClientId()));
-		verify(authorizationService).findByToken(
+		verify(authorizationService, times(2)).findByToken(
 				eq(authorization.getAttribute(OAuth2AuthorizationAttributeNames.CODE)),
 				eq(TokenType.AUTHORIZATION_CODE));
 		verify(authorizationService, times(2)).save(any());

+ 2 - 311
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java

@@ -22,7 +22,6 @@ import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
 import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
 import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
-import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
 import org.springframework.security.oauth2.jose.JoseHeaderNames;
 import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
 import org.springframework.security.oauth2.jwt.Jwt;
@@ -36,13 +35,9 @@ import org.springframework.security.oauth2.server.authorization.client.InMemoryR
 import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
 import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
 import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
-import org.springframework.security.oauth2.server.authorization.config.ClientSettings;
 
 import java.time.Instant;
 import java.time.temporal.ChronoUnit;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -59,19 +54,8 @@ import static org.mockito.Mockito.when;
  * @author Daniel Garnier-Moiroux
  */
 public class OAuth2AuthorizationCodeAuthenticationProviderTests {
-	private static final String PLAIN_CODE_VERIFIER = "pkce-key";
-	private static final String PLAIN_CODE_CHALLENGE = PLAIN_CODE_VERIFIER;
-
-	// See RFC 7636: Appendix B.  Example for the S256 code_challenge_method
-	// https://tools.ietf.org/html/rfc7636#appendix-B
-	private static final String S256_CODE_VERIFIER = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
-	private static final String S256_CODE_CHALLENGE = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM";
-
 	private static final String AUTHORIZATION_CODE = "code";
-
 	private RegisteredClient registeredClient;
-	private RegisteredClient otherRegisteredClient;
-	private RegisteredClient registeredClientRequiresProofKey;
 	private RegisteredClientRepository registeredClientRepository;
 	private OAuth2AuthorizationService authorizationService;
 	private JwtEncoder jwtEncoder;
@@ -80,17 +64,7 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
 	@Before
 	public void setUp() {
 		this.registeredClient = TestRegisteredClients.registeredClient().build();
-		this.otherRegisteredClient = TestRegisteredClients.registeredClient2().build();
-		this.registeredClientRequiresProofKey = TestRegisteredClients.registeredClient()
-				.id("registration-3")
-				.clientId("client-3")
-				.clientSettings(new ClientSettings().requireProofKey(true))
-				.build();
-		this.registeredClientRepository = new InMemoryRegisteredClientRepository(
-				this.registeredClient,
-				this.otherRegisteredClient,
-				this.registeredClientRequiresProofKey
-		);
+		this.registeredClientRepository = new InMemoryRegisteredClientRepository(this.registeredClient);
 		this.authorizationService = mock(OAuth2AuthorizationService.class);
 		this.jwtEncoder = mock(JwtEncoder.class);
 		this.authenticationProvider = new OAuth2AuthorizationCodeAuthenticationProvider(
@@ -139,7 +113,7 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
 	@Test
 	public void authenticateWhenClientPrincipalNotAuthenticatedThenThrowOAuth2AuthenticationException() {
 		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(
-				this.registeredClient.getClientId(), this.registeredClient.getClientSecret());
+				this.registeredClient.getClientId(), this.registeredClient.getClientSecret(), null);
 		OAuth2AuthorizationCodeAuthenticationToken authentication =
 				new OAuth2AuthorizationCodeAuthenticationToken(AUTHORIZATION_CODE, clientPrincipal, null, null);
 		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
@@ -149,31 +123,6 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
 				.isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
 	}
 
-	@Test
-	public void authenticateWhenPublicClientAndInvalidClientIdThenThrowOAuth2AuthenticationException() {
-		OAuth2Authorization authorization = TestOAuth2Authorizations
-				.authorization(this.registeredClient, createPkceParametersPlain())
-				.build();
-		when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
-				.thenReturn(authorization);
-
-		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
-				OAuth2AuthorizationAttributeNames.AUTHORIZATION_REQUEST);
-		OAuth2AuthorizationCodeAuthenticationToken authentication =
-				new OAuth2AuthorizationCodeAuthenticationToken(
-						AUTHORIZATION_CODE,
-						"invalid-client-id",
-						authorizationRequest.getRedirectUri(),
-						Collections.singletonMap(PkceParameterNames.CODE_VERIFIER, PLAIN_CODE_CHALLENGE)
-				);
-
-		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
-				.isInstanceOf(OAuth2AuthenticationException.class)
-				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
-				.extracting("errorCode")
-				.isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
-	}
-
 	@Test
 	public void authenticateWhenInvalidCodeThenThrowOAuth2AuthenticationException() {
 		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(this.registeredClient);
@@ -203,30 +152,6 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
 				.isEqualTo(OAuth2ErrorCodes.INVALID_GRANT);
 	}
 
-	@Test
-	public void authenticateWhenPublicClientAndClientIdNotMatchThenThrowOAuth2AuthenticationException() {
-		OAuth2Authorization authorization = TestOAuth2Authorizations
-				.authorization(this.registeredClient, createPkceParametersPlain())
-				.build();
-		when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
-				.thenReturn(authorization);
-
-		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
-				OAuth2AuthorizationAttributeNames.AUTHORIZATION_REQUEST);
-		OAuth2AuthorizationCodeAuthenticationToken authentication =
-				new OAuth2AuthorizationCodeAuthenticationToken(
-						AUTHORIZATION_CODE,
-						this.otherRegisteredClient.getClientId(),
-						authorizationRequest.getRedirectUri(),
-						Collections.singletonMap(PkceParameterNames.CODE_VERIFIER, PLAIN_CODE_VERIFIER)
-				);
-		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
-				.isInstanceOf(OAuth2AuthenticationException.class)
-				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
-				.extracting("errorCode")
-				.isEqualTo(OAuth2ErrorCodes.INVALID_GRANT);
-	}
-
 	@Test
 	public void authenticateWhenInvalidRedirectUriThenThrowOAuth2AuthenticationException() {
 		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization().build();
@@ -272,240 +197,6 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
 		assertThat(accessTokenAuthentication.getAccessToken()).isEqualTo(updatedAuthorization.getAccessToken());
 	}
 
-	@Test
-	public void authenticateWhenRequireProofKeyAndMissingCodeChallengeThenThrowOAuth2AuthenticationException() {
-		OAuth2Authorization authorization = TestOAuth2Authorizations
-				.authorization(this.registeredClientRequiresProofKey)
-				.build();
-		when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
-				.thenReturn(authorization);
-
-		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
-				OAuth2AuthorizationAttributeNames.AUTHORIZATION_REQUEST);
-		OAuth2AuthorizationCodeAuthenticationToken authentication =
-				new OAuth2AuthorizationCodeAuthenticationToken(
-						AUTHORIZATION_CODE,
-						this.registeredClientRequiresProofKey.getClientId(),
-						authorizationRequest.getRedirectUri(),
-						Collections.singletonMap(PkceParameterNames.CODE_VERIFIER, PLAIN_CODE_VERIFIER)
-				);
-		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
-				.isInstanceOf(OAuth2AuthenticationException.class)
-				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
-				.extracting("errorCode")
-				.isEqualTo(OAuth2ErrorCodes.INVALID_GRANT);
-	}
-
-	@Test
-	public void authenticateWhenPublicClientAndMissingCodeVerifierThenThrowOAuth2AuthenticationException() {
-		OAuth2Authorization authorization = TestOAuth2Authorizations
-				.authorization(this.registeredClient, createPkceParametersPlain())
-				.build();
-		when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
-				.thenReturn(authorization);
-
-		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
-				OAuth2AuthorizationAttributeNames.AUTHORIZATION_REQUEST);
-		OAuth2AuthorizationCodeAuthenticationToken authentication =
-				new OAuth2AuthorizationCodeAuthenticationToken(
-						AUTHORIZATION_CODE,
-						authorizationRequest.getClientId(),
-						authorizationRequest.getRedirectUri(),
-						null
-				);
-		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
-				.isInstanceOf(OAuth2AuthenticationException.class)
-				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
-				.extracting("errorCode")
-				.isEqualTo(OAuth2ErrorCodes.INVALID_GRANT);
-	}
-
-	@Test
-	public void authenticateWhenConfidentialClientRequireProofKeyAndMissingCodeVerifierThenThrowOAuth2AuthenticationException() {
-		OAuth2Authorization authorization = TestOAuth2Authorizations
-				.authorization(this.registeredClientRequiresProofKey, createPkceParametersPlain())
-				.build();
-		when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
-				.thenReturn(authorization);
-
-		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(
-				this.registeredClientRequiresProofKey);
-		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
-				OAuth2AuthorizationAttributeNames.AUTHORIZATION_REQUEST);
-		OAuth2AuthorizationCodeAuthenticationToken authentication =
-				new OAuth2AuthorizationCodeAuthenticationToken(
-						AUTHORIZATION_CODE,
-						clientPrincipal,
-						authorizationRequest.getRedirectUri(),
-						null
-				);
-		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
-				.isInstanceOf(OAuth2AuthenticationException.class)
-				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
-				.extracting("errorCode")
-				.isEqualTo(OAuth2ErrorCodes.INVALID_GRANT);
-	}
-
-	@Test
-	public void authenticateWhenPublicClientAndPlainMethodAndInvalidCodeVerifierThenThrowOAuth2AuthenticationException() {
-		OAuth2Authorization authorization = TestOAuth2Authorizations
-				.authorization(this.registeredClient, createPkceParametersPlain())
-				.build();
-		when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
-				.thenReturn(authorization);
-
-		OAuth2AuthorizationCodeAuthenticationToken authentication = createAuthorizationCodeAuthentication(
-				this.registeredClient, "invalid-code-verifier");
-		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
-				.isInstanceOf(OAuth2AuthenticationException.class)
-				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
-				.extracting("errorCode")
-				.isEqualTo(OAuth2ErrorCodes.INVALID_GRANT);
-	}
-
-	@Test
-	public void authenticateWhenPublicClientAndS256MethodAndInvalidCodeVerifierThenThrowOAuth2AuthenticationException() {
-		OAuth2Authorization authorization = TestOAuth2Authorizations
-				.authorization(this.registeredClient, createPkceParametersS256())
-				.build();
-		when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
-				.thenReturn(authorization);
-
-		OAuth2AuthorizationCodeAuthenticationToken authentication = createAuthorizationCodeAuthentication(
-				this.registeredClient, "invalid-code-verifier");
-		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
-				.isInstanceOf(OAuth2AuthenticationException.class)
-				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
-				.extracting("errorCode")
-				.isEqualTo(OAuth2ErrorCodes.INVALID_GRANT);
-	}
-
-	@Test
-	public void authenticateWhenRequireProofKeyAndUnsupportedCodeChallengeMethodThenThrowOAuth2AuthenticationException() {
-		Map<String, Object> pkceParameters = createPkceParametersPlain();
-		// This should never happen: the Authorization endpoint should not allow it
-		pkceParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "unsupported-challenge-method");
-		OAuth2Authorization authorization = TestOAuth2Authorizations
-				.authorization(this.registeredClientRequiresProofKey, pkceParameters)
-				.build();
-		when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
-				.thenReturn(authorization);
-
-		OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute(
-				OAuth2AuthorizationAttributeNames.AUTHORIZATION_REQUEST);
-		OAuth2AuthorizationCodeAuthenticationToken authentication =
-				new OAuth2AuthorizationCodeAuthenticationToken(
-						AUTHORIZATION_CODE,
-						this.registeredClientRequiresProofKey.getClientId(),
-						authorizationRequest.getRedirectUri(),
-						Collections.singletonMap(PkceParameterNames.CODE_VERIFIER, PLAIN_CODE_VERIFIER)
-				);
-		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
-				.isInstanceOf(OAuth2AuthenticationException.class)
-				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
-				.extracting("errorCode")
-				.isEqualTo(OAuth2ErrorCodes.SERVER_ERROR);
-	}
-
-	@Test
-	public void authenticateWhenPublicClientAndPlainMethodAndValidCodeVerifierThenReturnAccessToken() {
-		OAuth2Authorization authorization = TestOAuth2Authorizations
-				.authorization(this.registeredClient, createPkceParametersPlain())
-				.build();
-		when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
-				.thenReturn(authorization);
-		when(this.jwtEncoder.encode(any(), any())).thenReturn(createJwt());
-
-		OAuth2AuthorizationCodeAuthenticationToken authentication = createAuthorizationCodeAuthentication(
-				this.registeredClient, PLAIN_CODE_VERIFIER);
-
-		OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
-				(OAuth2AccessTokenAuthenticationToken) this.authenticationProvider.authenticate(authentication);
-
-		ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
-		verify(this.authorizationService).save(authorizationCaptor.capture());
-		OAuth2Authorization updatedAuthorization = authorizationCaptor.getValue();
-
-		OAuth2ClientAuthenticationToken clientAuthentication = (OAuth2ClientAuthenticationToken) accessTokenAuthentication.getPrincipal();
-		assertThat(clientAuthentication.getPrincipal()).isEqualTo(this.registeredClient.getClientId());
-		assertThat(updatedAuthorization.getAccessToken()).isNotNull();
-		assertThat(accessTokenAuthentication.getAccessToken()).isEqualTo(updatedAuthorization.getAccessToken());
-	}
-
-	@Test
-	public void authenticateWhenPublicClientAndMissingMethodThenDefaultPlainMethodAndReturnAccessToken() {
-		OAuth2Authorization authorization = TestOAuth2Authorizations
-				.authorization(this.registeredClient, Collections.singletonMap(PkceParameterNames.CODE_CHALLENGE, PLAIN_CODE_CHALLENGE))
-				.build();
-		when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
-				.thenReturn(authorization);
-		when(this.jwtEncoder.encode(any(), any())).thenReturn(createJwt());
-
-		OAuth2AuthorizationCodeAuthenticationToken authentication = createAuthorizationCodeAuthentication(
-				this.registeredClient, PLAIN_CODE_VERIFIER);
-
-		OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
-				(OAuth2AccessTokenAuthenticationToken) this.authenticationProvider.authenticate(authentication);
-
-		ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
-		verify(this.authorizationService).save(authorizationCaptor.capture());
-		OAuth2Authorization updatedAuthorization = authorizationCaptor.getValue();
-
-		OAuth2ClientAuthenticationToken clientAuthentication = (OAuth2ClientAuthenticationToken) accessTokenAuthentication.getPrincipal();
-		assertThat(clientAuthentication.getPrincipal()).isEqualTo(this.registeredClient.getClientId());
-		assertThat(updatedAuthorization.getAccessToken()).isNotNull();
-		assertThat(accessTokenAuthentication.getAccessToken()).isEqualTo(updatedAuthorization.getAccessToken());
-	}
-
-	@Test
-	public void authenticateWhenPublicClientAndS256MethodAndValidCodeVerifierThenReturnAccessToken() {
-		OAuth2Authorization authorization = TestOAuth2Authorizations
-				.authorization(this.registeredClient, createPkceParametersS256())
-				.build();
-		when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
-				.thenReturn(authorization);
-		when(this.jwtEncoder.encode(any(), any())).thenReturn(createJwt());
-
-		OAuth2AuthorizationCodeAuthenticationToken authentication = createAuthorizationCodeAuthentication(
-				this.registeredClient, S256_CODE_VERIFIER);
-
-		OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
-				(OAuth2AccessTokenAuthenticationToken) this.authenticationProvider.authenticate(authentication);
-
-		ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
-		verify(this.authorizationService).save(authorizationCaptor.capture());
-		OAuth2Authorization updatedAuthorization = authorizationCaptor.getValue();
-
-		OAuth2ClientAuthenticationToken clientAuthentication = (OAuth2ClientAuthenticationToken) accessTokenAuthentication.getPrincipal();
-		assertThat(clientAuthentication.getPrincipal()).isEqualTo(this.registeredClient.getClientId());
-		assertThat(updatedAuthorization.getAccessToken()).isNotNull();
-		assertThat(accessTokenAuthentication.getAccessToken()).isEqualTo(updatedAuthorization.getAccessToken());
-	}
-
-	private static Map<String, Object> createPkceParametersPlain() {
-		Map<String, Object> parameters = new HashMap<>();
-		parameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "plain");
-		parameters.put(PkceParameterNames.CODE_CHALLENGE, PLAIN_CODE_CHALLENGE);
-		return parameters;
-	}
-
-	private static Map<String, Object> createPkceParametersS256() {
-		Map<String, Object> parameters = new HashMap<>();
-		parameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
-		parameters.put(PkceParameterNames.CODE_CHALLENGE, S256_CODE_CHALLENGE);
-		return parameters;
-	}
-
-	private static OAuth2AuthorizationCodeAuthenticationToken createAuthorizationCodeAuthentication(
-			RegisteredClient registeredClient, String codeVerifier) {
-		return new OAuth2AuthorizationCodeAuthenticationToken(
-				AUTHORIZATION_CODE,
-				registeredClient.getClientId(),
-				registeredClient.getRedirectUris().iterator().next(),
-				Collections.singletonMap(PkceParameterNames.CODE_VERIFIER, codeVerifier)
-		);
-	}
-
 	private static Jwt createJwt() {
 		Instant issuedAt = Instant.now();
 		Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS);

+ 2 - 30
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationTokenTests.java

@@ -16,7 +16,6 @@
 package org.springframework.security.oauth2.server.authorization.authentication;
 
 import org.junit.Test;
-import org.springframework.security.core.Authentication;
 import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
 
 import java.util.Collections;
@@ -35,7 +34,6 @@ public class OAuth2AuthorizationCodeAuthenticationTokenTests {
 	private String code = "code";
 	private OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(
 			TestRegisteredClients.registeredClient().build());
-	private String clientId = "clientId";
 	private String redirectUri = "redirectUri";
 	private Map<String, Object> additionalParameters = Collections.singletonMap("param1", "value1");
 
@@ -48,18 +46,11 @@ public class OAuth2AuthorizationCodeAuthenticationTokenTests {
 
 	@Test
 	public void constructorWhenClientPrincipalNullThenThrowIllegalArgumentException() {
-		assertThatThrownBy(() -> new OAuth2AuthorizationCodeAuthenticationToken(this.code, (Authentication) null, this.redirectUri, null))
+		assertThatThrownBy(() -> new OAuth2AuthorizationCodeAuthenticationToken(this.code, null, this.redirectUri, null))
 				.isInstanceOf(IllegalArgumentException.class)
 				.hasMessage("clientPrincipal cannot be null");
 	}
 
-	@Test
-	public void constructorWhenClientIdNullThenThrowIllegalArgumentException() {
-		assertThatThrownBy(() -> new OAuth2AuthorizationCodeAuthenticationToken(this.code, (String) null, this.redirectUri, null))
-				.isInstanceOf(IllegalArgumentException.class)
-				.hasMessage("clientId cannot be empty");
-	}
-
 	@Test
 	public void constructorWhenClientPrincipalProvidedThenCreated() {
 		OAuth2AuthorizationCodeAuthenticationToken authentication = new OAuth2AuthorizationCodeAuthenticationToken(
@@ -71,21 +62,10 @@ public class OAuth2AuthorizationCodeAuthenticationTokenTests {
 		assertThat(authentication.getAdditionalParameters()).isEqualTo(this.additionalParameters);
 	}
 
-	@Test
-	public void constructorWhenClientIdProvidedThenCreated() {
-		OAuth2AuthorizationCodeAuthenticationToken authentication = new OAuth2AuthorizationCodeAuthenticationToken(
-				this.code, this.clientId, this.redirectUri, this.additionalParameters);
-		assertThat(authentication.getPrincipal()).isEqualTo(this.clientId);
-		assertThat(authentication.getCredentials().toString()).isEmpty();
-		assertThat(authentication.getCode()).isEqualTo(this.code);
-		assertThat(authentication.getRedirectUri()).isEqualTo(this.redirectUri);
-		assertThat(authentication.getAdditionalParameters()).isEqualTo(this.additionalParameters);
-	}
-
 	@Test
 	public void getAdditionalParametersWhenUpdateThenThrowUnsupportedOperationException() {
 		OAuth2AuthorizationCodeAuthenticationToken authentication = new OAuth2AuthorizationCodeAuthenticationToken(
-				this.code, this.clientId, this.redirectUri, this.additionalParameters);
+				this.code, this.clientPrincipal, this.redirectUri, this.additionalParameters);
 		assertThatThrownBy(() -> authentication.getAdditionalParameters().put("another_key", 1))
 				.isInstanceOf(UnsupportedOperationException.class);
 		assertThatThrownBy(() -> authentication.getAdditionalParameters().remove("some_key"))
@@ -93,12 +73,4 @@ public class OAuth2AuthorizationCodeAuthenticationTokenTests {
 		assertThatThrownBy(() -> authentication.getAdditionalParameters().clear())
 				.isInstanceOf(UnsupportedOperationException.class);
 	}
-
-	@Test
-	public void getClientIdWhenClientPrincipalProvidedThenNotNull() {
-		OAuth2AuthorizationCodeAuthenticationToken authentication = new OAuth2AuthorizationCodeAuthenticationToken(
-				this.code, this.clientPrincipal, this.redirectUri, this.additionalParameters);
-		assertThat(authentication.getClientId()).isNotNull();
-		assertThat(authentication.getClientId()).isEqualTo(this.clientPrincipal.getRegisteredClient().getClientId());
-	}
 }

+ 322 - 11
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProviderTests.java

@@ -17,41 +17,74 @@ package org.springframework.security.oauth2.server.authorization.authentication;
 
 import org.junit.Before;
 import org.junit.Test;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
 import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
 import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
-import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
+import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
+import org.springframework.security.oauth2.server.authorization.TokenType;
 import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
 import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
 import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
+import org.springframework.security.oauth2.server.authorization.config.ClientSettings;
+
+import java.util.HashMap;
+import java.util.Map;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
 
 /**
  * Tests for {@link OAuth2ClientAuthenticationProvider}.
  *
  * @author Patryk Kostrzewa
  * @author Joe Grandja
+ * @author Daniel Garnier-Moiroux
  */
 public class OAuth2ClientAuthenticationProviderTests {
-	private RegisteredClient registeredClient;
+	private static final String PLAIN_CODE_VERIFIER = "pkce-key";
+	private static final String PLAIN_CODE_CHALLENGE = PLAIN_CODE_VERIFIER;
+
+	// See RFC 7636: Appendix B.  Example for the S256 code_challenge_method
+	// https://tools.ietf.org/html/rfc7636#appendix-B
+	private static final String S256_CODE_VERIFIER = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
+	private static final String S256_CODE_CHALLENGE = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM";
+
+	private static final String AUTHORIZATION_CODE = "code";
+
 	private RegisteredClientRepository registeredClientRepository;
+	private OAuth2AuthorizationService authorizationService;
 	private OAuth2ClientAuthenticationProvider authenticationProvider;
 
 	@Before
 	public void setUp() {
-		this.registeredClient = TestRegisteredClients.registeredClient().build();
-		this.registeredClientRepository = new InMemoryRegisteredClientRepository(this.registeredClient);
-		this.authenticationProvider = new OAuth2ClientAuthenticationProvider(this.registeredClientRepository);
+		this.registeredClientRepository = mock(RegisteredClientRepository.class);
+		this.authorizationService = mock(OAuth2AuthorizationService.class);
+		this.authenticationProvider = new OAuth2ClientAuthenticationProvider(
+				this.registeredClientRepository, this.authorizationService);
 	}
 
 	@Test
 	public void constructorWhenRegisteredClientRepositoryNullThenThrowIllegalArgumentException() {
-		assertThatThrownBy(() -> new OAuth2ClientAuthenticationProvider(null))
+		assertThatThrownBy(() -> new OAuth2ClientAuthenticationProvider(null, this.authorizationService))
 				.isInstanceOf(IllegalArgumentException.class)
 				.hasMessage("registeredClientRepository cannot be null");
 	}
 
+	@Test
+	public void constructorWhenAuthorizationServiceNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> new OAuth2ClientAuthenticationProvider(this.registeredClientRepository, null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("authorizationService cannot be null");
+	}
+
 	@Test
 	public void supportsWhenTypeOAuth2ClientAuthenticationTokenThenReturnTrue() {
 		assertThat(this.authenticationProvider.supports(OAuth2ClientAuthenticationToken.class)).isTrue();
@@ -59,8 +92,12 @@ public class OAuth2ClientAuthenticationProviderTests {
 
 	@Test
 	public void authenticateWhenInvalidClientIdThenThrowOAuth2AuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+
 		OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
-				this.registeredClient.getClientId() + "-invalid", this.registeredClient.getClientSecret());
+				registeredClient.getClientId() + "-invalid", registeredClient.getClientSecret(), null);
 		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
 				.isInstanceOf(OAuth2AuthenticationException.class)
 				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
@@ -70,8 +107,12 @@ public class OAuth2ClientAuthenticationProviderTests {
 
 	@Test
 	public void authenticateWhenInvalidClientSecretThenThrowOAuth2AuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+
 		OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
-				this.registeredClient.getClientId(), this.registeredClient.getClientSecret() + "-invalid");
+				registeredClient.getClientId(), registeredClient.getClientSecret() + "-invalid", null);
 		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
 				.isInstanceOf(OAuth2AuthenticationException.class)
 				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
@@ -81,13 +122,283 @@ public class OAuth2ClientAuthenticationProviderTests {
 
 	@Test
 	public void authenticateWhenValidCredentialsThenAuthenticated() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+
 		OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
-				this.registeredClient.getClientId(), this.registeredClient.getClientSecret());
+				registeredClient.getClientId(), registeredClient.getClientSecret(), null);
+		OAuth2ClientAuthenticationToken authenticationResult =
+				(OAuth2ClientAuthenticationToken) this.authenticationProvider.authenticate(authentication);
+		assertThat(authenticationResult.isAuthenticated()).isTrue();
+		assertThat(authenticationResult.getPrincipal().toString()).isEqualTo(registeredClient.getClientId());
+		assertThat(authenticationResult.getCredentials()).isNull();
+		assertThat(authenticationResult.getRegisteredClient()).isEqualTo(registeredClient);
+	}
+
+	@Test
+	public void authenticateWhenNotPkceThenContinueAuthenticated() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+
+		OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
+				registeredClient.getClientId(), registeredClient.getClientSecret(), null);
+		OAuth2ClientAuthenticationToken authenticationResult =
+				(OAuth2ClientAuthenticationToken) this.authenticationProvider.authenticate(authentication);
+		assertThat(authenticationResult.isAuthenticated()).isTrue();
+
+		verifyNoInteractions(this.authorizationService);
+	}
+
+	@Test
+	public void authenticateWhenPkceAndInvalidCodeThenThrowOAuth2AuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+
+		OAuth2Authorization authorization = TestOAuth2Authorizations
+				.authorization(registeredClient, createPkceAuthorizationParametersPlain())
+				.build();
+		when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
+				.thenReturn(authorization);
+
+		Map<String, Object> parameters = createPkceTokenParameters(PLAIN_CODE_VERIFIER);
+		parameters.put(OAuth2ParameterNames.CODE, "invalid-code");
+
+		OAuth2ClientAuthenticationToken authentication =
+				new OAuth2ClientAuthenticationToken(registeredClient.getClientId(), parameters);
+
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+				.isInstanceOf(OAuth2AuthenticationException.class)
+				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
+				.extracting("errorCode")
+				.isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
+	}
+
+	@Test
+	public void authenticateWhenPkceAndRequireProofKeyAndMissingCodeChallengeThenThrowOAuth2AuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.clientSettings(
+						new ClientSettings().requireProofKey(true))
+				.build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+
+		OAuth2Authorization authorization = TestOAuth2Authorizations
+				.authorization(registeredClient)
+				.build();
+		when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
+				.thenReturn(authorization);
+
+		Map<String, Object> parameters = createPkceTokenParameters(PLAIN_CODE_VERIFIER);
+
+		OAuth2ClientAuthenticationToken authentication =
+				new OAuth2ClientAuthenticationToken(registeredClient.getClientId(), parameters);
+
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+				.isInstanceOf(OAuth2AuthenticationException.class)
+				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
+				.extracting("errorCode")
+				.isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
+	}
+
+	@Test
+	public void authenticateWhenPkceAndMissingCodeVerifierThenThrowOAuth2AuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+
+		OAuth2Authorization authorization = TestOAuth2Authorizations
+				.authorization(registeredClient, createPkceAuthorizationParametersPlain())
+				.build();
+		when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
+				.thenReturn(authorization);
+
+		Map<String, Object> parameters = createPkceTokenParameters(PLAIN_CODE_VERIFIER);
+		parameters.remove(PkceParameterNames.CODE_VERIFIER);
+
+		OAuth2ClientAuthenticationToken authentication =
+				new OAuth2ClientAuthenticationToken(registeredClient.getClientId(), parameters);
+
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+				.isInstanceOf(OAuth2AuthenticationException.class)
+				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
+				.extracting("errorCode")
+				.isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
+	}
+
+	@Test
+	public void authenticateWhenPkceAndPlainMethodAndInvalidCodeVerifierThenThrowOAuth2AuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+
+		OAuth2Authorization authorization = TestOAuth2Authorizations
+				.authorization(registeredClient, createPkceAuthorizationParametersPlain())
+				.build();
+		when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
+				.thenReturn(authorization);
+
+		Map<String, Object> parameters = createPkceTokenParameters("invalid-code-verifier");
+
+		OAuth2ClientAuthenticationToken authentication =
+				new OAuth2ClientAuthenticationToken(registeredClient.getClientId(), parameters);
+
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+				.isInstanceOf(OAuth2AuthenticationException.class)
+				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
+				.extracting("errorCode")
+				.isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
+	}
+
+	@Test
+	public void authenticateWhenPkceAndS256MethodAndInvalidCodeVerifierThenThrowOAuth2AuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+
+		OAuth2Authorization authorization = TestOAuth2Authorizations
+				.authorization(registeredClient, createPkceAuthorizationParametersS256())
+				.build();
+		when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
+				.thenReturn(authorization);
+
+		Map<String, Object> parameters = createPkceTokenParameters("invalid-code-verifier");
+
+		OAuth2ClientAuthenticationToken authentication =
+				new OAuth2ClientAuthenticationToken(registeredClient.getClientId(), parameters);
+
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+				.isInstanceOf(OAuth2AuthenticationException.class)
+				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
+				.extracting("errorCode")
+				.isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
+	}
+
+	@Test
+	public void authenticateWhenPkceAndPlainMethodAndValidCodeVerifierThenAuthenticated() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+
+		OAuth2Authorization authorization = TestOAuth2Authorizations
+				.authorization(registeredClient, createPkceAuthorizationParametersPlain())
+				.build();
+		when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
+				.thenReturn(authorization);
+
+		Map<String, Object> parameters = createPkceTokenParameters(PLAIN_CODE_VERIFIER);
+
+		OAuth2ClientAuthenticationToken authentication =
+				new OAuth2ClientAuthenticationToken(registeredClient.getClientId(), parameters);
+
 		OAuth2ClientAuthenticationToken authenticationResult =
 				(OAuth2ClientAuthenticationToken) this.authenticationProvider.authenticate(authentication);
 		assertThat(authenticationResult.isAuthenticated()).isTrue();
-		assertThat(authenticationResult.getPrincipal().toString()).isEqualTo(this.registeredClient.getClientId());
+		assertThat(authenticationResult.getPrincipal().toString()).isEqualTo(registeredClient.getClientId());
 		assertThat(authenticationResult.getCredentials()).isNull();
-		assertThat(authenticationResult.getRegisteredClient()).isEqualTo(this.registeredClient);
+		assertThat(authenticationResult.getRegisteredClient()).isEqualTo(registeredClient);
+	}
+
+	@Test
+	public void authenticateWhenPkceAndMissingMethodThenDefaultPlainMethodAndAuthenticated() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+
+		Map<String, Object> authorizationRequestAdditionalParameters = createPkceAuthorizationParametersPlain();
+		authorizationRequestAdditionalParameters.remove(PkceParameterNames.CODE_CHALLENGE_METHOD);
+		OAuth2Authorization authorization = TestOAuth2Authorizations
+				.authorization(registeredClient, authorizationRequestAdditionalParameters)
+				.build();
+		when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
+				.thenReturn(authorization);
+
+		Map<String, Object> parameters = createPkceTokenParameters(PLAIN_CODE_VERIFIER);
+
+		OAuth2ClientAuthenticationToken authentication =
+				new OAuth2ClientAuthenticationToken(registeredClient.getClientId(), parameters);
+
+		OAuth2ClientAuthenticationToken authenticationResult =
+				(OAuth2ClientAuthenticationToken) this.authenticationProvider.authenticate(authentication);
+		assertThat(authenticationResult.isAuthenticated()).isTrue();
+		assertThat(authenticationResult.getPrincipal().toString()).isEqualTo(registeredClient.getClientId());
+		assertThat(authenticationResult.getCredentials()).isNull();
+		assertThat(authenticationResult.getRegisteredClient()).isEqualTo(registeredClient);
+	}
+
+	@Test
+	public void authenticateWhenPkceAndS256MethodAndValidCodeVerifierThenAuthenticated() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+
+		OAuth2Authorization authorization = TestOAuth2Authorizations
+				.authorization(registeredClient, createPkceAuthorizationParametersS256())
+				.build();
+		when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
+				.thenReturn(authorization);
+
+		Map<String, Object> parameters = createPkceTokenParameters(S256_CODE_VERIFIER);
+
+		OAuth2ClientAuthenticationToken authentication =
+				new OAuth2ClientAuthenticationToken(registeredClient.getClientId(), parameters);
+
+		OAuth2ClientAuthenticationToken authenticationResult =
+				(OAuth2ClientAuthenticationToken) this.authenticationProvider.authenticate(authentication);
+		assertThat(authenticationResult.isAuthenticated()).isTrue();
+		assertThat(authenticationResult.getPrincipal().toString()).isEqualTo(registeredClient.getClientId());
+		assertThat(authenticationResult.getCredentials()).isNull();
+		assertThat(authenticationResult.getRegisteredClient()).isEqualTo(registeredClient);
+	}
+
+	@Test
+	public void authenticateWhenPkceAndUnsupportedCodeChallengeMethodThenThrowOAuth2AuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+
+		Map<String, Object> authorizationRequestAdditionalParameters = createPkceAuthorizationParametersPlain();
+		// This should never happen: the Authorization endpoint should not allow it
+		authorizationRequestAdditionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "unsupported-challenge-method");
+		OAuth2Authorization authorization = TestOAuth2Authorizations
+				.authorization(registeredClient, authorizationRequestAdditionalParameters)
+				.build();
+		when(this.authorizationService.findByToken(eq(AUTHORIZATION_CODE), eq(TokenType.AUTHORIZATION_CODE)))
+				.thenReturn(authorization);
+
+		Map<String, Object> parameters = createPkceTokenParameters(PLAIN_CODE_VERIFIER);
+
+		OAuth2ClientAuthenticationToken authentication =
+				new OAuth2ClientAuthenticationToken(registeredClient.getClientId(), parameters);
+
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+				.isInstanceOf(OAuth2AuthenticationException.class)
+				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
+				.extracting("errorCode")
+				.isEqualTo(OAuth2ErrorCodes.SERVER_ERROR);
+	}
+
+	private static Map<String, Object> createPkceTokenParameters(String codeVerifier) {
+		Map<String, Object> parameters = new HashMap<>();
+		parameters.put(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
+		parameters.put(OAuth2ParameterNames.CODE, AUTHORIZATION_CODE);
+		parameters.put(PkceParameterNames.CODE_VERIFIER, codeVerifier);
+		return parameters;
+	}
+
+	private static Map<String, Object> createPkceAuthorizationParametersPlain() {
+		Map<String, Object> parameters = new HashMap<>();
+		parameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "plain");
+		parameters.put(PkceParameterNames.CODE_CHALLENGE, PLAIN_CODE_CHALLENGE);
+		return parameters;
+	}
+
+	private static Map<String, Object> createPkceAuthorizationParametersS256() {
+		Map<String, Object> parameters = new HashMap<>();
+		parameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
+		parameters.put(PkceParameterNames.CODE_CHALLENGE, S256_CODE_CHALLENGE);
+		return parameters;
 	}
 }

+ 18 - 3
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationTokenTests.java

@@ -19,6 +19,9 @@ import org.junit.Test;
 import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
 import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
 
+import java.util.HashMap;
+import java.util.Map;
+
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
@@ -31,14 +34,14 @@ public class OAuth2ClientAuthenticationTokenTests {
 
 	@Test
 	public void constructorWhenClientIdNullThenThrowIllegalArgumentException() {
-		assertThatThrownBy(() -> new OAuth2ClientAuthenticationToken(null, "secret"))
+		assertThatThrownBy(() -> new OAuth2ClientAuthenticationToken(null, "secret", null))
 				.isInstanceOf(IllegalArgumentException.class)
 				.hasMessage("clientId cannot be empty");
 	}
 
 	@Test
 	public void constructorWhenClientSecretNullThenThrowIllegalArgumentException() {
-		assertThatThrownBy(() -> new OAuth2ClientAuthenticationToken("clientId", null))
+		assertThatThrownBy(() -> new OAuth2ClientAuthenticationToken("clientId", null, null))
 				.isInstanceOf(IllegalArgumentException.class)
 				.hasMessage("clientSecret cannot be empty");
 	}
@@ -52,13 +55,25 @@ public class OAuth2ClientAuthenticationTokenTests {
 
 	@Test
 	public void constructorWhenClientCredentialsProvidedThenCreated() {
-		OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken("clientId", "secret");
+		OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken("clientId", "secret", null);
 		assertThat(authentication.isAuthenticated()).isFalse();
 		assertThat(authentication.getPrincipal().toString()).isEqualTo("clientId");
 		assertThat(authentication.getCredentials()).isEqualTo("secret");
 		assertThat(authentication.getRegisteredClient()).isNull();
 	}
 
+	@Test
+	public void constructorWhenClientIdProvidedThenCreated() {
+		Map<String, Object> additionalParameters = new HashMap<>();
+		additionalParameters.put("param1", "value1");
+		OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken("clientId", additionalParameters);
+		assertThat(authentication.isAuthenticated()).isFalse();
+		assertThat(authentication.getPrincipal().toString()).isEqualTo("clientId");
+		assertThat(authentication.getCredentials()).isNull();
+		assertThat(authentication.getAdditionalParameters()).isEqualTo(additionalParameters);
+		assertThat(authentication.getRegisteredClient()).isNull();
+	}
+
 	@Test
 	public void constructorWhenRegisteredClientProvidedThenCreated() {
 		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();

+ 1 - 1
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProviderTests.java

@@ -103,7 +103,7 @@ public class OAuth2ClientCredentialsAuthenticationProviderTests {
 	@Test
 	public void authenticateWhenClientPrincipalNotAuthenticatedThenThrowOAuth2AuthenticationException() {
 		OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(
-				this.registeredClient.getClientId(), this.registeredClient.getClientSecret());
+				this.registeredClient.getClientId(), this.registeredClient.getClientSecret(), null);
 		OAuth2ClientCredentialsAuthenticationToken authentication = new OAuth2ClientCredentialsAuthenticationToken(clientPrincipal);
 
 		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))

+ 26 - 0
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/ClientSecretBasicAuthenticationConverterTests.java

@@ -19,8 +19,11 @@ import org.junit.Test;
 import org.springframework.http.HttpHeaders;
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
 import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
 import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
 import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
 
 import java.net.URLEncoder;
@@ -29,6 +32,7 @@ import java.util.Base64;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.Assertions.entry;
 
 /**
  * Tests for {@link ClientSecretBasicAuthenticationConverter}.
@@ -96,6 +100,20 @@ public class ClientSecretBasicAuthenticationConverterTests {
 		assertThat(authentication.getCredentials()).isEqualTo("secret");
 	}
 
+	@Test
+	public void convertWhenConfidentialClientWithPkceParametersThenAdditionalParametersIncluded() throws Exception {
+		MockHttpServletRequest request = createPkceTokenRequest();
+		request.addHeader(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth("clientId", "secret"));
+		OAuth2ClientAuthenticationToken authentication = (OAuth2ClientAuthenticationToken) this.converter.convert(request);
+		assertThat(authentication.getPrincipal()).isEqualTo("clientId");
+		assertThat(authentication.getCredentials()).isEqualTo("secret");
+		assertThat(authentication.getAdditionalParameters())
+				.containsOnly(
+						entry(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue()),
+						entry(OAuth2ParameterNames.CODE, "code"),
+						entry(PkceParameterNames.CODE_VERIFIER, "code-verifier-1"));
+	}
+
 	private static String encodeBasicAuth(String clientId, String secret) throws Exception {
 		clientId = URLEncoder.encode(clientId, StandardCharsets.UTF_8.name());
 		secret = URLEncoder.encode(secret, StandardCharsets.UTF_8.name());
@@ -103,4 +121,12 @@ public class ClientSecretBasicAuthenticationConverterTests {
 		byte[] encodedBytes = Base64.getEncoder().encode(credentialsString.getBytes(StandardCharsets.UTF_8));
 		return new String(encodedBytes, StandardCharsets.UTF_8);
 	}
+
+	private static MockHttpServletRequest createPkceTokenRequest() {
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.addParameter(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
+		request.addParameter(OAuth2ParameterNames.CODE, "code");
+		request.addParameter(PkceParameterNames.CODE_VERIFIER, "code-verifier-1");
+		return request;
+	}
 }

+ 2 - 2
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilterTests.java

@@ -162,7 +162,7 @@ public class OAuth2ClientAuthenticationFilterTests {
 	@Test
 	public void doFilterWhenRequestMatchesAndBadCredentialsThenInvalidClientError() throws Exception {
 		when(this.authenticationConverter.convert(any(HttpServletRequest.class))).thenReturn(
-				new OAuth2ClientAuthenticationToken("clientId", "invalid-secret"));
+				new OAuth2ClientAuthenticationToken("clientId", "invalid-secret", null));
 		when(this.authenticationManager.authenticate(any(Authentication.class))).thenThrow(
 				new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT)));
 
@@ -185,7 +185,7 @@ public class OAuth2ClientAuthenticationFilterTests {
 	public void doFilterWhenRequestMatchesAndValidCredentialsThenProcessed() throws Exception {
 		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
 		when(this.authenticationConverter.convert(any(HttpServletRequest.class))).thenReturn(
-				new OAuth2ClientAuthenticationToken(registeredClient.getClientId(), registeredClient.getClientSecret()));
+				new OAuth2ClientAuthenticationToken(registeredClient.getClientId(), registeredClient.getClientSecret(), null));
 		when(this.authenticationManager.authenticate(any(Authentication.class))).thenReturn(
 				new OAuth2ClientAuthenticationToken(registeredClient));
 

+ 0 - 32
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2TokenEndpointFilterTests.java

@@ -34,7 +34,6 @@ import org.springframework.security.oauth2.core.OAuth2Error;
 import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
 import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
 import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
-import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
 import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
 import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
@@ -167,17 +166,6 @@ public class OAuth2TokenEndpointFilterTests {
 				OAuth2ParameterNames.GRANT_TYPE, OAuth2ErrorCodes.UNSUPPORTED_GRANT_TYPE, request);
 	}
 
-	@Test
-	public void doFilterWhenTokenRequestMultipleClientIdThenInvalidRequestError() throws Exception {
-		MockHttpServletRequest request = createAuthorizationCodeTokenRequest(
-				TestRegisteredClients.registeredClient().build());
-		request.addParameter(OAuth2ParameterNames.CLIENT_ID, "client-1");
-		request.addParameter(OAuth2ParameterNames.CLIENT_ID, "client-2");
-
-		doFilterWhenTokenRequestInvalidParameterThenError(
-				OAuth2ParameterNames.CLIENT_ID, OAuth2ErrorCodes.INVALID_REQUEST, request);
-	}
-
 	@Test
 	public void doFilterWhenTokenRequestMissingCodeThenInvalidRequestError() throws Exception {
 		MockHttpServletRequest request = createAuthorizationCodeTokenRequest(
@@ -208,26 +196,6 @@ public class OAuth2TokenEndpointFilterTests {
 				OAuth2ParameterNames.REDIRECT_URI, OAuth2ErrorCodes.INVALID_REQUEST, request);
 	}
 
-	@Test
-	public void doFilterWhenTokenRequestNotAuthenticatedAndMissingCodeVerifierThenInvalidRequestError() throws Exception {
-		MockHttpServletRequest request = createAuthorizationCodeTokenRequest(
-				TestRegisteredClients.registeredClient().build());
-
-		doFilterWhenTokenRequestInvalidParameterThenError(
-				PkceParameterNames.CODE_VERIFIER, OAuth2ErrorCodes.INVALID_REQUEST, request);
-	}
-
-	@Test
-	public void doFilterWhenTokenRequestNotAuthenticatedAndMultipleCodeVerifierThenInvalidRequestError() throws Exception {
-		MockHttpServletRequest request = createAuthorizationCodeTokenRequest(
-				TestRegisteredClients.registeredClient().build());
-		request.addParameter(PkceParameterNames.CODE_VERIFIER, "one-verifier");
-		request.addParameter(PkceParameterNames.CODE_VERIFIER, "two-verifier2");
-
-		doFilterWhenTokenRequestInvalidParameterThenError(
-				PkceParameterNames.CODE_VERIFIER, OAuth2ErrorCodes.INVALID_REQUEST, request);
-	}
-
 	@Test
 	public void doFilterWhenAuthorizationCodeTokenRequestValidThenAccessTokenResponse() throws Exception {
 		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();

+ 97 - 0
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/PublicClientAuthenticationConverterTests.java

@@ -0,0 +1,97 @@
+/*
+ * Copyright 2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.security.oauth2.server.authorization.web;
+
+import org.junit.Test;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
+import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.Assertions.entry;
+
+/**
+ * Tests for {@link PublicClientAuthenticationConverter}.
+ *
+ * @author Joe Grandja
+ */
+public class PublicClientAuthenticationConverterTests {
+	private PublicClientAuthenticationConverter converter = new PublicClientAuthenticationConverter();
+
+	@Test
+	public void convertWhenNotPublicClientThenReturnNull() {
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		Authentication authentication = this.converter.convert(request);
+		assertThat(authentication).isNull();
+	}
+
+	@Test
+	public void convertWhenMissingClientIdThenReturnNull() {
+		MockHttpServletRequest request = createPkceTokenRequest();
+		request.removeParameter(OAuth2ParameterNames.CLIENT_ID);
+		Authentication authentication = this.converter.convert(request);
+		assertThat(authentication).isNull();
+	}
+
+	@Test
+	public void convertWhenMultipleClientIdThenInvalidRequestError() {
+		MockHttpServletRequest request = createPkceTokenRequest();
+		request.addParameter(OAuth2ParameterNames.CLIENT_ID, "client-2");
+		assertThatThrownBy(() -> this.converter.convert(request))
+				.isInstanceOf(OAuth2AuthenticationException.class)
+				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
+				.extracting("errorCode")
+				.isEqualTo(OAuth2ErrorCodes.INVALID_REQUEST);
+	}
+
+	@Test
+	public void convertWhenMultipleCodeVerifierThenInvalidRequestError() {
+		MockHttpServletRequest request = createPkceTokenRequest();
+		request.addParameter(PkceParameterNames.CODE_VERIFIER, "code-verifier-2");
+		assertThatThrownBy(() -> this.converter.convert(request))
+				.isInstanceOf(OAuth2AuthenticationException.class)
+				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
+				.extracting("errorCode")
+				.isEqualTo(OAuth2ErrorCodes.INVALID_REQUEST);
+	}
+
+	@Test
+	public void convertWhenPublicClientThenReturnClientAuthenticationToken() {
+		MockHttpServletRequest request = createPkceTokenRequest();
+		OAuth2ClientAuthenticationToken authentication = (OAuth2ClientAuthenticationToken) this.converter.convert(request);
+		assertThat(authentication.getPrincipal()).isEqualTo("client-1");
+		assertThat(authentication.getAdditionalParameters())
+				.containsOnly(
+						entry(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue()),
+						entry(OAuth2ParameterNames.CODE, "code"),
+						entry(PkceParameterNames.CODE_VERIFIER, "code-verifier-1"));
+	}
+
+	private static MockHttpServletRequest createPkceTokenRequest() {
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.addParameter(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
+		request.addParameter(OAuth2ParameterNames.CODE, "code");
+		request.addParameter(OAuth2ParameterNames.CLIENT_ID, "client-1");
+		request.addParameter(PkceParameterNames.CODE_VERIFIER, "code-verifier-1");
+		return request;
+	}
+}