Ver Fonte

Polish gh-293

Joe Grandja há 3 anos atrás
pai
commit
f1a01597d9
31 ficheiros alterados com 958 adições e 1480 exclusões
  1. 18 17
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcClientMetadataClaimAccessor.java
  2. 16 14
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcClientMetadataClaimNames.java
  3. 41 19
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcClientRegistration.java
  4. 5 2
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/http/converter/OidcClientRegistrationHttpMessageConverter.java
  5. 103 97
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProvider.java
  6. 0 37
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationToken.java
  7. 27 26
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/ClientSettings.java
  8. 10 4
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/ConfigurationSettingNames.java
  9. 1 1
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/JwsAlgorithmMixin.java
  10. 0 33
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/MacAlgorithmMixin.java
  11. 4 4
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/OAuth2AuthorizationServerJackson2Module.java
  12. 42 32
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProvider.java
  13. 19 10
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java
  14. 2 2
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2ClientAuthenticationFilter.java
  15. 2 1
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/ClientSecretBasicAuthenticationConverter.java
  16. 4 2
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/ClientSecretPostAuthenticationConverter.java
  17. 27 30
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/JwtClientAssertionAuthenticationConverter.java
  18. 11 10
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2EndpointUtils.java
  19. 76 195
      oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcClientRegistrationTests.java
  20. 0 65
      oauth2-authorization-server/src/test/java/org/springframework/security/config/util/ValueCaptureMatcher.java
  21. 22 11
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/OidcClientRegistrationTests.java
  22. 15 13
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/http/converter/OidcClientRegistrationHttpMessageConverterTests.java
  23. 23 0
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/jose/TestJwks.java
  24. 280 158
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProviderTests.java
  25. 3 11
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationTokenTests.java
  26. 0 436
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/RegisteredClientJwtAssertionDecoderFactoryTests.java
  27. 2 9
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/client/JdbcRegisteredClientRepositoryTests.java
  28. 9 10
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/ClientSettingsTests.java
  29. 150 164
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProviderTests.java
  30. 0 1
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilterTests.java
  31. 46 66
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/authentication/JwtClientAssertionAuthenticationConverterTests.java

+ 18 - 17
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcClientMetadataClaimAccessor.java

@@ -20,7 +20,10 @@ import java.time.Instant;
 import java.util.List;
 
 import org.springframework.security.oauth2.core.ClaimAccessor;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
 import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
+import org.springframework.security.oauth2.jwt.Jwt;
 
 /**
  * A {@link ClaimAccessor} for the "claims" that are contained
@@ -100,14 +103,12 @@ public interface OidcClientMetadataClaimAccessor extends ClaimAccessor {
 	}
 
 	/**
-	 * Returns the {@link SignatureAlgorithm JWS} algorithm that must be used for signing the JWT used to authenticate
-	 * the Client at the Token Endpoint for the {@code private_key_jwt} and {@code client_secret_jwt} authentication
-	 * methods {@code (token_endpoint_auth_signing_alg)}
+	 * Returns the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT} used to authenticate
+	 * the Client at the Token Endpoint for the {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT private_key_jwt} and
+	 * {@link ClientAuthenticationMethod#CLIENT_SECRET_JWT client_secret_jwt} authentication methods {@code (token_endpoint_auth_signing_alg)}.
 	 *
-	 * @return the {@link SignatureAlgorithm JWS} algorithm that must be used for signing the JWT used to authenticate
-	 * 	       the Client at the Token Endpoint for the {@code private_key_jwt} and {@code client_secret_jwt}
-	 * 	       authentication methods {@code (token_endpoint_auth_signing_alg)}
-	 * @since 0.2.1
+	 * @return the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT} used to authenticate the Client at the Token Endpoint
+	 * @since 0.2.2
 	 */
 	default String getTokenEndpointAuthenticationSigningAlgorithm() {
 		return getClaimAsString(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_SIGNING_ALG);
@@ -140,6 +141,16 @@ public interface OidcClientMetadataClaimAccessor extends ClaimAccessor {
 		return getClaimAsStringList(OidcClientMetadataClaimNames.SCOPE);
 	}
 
+	/**
+	 * Returns the {@code URL} for the Client's JSON Web Key Set {@code (jwks_uri)}.
+	 *
+	 * @return the {@code URL} for the Client's JSON Web Key Set {@code (jwks_uri)}
+	 * @since 0.2.2
+	 */
+	default URL getJwkSetUrl() {
+		return getClaimAsURL(OidcClientMetadataClaimNames.JWKS_URI);
+	}
+
 	/**
 	 * Returns the {@link SignatureAlgorithm JWS} algorithm required for signing the {@link OidcIdToken ID Token} issued to the Client {@code (id_token_signed_response_alg)}.
 	 *
@@ -169,14 +180,4 @@ public interface OidcClientMetadataClaimAccessor extends ClaimAccessor {
 		return getClaimAsURL(OidcClientMetadataClaimNames.REGISTRATION_CLIENT_URI);
 	}
 
-	/**
-	 * Returns {@code URL} for the Client's JSON Web Key Set {@code (jwks_uri)}
-	 *
-	 * @return {@code URL} for the Client's JSON Web Key Set {@code (jwks_uri)}
-	 * @since 0.2.1
-	 */
-	default URL getJwkSetUrl() {
-		return getClaimAsURL(OidcClientMetadataClaimNames.JWKS_URI);
-	}
-
 }

+ 16 - 14
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcClientMetadataClaimNames.java

@@ -15,8 +15,9 @@
  */
 package org.springframework.security.oauth2.core.oidc;
 
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
 import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
-import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
+import org.springframework.security.oauth2.jwt.Jwt;
 
 /**
  * The names of the "claims" defined by OpenID Connect Dynamic Client Registration 1.0
@@ -64,6 +65,14 @@ public interface OidcClientMetadataClaimNames {
 	 */
 	String TOKEN_ENDPOINT_AUTH_METHOD = "token_endpoint_auth_method";
 
+	/**
+	 * {@code token_endpoint_auth_signing_alg} - the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT}
+	 * used to authenticate the Client at the Token Endpoint for the {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT private_key_jwt} and
+	 * {@link ClientAuthenticationMethod#CLIENT_SECRET_JWT client_secret_jwt} authentication methods
+	 * @since 0.2.2
+	 */
+	String TOKEN_ENDPOINT_AUTH_SIGNING_ALG = "token_endpoint_auth_signing_alg";
+
 	/**
 	 * {@code grant_types} - the OAuth 2.0 {@code grant_type} values that the Client will restrict itself to using
 	 */
@@ -79,6 +88,12 @@ public interface OidcClientMetadataClaimNames {
 	 */
 	String SCOPE = "scope";
 
+	/**
+	 * {@code jwks_uri} - the {@code URL} for the Client's JSON Web Key Set
+	 * @since 0.2.2
+	 */
+	String JWKS_URI = "jwks_uri";
+
 	/**
 	 * {@code id_token_signed_response_alg} - the {@link JwsAlgorithm JWS} algorithm required for signing the {@link OidcIdToken ID Token} issued to the Client
 	 */
@@ -96,17 +111,4 @@ public interface OidcClientMetadataClaimNames {
 	 */
 	String REGISTRATION_CLIENT_URI = "registration_client_uri";
 
-	/**
-	 * {@code jwks_uri} - {@code URL} for the Client's JSON Web Key Set
-	 * @since 0.2.1
-	 */
-	String JWKS_URI = "jwks_uri";
-
-	/**
-	 * {@code token_endpoint_auth_signing_alg} - {@link SignatureAlgorithm JWS} algorithm that must be used for signing
-	 * the JWT used to authenticate the Client at the Token Endpoint for the {@code private_key_jwt} and {@code client_secret_jwt}
-	 * authentication methods
-	 * @since 0.2.1
-	 */
-	String TOKEN_ENDPOINT_AUTH_SIGNING_ALG = "token_endpoint_auth_signing_alg";
 }

+ 41 - 19
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/OidcClientRegistration.java

@@ -16,6 +16,8 @@
 package org.springframework.security.oauth2.core.oidc;
 
 import java.io.Serializable;
+import java.net.URI;
+import java.net.URL;
 import java.time.Instant;
 import java.util.Collections;
 import java.util.LinkedHashMap;
@@ -24,8 +26,11 @@ import java.util.List;
 import java.util.Map;
 import java.util.function.Consumer;
 
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
 import org.springframework.security.oauth2.core.Version;
+import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
 import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
+import org.springframework.security.oauth2.jwt.Jwt;
 import org.springframework.util.Assert;
 
 /**
@@ -173,17 +178,17 @@ public final class OidcClientRegistration implements OidcClientMetadataClaimAcce
 		}
 
 		/**
-		 * Sets the {@link SignatureAlgorithm JWS} algorithm that must be used for signing the JWT used to authenticate
-		 * the Client at the Token Endpoint for the {@code private_key_jwt} and {@code client_secret_jwt} authentication
-		 * methods
-		 * @param signingAlgorithm the {@link SignatureAlgorithm JWS} algorithm that must be used for signing
-		 *        the JWT used to authenticate the Client at the Token Endpoint for the {@code private_key_jwt} and
-		 *        {@code client_secret_jwt} authentication methods
+		 * Sets the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT} used to authenticate
+		 * the Client at the Token Endpoint for the {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT private_key_jwt} and
+		 * {@link ClientAuthenticationMethod#CLIENT_SECRET_JWT client_secret_jwt} authentication methods, OPTIONAL.
+
+		 * @param authenticationSigningAlgorithm the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT}
+		 *                                       used to authenticate the Client at the Token Endpoint
 		 * @return the {@link Builder} for further configuration
-		 * @since 0.2.1
+		 * @since 0.2.2
 		 */
-		public Builder tokenEndpointAuthenticationSigningAlgorithm(String signingAlgorithm) {
-			return claim(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_SIGNING_ALG, signingAlgorithm);
+		public Builder tokenEndpointAuthenticationSigningAlgorithm(String authenticationSigningAlgorithm) {
+			return claim(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_SIGNING_ALG, authenticationSigningAlgorithm);
 		}
 
 		/**
@@ -255,6 +260,17 @@ public final class OidcClientRegistration implements OidcClientMetadataClaimAcce
 			return this;
 		}
 
+		/**
+		 * Sets the {@code URL} for the Client's JSON Web Key Set, OPTIONAL.
+		 *
+		 * @param jwkSetUrl the {@code URL} for the Client's JSON Web Key Set
+		 * @return the {@link Builder} for further configuration
+		 * @since 0.2.2
+		 */
+		public Builder jwkSetUrl(String jwkSetUrl) {
+			return claim(OidcClientMetadataClaimNames.JWKS_URI, jwkSetUrl);
+		}
+
 		/**
 		 * Sets the {@link SignatureAlgorithm JWS} algorithm required for signing the {@link OidcIdToken ID Token} issued to the Client, OPTIONAL.
 		 *
@@ -287,16 +303,6 @@ public final class OidcClientRegistration implements OidcClientMetadataClaimAcce
 			return claim(OidcClientMetadataClaimNames.REGISTRATION_CLIENT_URI, registrationClientUrl);
 		}
 
-		/**
-		 * Sets {@code URL} for the Client's JSON Web Key Set {@code (jwks_uri)}
-		 * @param jwksSetUrl {@code URL} for the Client's JSON Web Key Set {@code (jwks_uri)}
-		 * @return the {@link Builder} for further configuration
-		 * @since 0.2.1
-		 */
-		public Builder jwkSetUrl(String jwksSetUrl) {
-			return claim(OidcClientMetadataClaimNames.JWKS_URI, jwksSetUrl);
-		}
-
 		/**
 		 * Sets the claim.
 		 *
@@ -363,6 +369,9 @@ public final class OidcClientRegistration implements OidcClientMetadataClaimAcce
 				Assert.isInstanceOf(List.class, this.claims.get(OidcClientMetadataClaimNames.SCOPE), "scope must be of type List");
 				Assert.notEmpty((List<?>) this.claims.get(OidcClientMetadataClaimNames.SCOPE), "scope cannot be empty");
 			}
+			if (this.claims.get(OidcClientMetadataClaimNames.JWKS_URI) != null) {
+				validateURL(this.claims.get(OidcClientMetadataClaimNames.JWKS_URI), "jwksUri must be a valid URL");
+			}
 		}
 
 		@SuppressWarnings("unchecked")
@@ -382,5 +391,18 @@ public final class OidcClientRegistration implements OidcClientMetadataClaimAcce
 			valuesConsumer.accept(values);
 		}
 
+		private static void validateURL(Object url, String errorMessage) {
+			if (URL.class.isAssignableFrom(url.getClass())) {
+				return;
+			}
+
+			try {
+				new URI(url.toString()).toURL();
+			} catch (Exception ex) {
+				throw new IllegalArgumentException(errorMessage, ex);
+			}
+		}
+
 	}
+
 }

+ 5 - 2
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/core/oidc/http/converter/OidcClientRegistrationHttpMessageConverter.java

@@ -15,6 +15,7 @@
  */
 package org.springframework.security.oauth2.core.oidc.http.converter;
 
+import java.net.URL;
 import java.time.Instant;
 import java.util.Arrays;
 import java.util.Collection;
@@ -130,6 +131,7 @@ public class OidcClientRegistrationHttpMessageConverter extends AbstractHttpMess
 		private static final TypeDescriptor OBJECT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Object.class);
 		private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
 		private static final TypeDescriptor INSTANT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Instant.class);
+		private static final TypeDescriptor URL_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(URL.class);
 		private static final Converter<Object, ?> INSTANT_CONVERTER = getConverter(INSTANT_TYPE_DESCRIPTOR);
 		private final ClaimTypeConverter claimTypeConverter;
 
@@ -137,6 +139,7 @@ public class OidcClientRegistrationHttpMessageConverter extends AbstractHttpMess
 			Converter<Object, ?> stringConverter = getConverter(STRING_TYPE_DESCRIPTOR);
 			Converter<Object, ?> collectionStringConverter = getConverter(
 					TypeDescriptor.collection(Collection.class, STRING_TYPE_DESCRIPTOR));
+			Converter<Object, ?> urlConverter = getConverter(URL_TYPE_DESCRIPTOR);
 
 			Map<String, Converter<Object, ?>> claimConverters = new HashMap<>();
 			claimConverters.put(OidcClientMetadataClaimNames.CLIENT_ID, stringConverter);
@@ -146,12 +149,12 @@ public class OidcClientRegistrationHttpMessageConverter extends AbstractHttpMess
 			claimConverters.put(OidcClientMetadataClaimNames.CLIENT_NAME, stringConverter);
 			claimConverters.put(OidcClientMetadataClaimNames.REDIRECT_URIS, collectionStringConverter);
 			claimConverters.put(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD, stringConverter);
+			claimConverters.put(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_SIGNING_ALG, stringConverter);
 			claimConverters.put(OidcClientMetadataClaimNames.GRANT_TYPES, collectionStringConverter);
 			claimConverters.put(OidcClientMetadataClaimNames.RESPONSE_TYPES, collectionStringConverter);
 			claimConverters.put(OidcClientMetadataClaimNames.SCOPE, MapOidcClientRegistrationConverter::convertScope);
+			claimConverters.put(OidcClientMetadataClaimNames.JWKS_URI, urlConverter);
 			claimConverters.put(OidcClientMetadataClaimNames.ID_TOKEN_SIGNED_RESPONSE_ALG, stringConverter);
-			claimConverters.put(OidcClientMetadataClaimNames.JWKS_URI, stringConverter);
-			claimConverters.put(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_SIGNING_ALG, stringConverter);
 			this.claimTypeConverter = new ClaimTypeConverter(claimConverters);
 		}
 

+ 103 - 97
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProvider.java

@@ -18,15 +18,17 @@ package org.springframework.security.oauth2.server.authorization.authentication;
 import java.nio.charset.StandardCharsets;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
 import java.util.Base64;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
-import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
-import java.util.function.Function;
+import java.util.function.Predicate;
+
+import javax.crypto.spec.SecretKeySpec;
 
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.authentication.AuthenticationProvider;
@@ -49,6 +51,7 @@ import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
 import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
 import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
 import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.jwt.JwtClaimNames;
 import org.springframework.security.oauth2.jwt.JwtClaimValidator;
 import org.springframework.security.oauth2.jwt.JwtDecoder;
 import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
@@ -61,9 +64,9 @@ import org.springframework.security.oauth2.server.authorization.client.Registere
 import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
 import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
 import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
 import org.springframework.util.StringUtils;
-
-import javax.crypto.spec.SecretKeySpec;
+import org.springframework.web.util.UriComponentsBuilder;
 
 /**
  * An {@link AuthenticationProvider} implementation used for authenticating an OAuth 2.0 Client.
@@ -80,16 +83,13 @@ import javax.crypto.spec.SecretKeySpec;
  * @see PasswordEncoder
  */
 public final class OAuth2ClientAuthenticationProvider implements AuthenticationProvider {
-	private static final String CLIENT_AUTHENTICATION_ERROR_URI = "https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-3.2.1";
-
+	private static final String CLIENT_AUTHENTICATION_ERROR_URI = "https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-04#section-3.2.1";
 	private static final ClientAuthenticationMethod JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD =
 			new ClientAuthenticationMethod("urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
-
 	private static final OAuth2TokenType AUTHORIZATION_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.CODE);
 	private final RegisteredClientRepository registeredClientRepository;
 	private final OAuth2AuthorizationService authorizationService;
-	private JwtDecoderFactory<RegisteredClient> jwtDecoderFactory;
-	private ProviderSettings providerSettings;
+	private final JwtClientAssertionDecoderFactory jwtClientAssertionDecoderFactory;
 	private PasswordEncoder passwordEncoder;
 
 	/**
@@ -104,8 +104,8 @@ public final class OAuth2ClientAuthenticationProvider implements AuthenticationP
 		Assert.notNull(authorizationService, "authorizationService cannot be null");
 		this.registeredClientRepository = registeredClientRepository;
 		this.authorizationService = authorizationService;
+		this.jwtClientAssertionDecoderFactory = new JwtClientAssertionDecoderFactory();
 		this.passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
-		this.jwtDecoderFactory = new RegisteredClientJwtAssertionDecoderFactory();
 	}
 
 	/**
@@ -123,7 +123,7 @@ public final class OAuth2ClientAuthenticationProvider implements AuthenticationP
 
 	@Autowired
 	protected void setProviderSettings(ProviderSettings providerSettings) {
-		this.providerSettings = providerSettings;
+		this.jwtClientAssertionDecoderFactory.setProviderSettings(providerSettings);
 	}
 
 	@Override
@@ -132,11 +132,16 @@ public final class OAuth2ClientAuthenticationProvider implements AuthenticationP
 				(OAuth2ClientAuthenticationToken) authentication;
 
 		return JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD.equals(clientAuthentication.getClientAuthenticationMethod()) ?
-				authenticateClientAssertion(authentication) :
-				authenticationClientCredentials(authentication);
+				authenticateJwtClientAssertion(authentication) :
+				authenticateClientCredentials(authentication);
 	}
 
-	private Authentication authenticationClientCredentials(Authentication authentication) throws AuthenticationException {
+	@Override
+	public boolean supports(Class<?> authentication) {
+		return OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication);
+	}
+
+	private Authentication authenticateClientCredentials(Authentication authentication) throws AuthenticationException {
 		OAuth2ClientAuthenticationToken clientAuthentication =
 				(OAuth2ClientAuthenticationToken) authentication;
 
@@ -171,7 +176,7 @@ public final class OAuth2ClientAuthenticationProvider implements AuthenticationP
 				clientAuthentication.getClientAuthenticationMethod(), clientAuthentication.getCredentials());
 	}
 
-	private Authentication authenticateClientAssertion(Authentication authentication) throws AuthenticationException {
+	private Authentication authenticateJwtClientAssertion(Authentication authentication) throws AuthenticationException {
 		OAuth2ClientAuthenticationToken clientAuthentication =
 				(OAuth2ClientAuthenticationToken) authentication;
 
@@ -181,26 +186,20 @@ public final class OAuth2ClientAuthenticationProvider implements AuthenticationP
 			throwInvalidClient(OAuth2ParameterNames.CLIENT_ID);
 		}
 
-		Set<ClientAuthenticationMethod> allowedAuthenticationMethods = registeredClient.getClientAuthenticationMethods();
-
-		if (!allowedAuthenticationMethods.contains(ClientAuthenticationMethod.CLIENT_SECRET_JWT) &&
-				!allowedAuthenticationMethods.contains(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) {
+		if (!registeredClient.getClientAuthenticationMethods().contains(ClientAuthenticationMethod.PRIVATE_KEY_JWT) &&
+				!registeredClient.getClientAuthenticationMethods().contains(ClientAuthenticationMethod.CLIENT_SECRET_JWT)) {
 			throwInvalidClient("authentication_method");
 		}
 
 		boolean credentialsAuthenticated = false;
 
+		Jwt jwtAssertion = null;
+		JwtDecoder jwtDecoder = this.jwtClientAssertionDecoderFactory.createDecoder(registeredClient);
 		try {
-			JwtDecoder jwtDecoder = this.jwtDecoderFactory.createDecoder(registeredClient);
-			Jwt jwt = jwtDecoder.decode(clientAuthentication.getCredentials().toString());
-			List<String> aud = jwt.getClaimAsStringList("aud");
-			String issuer = getIssuerUri(clientAuthentication.getRequestUri());
-			if (aud == null || !aud.contains(issuer)) {
-				throwInvalidClient(OAuth2ParameterNames.CLIENT_ASSERTION);
-			}
+			jwtAssertion = jwtDecoder.decode(clientAuthentication.getCredentials().toString());
 			credentialsAuthenticated = true;
-		} catch (JwtException e) {
-			throwInvalidClient(OAuth2ParameterNames.CLIENT_ASSERTION);
+		} catch (JwtException ex) {
+			throwInvalidClient(OAuth2ParameterNames.CLIENT_ASSERTION, ex);
 		}
 
 		boolean pkceAuthenticated = authenticatePkceIfAvailable(clientAuthentication, registeredClient);
@@ -209,29 +208,12 @@ public final class OAuth2ClientAuthenticationProvider implements AuthenticationP
 			throwInvalidClient("credentials");
 		}
 
-		JwsAlgorithm tokenEndpointSigningAlgorithm = registeredClient.getClientSettings().getTokenEndpointSigningAlgorithm();
-		ClientAuthenticationMethod clientAuthentiationMethod = tokenEndpointSigningAlgorithm instanceof MacAlgorithm ?
-				ClientAuthenticationMethod.CLIENT_SECRET_JWT : ClientAuthenticationMethod.PRIVATE_KEY_JWT;
-
-		return new OAuth2ClientAuthenticationToken(registeredClient,
-				clientAuthentiationMethod, clientAuthentication.getCredentials());
-	}
-
-	private String getIssuerUri(String requestUri) throws AuthenticationException {
-		if (requestUri.endsWith(providerSettings.getTokenEndpoint())) {
-			return providerSettings.getIssuer() + providerSettings.getTokenEndpoint();
-		} else if (requestUri.endsWith(providerSettings.getTokenIntrospectionEndpoint())) {
-			return providerSettings.getIssuer() + providerSettings.getTokenIntrospectionEndpoint();
-		} else if (requestUri.endsWith(providerSettings.getTokenRevocationEndpoint())) {
-			return providerSettings.getIssuer() + providerSettings.getTokenRevocationEndpoint();
-		}
-		throwInvalidClient(OAuth2ParameterNames.CLIENT_ASSERTION);
-		return null;
-	}
+		ClientAuthenticationMethod clientAuthenticationMethod =
+				registeredClient.getClientSettings().getTokenEndpointAuthenticationSigningAlgorithm() instanceof SignatureAlgorithm ?
+						ClientAuthenticationMethod.PRIVATE_KEY_JWT :
+						ClientAuthenticationMethod.CLIENT_SECRET_JWT;
 
-	@Override
-	public boolean supports(Class<?> authentication) {
-		return OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication);
+		return new OAuth2ClientAuthenticationToken(registeredClient, clientAuthenticationMethod, jwtAssertion);
 	}
 
 	private boolean authenticatePkceIfAvailable(OAuth2ClientAuthenticationToken clientAuthentication,
@@ -298,29 +280,21 @@ public final class OAuth2ClientAuthenticationProvider implements AuthenticationP
 	}
 
 	private static void throwInvalidClient(String parameterName) {
+		throwInvalidClient(parameterName, null);
+	}
+
+	private static void throwInvalidClient(String parameterName, Throwable cause) {
 		OAuth2Error error = new OAuth2Error(
 				OAuth2ErrorCodes.INVALID_CLIENT,
 				"Client authentication failed: " + parameterName,
 				CLIENT_AUTHENTICATION_ERROR_URI);
-		throw new OAuth2AuthenticationException(error);
+		throw new OAuth2AuthenticationException(error, error.toString(), cause);
 	}
 
-	private static class CachedJwtDecoder {
-		private final NimbusJwtDecoder jwtDecoder;
-		private final RegisteredClient registeredClient;
-
-		CachedJwtDecoder(NimbusJwtDecoder jwtDecoder, RegisteredClient registeredClient) {
-			this.jwtDecoder = jwtDecoder;
-			this.registeredClient = registeredClient;
-		}
-	}
-
-	private static class RegisteredClientJwtAssertionDecoderFactory implements JwtDecoderFactory<RegisteredClient> {
-
-		private static final String CLIENT_ASSERTION_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7523#section-3";
+	private static class JwtClientAssertionDecoderFactory implements JwtDecoderFactory<RegisteredClient> {
+		private static final String JWT_CLIENT_AUTHENTICATION_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7523#section-3";
 
 		private static final Map<JwsAlgorithm, String> JCA_ALGORITHM_MAPPINGS;
-
 		static {
 			Map<JwsAlgorithm, String> mappings = new HashMap<>();
 			mappings.put(MacAlgorithm.HS256, "HmacSHA256");
@@ -329,68 +303,100 @@ public final class OAuth2ClientAuthenticationProvider implements AuthenticationP
 			JCA_ALGORITHM_MAPPINGS = Collections.unmodifiableMap(mappings);
 		}
 
-		private final Function<RegisteredClient, JwsAlgorithm> jwsAlgorithmResolver =
-				rc -> rc.getClientSettings().getTokenEndpointSigningAlgorithm();
+		private final Map<String, JwtDecoder> jwtDecoders = new ConcurrentHashMap<>();
+		private List<String> providerAudience = Collections.emptyList();
 
-		private final Map<String, CachedJwtDecoder> cachedDecoders = new ConcurrentHashMap<>();
+		private void setProviderSettings(ProviderSettings providerSettings) {
+			this.providerAudience = getProviderAudience(providerSettings);
+		}
 
 		@Override
 		public JwtDecoder createDecoder(RegisteredClient registeredClient) {
 			Assert.notNull(registeredClient, "registeredClient cannot be null");
-
-			CachedJwtDecoder cachedDecoder = this.cachedDecoders.get(registeredClient.getClientId());
-			if (cachedDecoder != null && registeredClient.equals(cachedDecoder.registeredClient)) {
-				return cachedDecoder.jwtDecoder;
-			}
-
-			cachedDecoder = new CachedJwtDecoder(buildDecoder(registeredClient), registeredClient);
-			cachedDecoder.jwtDecoder.setJwtValidator(createTokenValidator(registeredClient));
-			this.cachedDecoders.put(registeredClient.getClientId(), cachedDecoder);
-			return cachedDecoder.jwtDecoder;
+			return this.jwtDecoders.computeIfAbsent(registeredClient.getId(), (key) -> {
+				NimbusJwtDecoder jwtDecoder = buildDecoder(registeredClient);
+				jwtDecoder.setJwtValidator(createJwtValidator(registeredClient));
+				return jwtDecoder;
+			});
 		}
 
 		private NimbusJwtDecoder buildDecoder(RegisteredClient registeredClient) {
-			JwsAlgorithm jwsAlgorithm = this.jwsAlgorithmResolver.apply(registeredClient);
-
-			if (jwsAlgorithm != null && SignatureAlgorithm.class.isAssignableFrom(jwsAlgorithm.getClass())) {
+			JwsAlgorithm jwsAlgorithm = registeredClient.getClientSettings().getTokenEndpointAuthenticationSigningAlgorithm();
+			if (jwsAlgorithm instanceof SignatureAlgorithm) {
 				String jwkSetUrl = registeredClient.getClientSettings().getJwkSetUrl();
 				if (!StringUtils.hasText(jwkSetUrl)) {
 					OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
-							"misconfigured client", CLIENT_ASSERTION_ERROR_URI);
-					throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
+							"Failed to find a Signature Verifier for Client: '"
+									+ registeredClient.getId()
+									+ "'. Check to ensure you have configured the JWK Set URL.",
+							JWT_CLIENT_AUTHENTICATION_ERROR_URI);
+					throw new OAuth2AuthenticationException(oauth2Error);
 				}
 				return NimbusJwtDecoder.withJwkSetUri(jwkSetUrl).jwsAlgorithm((SignatureAlgorithm) jwsAlgorithm).build();
 			}
-
-			if (jwsAlgorithm != null && MacAlgorithm.class.isAssignableFrom(jwsAlgorithm.getClass())) {
+			if (jwsAlgorithm instanceof MacAlgorithm) {
 				String clientSecret = registeredClient.getClientSecret();
 				if (!StringUtils.hasText(clientSecret)) {
 					OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
-							"misconfigured client", CLIENT_ASSERTION_ERROR_URI);
-					throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
+							"Failed to find a Signature Verifier for Client: '"
+									+ registeredClient.getId()
+									+ "'. Check to ensure you have configured the client secret.",
+							JWT_CLIENT_AUTHENTICATION_ERROR_URI);
+					throw new OAuth2AuthenticationException(oauth2Error);
 				}
 				SecretKeySpec secretKeySpec = new SecretKeySpec(clientSecret.getBytes(StandardCharsets.UTF_8),
 						JCA_ALGORITHM_MAPPINGS.get(jwsAlgorithm));
 				return NimbusJwtDecoder.withSecretKey(secretKeySpec).macAlgorithm((MacAlgorithm) jwsAlgorithm).build();
 			}
-
 			OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
-					"misconfigured client", CLIENT_ASSERTION_ERROR_URI);
-			throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
+					"Failed to find a Signature Verifier for Client: '"
+							+ registeredClient.getId()
+							+ "'. Check to ensure you have configured a valid JWS Algorithm: '" + jwsAlgorithm + "'.",
+					JWT_CLIENT_AUTHENTICATION_ERROR_URI);
+			throw new OAuth2AuthenticationException(oauth2Error);
 		}
 
-		private OAuth2TokenValidator<Jwt> createTokenValidator(RegisteredClient registeredClient) {
+		private OAuth2TokenValidator<Jwt> createJwtValidator(RegisteredClient registeredClient) {
 			String clientId = registeredClient.getClientId();
 			return new DelegatingOAuth2TokenValidator<>(
-					new JwtClaimValidator<String>("iss", clientId::equals),      // RFC 7523 section 3 (iss)
-					new JwtClaimValidator<String>("sub", clientId::equals),      // RFC 7523 section 3 (sub)
-					new JwtClaimValidator<>("exp", Objects::nonNull),            // RFC 7523 section 3 (exp != null)
-					new JwtTimestampValidator()                                  // RFC 7523 section 3 (exp, nbf)
+					new JwtClaimValidator<>(JwtClaimNames.ISS, clientId::equals),
+					new JwtClaimValidator<>(JwtClaimNames.SUB, clientId::equals),
+					new JwtClaimValidator<>(JwtClaimNames.AUD, containsProviderAudience()),
+					new JwtClaimValidator<>(JwtClaimNames.EXP, Objects::nonNull),
+					new JwtTimestampValidator()
 			);
-			// The `aud` claim is not verified here
+		}
 
-			// TODO RFC 7523 section 3 #7: JWT may contain "jti" claim that provides unique identified for the token (OPTIONAL)
+		private Predicate<List<String>> containsProviderAudience() {
+			return (audienceClaim) -> {
+				if (CollectionUtils.isEmpty(audienceClaim)) {
+					return false;
+				}
+				for (String audience : audienceClaim) {
+					if (this.providerAudience.contains(audience)) {
+						return true;
+					}
+				}
+				return false;
+			};
 		}
+
+		private static List<String> getProviderAudience(ProviderSettings providerSettings) {
+			if (!StringUtils.hasText(providerSettings.getIssuer())) {
+				return Collections.emptyList();
+			}
+			List<String> providerAudience = new ArrayList<>();
+			providerAudience.add(providerSettings.getIssuer());
+			providerAudience.add(asUrl(providerSettings.getIssuer(), providerSettings.getTokenEndpoint()));
+			providerAudience.add(asUrl(providerSettings.getIssuer(), providerSettings.getTokenIntrospectionEndpoint()));
+			providerAudience.add(asUrl(providerSettings.getIssuer(), providerSettings.getTokenRevocationEndpoint()));
+			return providerAudience;
+		}
+
+		private static String asUrl(String issuer, String endpoint) {
+			return UriComponentsBuilder.fromUriString(issuer).path(endpoint).build().toUriString();
+		}
+
 	}
 
 }

+ 0 - 37
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationToken.java

@@ -41,7 +41,6 @@ import org.springframework.util.Assert;
 @Transient
 public class OAuth2ClientAuthenticationToken extends AbstractAuthenticationToken {
 	private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
-	private final String requestUri;
 	private final String clientId;
 	private final RegisteredClient registeredClient;
 	private final ClientAuthenticationMethod clientAuthenticationMethod;
@@ -56,37 +55,11 @@ public class OAuth2ClientAuthenticationToken extends AbstractAuthenticationToken
 	 * @param credentials the client credentials
 	 * @param additionalParameters the additional parameters
 	 */
-	@Deprecated
 	public OAuth2ClientAuthenticationToken(String clientId, ClientAuthenticationMethod clientAuthenticationMethod,
 			@Nullable Object credentials, @Nullable Map<String, Object> additionalParameters) {
 		super(Collections.emptyList());
 		Assert.hasText(clientId, "clientId cannot be empty");
 		Assert.notNull(clientAuthenticationMethod, "clientAuthenticationMethod cannot be null");
-		this.requestUri = null;
-		this.clientId = clientId;
-		this.registeredClient = null;
-		this.clientAuthenticationMethod = clientAuthenticationMethod;
-		this.credentials = credentials;
-		this.additionalParameters = Collections.unmodifiableMap(
-				additionalParameters != null ? additionalParameters : Collections.emptyMap());
-	}
-
-	/**
-	 * Constructs an {@code OAuth2ClientAuthenticationToken} using the provided parameters.
-	 *
-	 * @param requestUri the issuer identifier
-	 * @param clientId the client identifier
-	 * @param clientAuthenticationMethod the authentication method used by the client
-	 * @param credentials the client credentials
-	 * @param additionalParameters the additional parameters
-	 */
-	public OAuth2ClientAuthenticationToken(String requestUri, String clientId, ClientAuthenticationMethod clientAuthenticationMethod,
-			@Nullable Object credentials, @Nullable Map<String, Object> additionalParameters) {
-		super(Collections.emptyList());
-		Assert.hasText(requestUri, "requestUri cannot be empty");
-		Assert.hasText(clientId, "clientId cannot be empty");
-		Assert.notNull(clientAuthenticationMethod, "clientAuthenticationMethod cannot be null");
-		this.requestUri = requestUri;
 		this.clientId = clientId;
 		this.registeredClient = null;
 		this.clientAuthenticationMethod = clientAuthenticationMethod;
@@ -107,7 +80,6 @@ public class OAuth2ClientAuthenticationToken extends AbstractAuthenticationToken
 		super(Collections.emptyList());
 		Assert.notNull(registeredClient, "registeredClient cannot be null");
 		Assert.notNull(clientAuthenticationMethod, "clientAuthenticationMethod cannot be null");
-		this.requestUri = null;
 		this.clientId = registeredClient.getClientId();
 		this.registeredClient = registeredClient;
 		this.clientAuthenticationMethod = clientAuthenticationMethod;
@@ -146,15 +118,6 @@ public class OAuth2ClientAuthenticationToken extends AbstractAuthenticationToken
 		return this.clientAuthenticationMethod;
 	}
 
-	/**
-	 * Returns URI of authenticated request. This is used to validate aud claim of JWT client assertions
-	 *
-	 * @return URI of authenticated request
-	 */
-	public String getRequestUri() {
-		return requestUri;
-	}
-
 	/**
 	 * Returns the additional parameters.
 	 *

+ 27 - 26
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/ClientSettings.java

@@ -17,8 +17,9 @@ package org.springframework.security.oauth2.server.authorization.config;
 
 import java.util.Map;
 
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
 import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
-import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
+import org.springframework.security.oauth2.jwt.Jwt;
 import org.springframework.util.Assert;
 
 /**
@@ -56,24 +57,25 @@ public final class ClientSettings extends AbstractSettings {
 	}
 
 	/**
-	 * Returns {@code URL} for the Client's JSON Web Key Set {@code (jwks_uri)}
-	 * @return {@code URL} for the Client's JSON Web Key Set {@code (jwks_uri)}
-	 * @since 0.2.1
+	 * Returns the {@code URL} for the Client's JSON Web Key Set.
+	 *
+	 * @return the {@code URL} for the Client's JSON Web Key Set
+	 * @since 0.2.2
 	 */
 	public String getJwkSetUrl() {
 		return getSetting(ConfigurationSettingNames.Client.JWK_SET_URL);
 	}
 
 	/**
-	 * Returns {@link SignatureAlgorithm JWS} algorithm that must be used for signing the JWT used to authenticate the
-	 * Client at the Token Endpoint for the {@code private_key_jwt} and {@code client_secret_jwt} authentication methods
-	 * @return {@link SignatureAlgorithm JWS} algorithm that must be used for signing the JWT used to authenticate the
-	 * 	       Client at the Token Endpoint for the {@code private_key_jwt} and {@code client_secret_jwt} authentication
-	 * 	       methods
-	 * @since 0.2.1
+	 * Returns the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT} used to authenticate
+	 * the Client at the Token Endpoint for the {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT private_key_jwt} and
+	 * {@link ClientAuthenticationMethod#CLIENT_SECRET_JWT client_secret_jwt} authentication methods.
+	 *
+	 * @return the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT} used to authenticate the Client at the Token Endpoint
+	 * @since 0.2.2
 	 */
-	public JwsAlgorithm getTokenEndpointSigningAlgorithm() {
-		return getSetting(ConfigurationSettingNames.Client.TOKEN_ENDPOINT_SIGNING_ALGORITHM);
+	public JwsAlgorithm getTokenEndpointAuthenticationSigningAlgorithm() {
+		return getSetting(ConfigurationSettingNames.Client.TOKEN_ENDPOINT_AUTHENTICATION_SIGNING_ALGORITHM);
 	}
 
 	/**
@@ -84,8 +86,7 @@ public final class ClientSettings extends AbstractSettings {
 	public static Builder builder() {
 		return new Builder()
 				.requireProofKey(false)
-				.requireAuthorizationConsent(false)
-				.tokenEndpointSigningAlgorithm(SignatureAlgorithm.RS256);
+				.requireAuthorizationConsent(false);
 	}
 
 	/**
@@ -131,28 +132,28 @@ public final class ClientSettings extends AbstractSettings {
 		}
 
 		/**
-		 * Sets {@code URL} for the Client's JSON Web Key Set
+		 * Sets the {@code URL} for the Client's JSON Web Key Set.
 		 *
-		 * @param jwkSetUrl {@code URL} for the Client's JSON Web Key Set
+		 * @param jwkSetUrl the {@code URL} for the Client's JSON Web Key Set
 		 * @return the {@link Builder} for further configuration
-		 * @since 0.2.1
+		 * @since 0.2.2
 		 */
 		public Builder jwkSetUrl(String jwkSetUrl) {
 			return setting(ConfigurationSettingNames.Client.JWK_SET_URL, jwkSetUrl);
 		}
 
 		/**
-		 * Sets {@link SignatureAlgorithm JWS} algorithm that must be used for signing the JWT used to authenticate the
-		 * Client at the Token Endpoint for the {@code private_key_jwt} and {@code client_secret_jwt} authentication methods
-		 *
-		 * @param signingAlgorithm {@link SignatureAlgorithm JWS} algorithm that must be used for signing
-		 *        the JWT used to authenticate the Client at the Token Endpoint for the {@code private_key_jwt} and
-		 *        {@code client_secret_jwt} authentication methods
+		 * Sets the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT} used to authenticate
+		 * the Client at the Token Endpoint for the {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT private_key_jwt} and
+		 * {@link ClientAuthenticationMethod#CLIENT_SECRET_JWT client_secret_jwt} authentication methods.
+
+		 * @param authenticationSigningAlgorithm the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT}
+		 *                                       used to authenticate the Client at the Token Endpoint
 		 * @return the {@link Builder} for further configuration
-		 * @since 0.2.1
+		 * @since 0.2.2
 		 */
-		public Builder tokenEndpointSigningAlgorithm(JwsAlgorithm signingAlgorithm) {
-			return setting(ConfigurationSettingNames.Client.TOKEN_ENDPOINT_SIGNING_ALGORITHM, signingAlgorithm);
+		public Builder tokenEndpointAuthenticationSigningAlgorithm(JwsAlgorithm authenticationSigningAlgorithm) {
+			return setting(ConfigurationSettingNames.Client.TOKEN_ENDPOINT_AUTHENTICATION_SIGNING_ALGORITHM, authenticationSigningAlgorithm);
 		}
 
 		/**

+ 10 - 4
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/ConfigurationSettingNames.java

@@ -15,8 +15,11 @@
  */
 package org.springframework.security.oauth2.server.authorization.config;
 
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
 import org.springframework.security.oauth2.core.oidc.OidcIdToken;
+import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
 import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
+import org.springframework.security.oauth2.jwt.Jwt;
 
 /**
  * The names for all the configuration settings.
@@ -49,15 +52,18 @@ public final class ConfigurationSettingNames {
 		public static final String REQUIRE_AUTHORIZATION_CONSENT = CLIENT_SETTINGS_NAMESPACE.concat("require-authorization-consent");
 
 		/**
-		 * {@code URL} for the Client's JSON Web Key Set {@code (jwks_uri)}
+		 * Set the {@code URL} for the Client's JSON Web Key Set.
+		 * @since 0.2.2
 		 */
 		public static final String JWK_SET_URL = CLIENT_SETTINGS_NAMESPACE.concat("jwk-set-url");
 
 		/**
-		 * {@link SignatureAlgorithm JWS} algorithm that must be used for signing the JWT used to authenticate the
-		 * Client at the Token Endpoint for the {@code private_key_jwt} and {@code client_secret_jwt} authentication methods
+		 * Set the {@link JwsAlgorithm JWS} algorithm that must be used for signing the {@link Jwt JWT}
+		 * used to authenticate the Client at the Token Endpoint for the {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT private_key_jwt} and
+		 * {@link ClientAuthenticationMethod#CLIENT_SECRET_JWT client_secret_jwt} authentication methods.
+		 * @since 0.2.2
 		 */
-		public static final String TOKEN_ENDPOINT_SIGNING_ALGORITHM = CLIENT_SETTINGS_NAMESPACE.concat("token-endpoint-signing-algorithm");
+		public static final String TOKEN_ENDPOINT_AUTHENTICATION_SIGNING_ALGORITHM = CLIENT_SETTINGS_NAMESPACE.concat("token-endpoint-authentication-signing-algorithm");
 
 		private Client() {
 		}

+ 1 - 1
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/SignatureAlgorithmMixin.java → oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/JwsAlgorithmMixin.java

@@ -30,5 +30,5 @@ import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
 @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
 @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
 		isGetterVisibility = JsonAutoDetect.Visibility.NONE)
-abstract class SignatureAlgorithmMixin {
+abstract class JwsAlgorithmMixin {
 }

+ 0 - 33
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson2/MacAlgorithmMixin.java

@@ -1,33 +0,0 @@
-/*
- * Copyright 2020-2021 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.springframework.security.oauth2.server.authorization.jackson2;
-
-import com.fasterxml.jackson.annotation.JsonAutoDetect;
-import com.fasterxml.jackson.annotation.JsonTypeInfo;
-import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
-
-/**
- * This mixin class is used to serialize/deserialize {@link MacAlgorithm}.
- *
- * @author Rafal Lewczuk
- * @since 0.2.1
- * @see MacAlgorithm
- */
-@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
-@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
-		isGetterVisibility = JsonAutoDetect.Visibility.NONE)
-public class MacAlgorithmMixin {
-}

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

@@ -37,7 +37,7 @@ import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
  * <li>{@link HashSetMixin}</li>
  * <li>{@link OAuth2AuthorizationRequestMixin}</li>
  * <li>{@link DurationMixin}</li>
- * <li>{@link SignatureAlgorithmMixin}</li>
+ * <li>{@link JwsAlgorithmMixin}</li>
  * </ul>
  *
  * If not already enabled, default typing will be automatically enabled as type info is
@@ -59,7 +59,7 @@ import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
  * @see HashSetMixin
  * @see OAuth2AuthorizationRequestMixin
  * @see DurationMixin
- * @see SignatureAlgorithmMixin
+ * @see JwsAlgorithmMixin
  */
 public class OAuth2AuthorizationServerJackson2Module extends SimpleModule {
 
@@ -76,8 +76,8 @@ public class OAuth2AuthorizationServerJackson2Module extends SimpleModule {
 		context.setMixInAnnotations(LinkedHashSet.class, HashSetMixin.class);
 		context.setMixInAnnotations(OAuth2AuthorizationRequest.class, OAuth2AuthorizationRequestMixin.class);
 		context.setMixInAnnotations(Duration.class, DurationMixin.class);
-		context.setMixInAnnotations(SignatureAlgorithm.class, SignatureAlgorithmMixin.class);
-		context.setMixInAnnotations(MacAlgorithm.class, MacAlgorithmMixin.class);
+		context.setMixInAnnotations(SignatureAlgorithm.class, JwsAlgorithmMixin.class);
+		context.setMixInAnnotations(MacAlgorithm.class, JwsAlgorithmMixin.class);
 	}
 
 }

+ 42 - 32
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProvider.java

@@ -36,12 +36,13 @@ import org.springframework.security.oauth2.core.AuthorizationGrantType;
 import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
 import org.springframework.security.oauth2.core.OAuth2AccessToken;
 import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
 import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
 import org.springframework.security.oauth2.core.OAuth2TokenType;
 import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
 import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.core.oidc.OidcClientMetadataClaimNames;
 import org.springframework.security.oauth2.core.oidc.OidcClientRegistration;
-import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
 import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
 import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
 import org.springframework.security.oauth2.jwt.JoseHeader;
@@ -58,6 +59,7 @@ import org.springframework.security.oauth2.server.authorization.config.TokenSett
 import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken;
 import org.springframework.util.Assert;
 import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
 import org.springframework.web.util.UriComponentsBuilder;
 
 /**
@@ -196,13 +198,12 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
 
 		if (!isValidRedirectUris(clientRegistrationAuthentication.getClientRegistration().getRedirectUris())) {
 			// TODO Add OAuth2ErrorCodes.INVALID_REDIRECT_URI
-			throw new OAuth2AuthenticationException("invalid_redirect_uri");
+			throwInvalidClientRegistration("invalid_redirect_uri", OidcClientMetadataClaimNames.REDIRECT_URIS);
 		}
 
-		if (!isValidJwtClientAuthenticationMetadata(clientRegistrationAuthentication.getClientRegistration())) {
+		if (!isValidTokenEndpointAuthenticationMethod(clientRegistrationAuthentication.getClientRegistration())) {
 			// TODO Add OAuth2ErrorCodes.INVALID_CLIENT_METADATA
-			// TODO populate "error_description"
-			throw new OAuth2AuthenticationException("invalid_client_metadata");
+			throwInvalidClientRegistration("invalid_client_metadata", OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD);
 		}
 
 		RegisteredClient registeredClient = createClient(clientRegistrationAuthentication.getClientRegistration());
@@ -298,8 +299,8 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
 			builder.jwkSetUrl(clientSettings.getJwkSetUrl());
 		}
 
-		if (clientSettings.getTokenEndpointSigningAlgorithm() != null) {
-			builder.tokenEndpointAuthenticationSigningAlgorithm(clientSettings.getTokenEndpointSigningAlgorithm().getName());
+		if (clientSettings.getTokenEndpointAuthenticationSigningAlgorithm() != null) {
+			builder.tokenEndpointAuthenticationSigningAlgorithm(clientSettings.getTokenEndpointAuthenticationSigningAlgorithm().getName());
 		}
 
 		return builder;
@@ -347,29 +348,28 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
 		return true;
 	}
 
-	private static boolean isValidJwtClientAuthenticationMetadata(OidcClientRegistration clientRegistration) {
+	private static boolean isValidTokenEndpointAuthenticationMethod(OidcClientRegistration clientRegistration) {
 		String authenticationMethod = clientRegistration.getTokenEndpointAuthenticationMethod();
-		String signingAlgorithm = clientRegistration.getTokenEndpointAuthenticationSigningAlgorithm();
+		String authenticationSigningAlgorithm = clientRegistration.getTokenEndpointAuthenticationSigningAlgorithm();
 
-		if ("none".equals(signingAlgorithm)) {
-			return false;
+		if (!ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue().equals(authenticationMethod) &&
+				!ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue().equals(authenticationMethod)) {
+			return !StringUtils.hasText(authenticationSigningAlgorithm);
 		}
 
-		if (ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue().equals(authenticationMethod)) {
-			return signingAlgorithm == null || MacAlgorithm.from(signingAlgorithm) != null;
+		if ("none".equals(authenticationSigningAlgorithm)) {
+			return false;
 		}
 
 		if (ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue().equals(authenticationMethod)) {
-			try {
-				return clientRegistration.getJwkSetUrl() != null && (signingAlgorithm == null
-						|| SignatureAlgorithm.from(signingAlgorithm) != null);
-			} catch (IllegalArgumentException e) {
-				return false;
-			}
+			return clientRegistration.getJwkSetUrl() != null &&
+					(!StringUtils.hasText(authenticationSigningAlgorithm) ||
+							SignatureAlgorithm.from(authenticationSigningAlgorithm) != null);
+		} else {
+			// client_secret_jwt
+			return !StringUtils.hasText(authenticationSigningAlgorithm) ||
+					MacAlgorithm.from(authenticationSigningAlgorithm) != null;
 		}
-
-		// TODO return false if token_endpoint_auth_signing_alg or jwks_uri exists but authentication method is not client_secret_jwt nor private_key_jwt ?
-		return true;
 	}
 
 	private static RegisteredClient createClient(OidcClientRegistration clientRegistration) {
@@ -387,7 +387,7 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
 		} else if (ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue().equals(clientRegistration.getTokenEndpointAuthenticationMethod())) {
 			builder.clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT);
 		} else {
-				builder.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
+			builder.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
 		}
 
 		builder.redirectUris(redirectUris ->
@@ -414,16 +414,18 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
 				.requireProofKey(true)
 				.requireAuthorizationConsent(true);
 
-		String signatureAlgorithm = clientRegistration.getTokenEndpointAuthenticationSigningAlgorithm();
-
 		if (ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue().equals(clientRegistration.getTokenEndpointAuthenticationMethod())) {
-			JwsAlgorithm macAlgorithm = signatureAlgorithm != null ? MacAlgorithm.from(signatureAlgorithm) : MacAlgorithm.HS256;
-			clientSettingsBuilder.tokenEndpointSigningAlgorithm(macAlgorithm);
-		}
-
-		if (ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue().equals(clientRegistration.getTokenEndpointAuthenticationMethod())) {
-			JwsAlgorithm jwsAlgorithm = signatureAlgorithm != null ? SignatureAlgorithm.from(signatureAlgorithm) : SignatureAlgorithm.RS256;
-			clientSettingsBuilder.tokenEndpointSigningAlgorithm(jwsAlgorithm);
+			MacAlgorithm macAlgorithm = MacAlgorithm.from(clientRegistration.getTokenEndpointAuthenticationSigningAlgorithm());
+			if (macAlgorithm == null) {
+				macAlgorithm = MacAlgorithm.HS256;
+			}
+			clientSettingsBuilder.tokenEndpointAuthenticationSigningAlgorithm(macAlgorithm);
+		} else if (ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue().equals(clientRegistration.getTokenEndpointAuthenticationMethod())) {
+			SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.from(clientRegistration.getTokenEndpointAuthenticationSigningAlgorithm());
+			if (signatureAlgorithm == null) {
+				signatureAlgorithm = SignatureAlgorithm.RS256;
+			}
+			clientSettingsBuilder.tokenEndpointAuthenticationSigningAlgorithm(signatureAlgorithm);
 			clientSettingsBuilder.jwkSetUrl(clientRegistration.getJwkSetUrl().toString());
 		}
 
@@ -437,4 +439,12 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
 		// @formatter:on
 	}
 
+	private static void throwInvalidClientRegistration(String errorCode, String fieldName) {
+		OAuth2Error error = new OAuth2Error(
+				errorCode,
+				"Invalid Client Registration: " + fieldName,
+				"https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationError");
+		throw new OAuth2AuthenticationException(error);
+	}
+
 }

+ 19 - 10
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java

@@ -15,6 +15,15 @@
  */
 package org.springframework.security.oauth2.server.authorization.oidc.web;
 
+import java.io.IOException;
+import java.util.List;
+import java.util.function.Consumer;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
 import org.springframework.http.HttpMethod;
 import org.springframework.http.MediaType;
 import org.springframework.http.server.ServletServerHttpResponse;
@@ -32,12 +41,6 @@ import org.springframework.util.Assert;
 import org.springframework.web.filter.OncePerRequestFilter;
 import org.springframework.web.util.UriComponentsBuilder;
 
-import javax.servlet.FilterChain;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import java.io.IOException;
-
 /**
  * A {@code Filter} that processes OpenID Provider Configuration Requests.
  *
@@ -80,11 +83,8 @@ public final class OidcProviderConfigurationEndpointFilter extends OncePerReques
 				.issuer(this.providerSettings.getIssuer())
 				.authorizationEndpoint(asUrl(this.providerSettings.getIssuer(), this.providerSettings.getAuthorizationEndpoint()))
 				.tokenEndpoint(asUrl(this.providerSettings.getIssuer(), this.providerSettings.getTokenEndpoint()))
-				.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue())
-				.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue())
+				.tokenEndpointAuthenticationMethods(clientAuthenticationMethods())
 				.jwkSetUrl(asUrl(this.providerSettings.getIssuer(), this.providerSettings.getJwkSetEndpoint()))
-				.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue())
-				.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue())
 				.responseType(OAuth2AuthorizationResponseType.CODE.getValue())
 				.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
 				.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
@@ -99,6 +99,15 @@ public final class OidcProviderConfigurationEndpointFilter extends OncePerReques
 				providerConfiguration, MediaType.APPLICATION_JSON, httpResponse);
 	}
 
+	private static Consumer<List<String>> clientAuthenticationMethods() {
+		return (authenticationMethods) -> {
+			authenticationMethods.add(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue());
+			authenticationMethods.add(ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue());
+			authenticationMethods.add(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue());
+			authenticationMethods.add(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue());
+		};
+	}
+
 	private static String asUrl(String issuer, String endpoint) {
 		return UriComponentsBuilder.fromUriString(issuer).path(endpoint).build().toUriString();
 	}

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

@@ -42,8 +42,8 @@ import org.springframework.security.oauth2.server.authorization.authentication.O
 import org.springframework.security.oauth2.server.authorization.web.authentication.ClientSecretBasicAuthenticationConverter;
 import org.springframework.security.oauth2.server.authorization.web.authentication.ClientSecretPostAuthenticationConverter;
 import org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter;
-import org.springframework.security.oauth2.server.authorization.web.authentication.PublicClientAuthenticationConverter;
 import org.springframework.security.oauth2.server.authorization.web.authentication.JwtClientAssertionAuthenticationConverter;
+import org.springframework.security.oauth2.server.authorization.web.authentication.PublicClientAuthenticationConverter;
 import org.springframework.security.web.authentication.AuthenticationConverter;
 import org.springframework.security.web.authentication.AuthenticationFailureHandler;
 import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
@@ -87,9 +87,9 @@ public final class OAuth2ClientAuthenticationFilter extends OncePerRequestFilter
 		this.requestMatcher = requestMatcher;
 		this.authenticationConverter = new DelegatingAuthenticationConverter(
 				Arrays.asList(
+						new JwtClientAssertionAuthenticationConverter(),
 						new ClientSecretBasicAuthenticationConverter(),
 						new ClientSecretPostAuthenticationConverter(),
-						new JwtClientAssertionAuthenticationConverter(),
 						new PublicClientAuthenticationConverter()));
 	}
 

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

@@ -89,6 +89,7 @@ public final class ClientSecretBasicAuthenticationConverter implements Authentic
 		}
 
 		return new OAuth2ClientAuthenticationToken(clientID, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, clientSecret,
-				OAuth2EndpointUtils.extractAdditionalParameters(request));
+				OAuth2EndpointUtils.getParametersIfMatchesAuthorizationCodeGrantRequest(request));
 	}
+
 }

+ 4 - 2
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/ClientSecretPostAuthenticationConverter.java

@@ -69,10 +69,12 @@ public final class ClientSecretPostAuthenticationConverter implements Authentica
 			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
 		}
 
-		Map<String, Object> additionalParameters = OAuth2EndpointUtils.extractAdditionalParameters(request,
-				OAuth2ParameterNames.CLIENT_ID, OAuth2ParameterNames.CLIENT_SECRET);
+		Map<String, Object> additionalParameters = OAuth2EndpointUtils.getParametersIfMatchesAuthorizationCodeGrantRequest(request,
+				OAuth2ParameterNames.CLIENT_ID,
+				OAuth2ParameterNames.CLIENT_SECRET);
 
 		return new OAuth2ClientAuthenticationToken(clientId, ClientAuthenticationMethod.CLIENT_SECRET_POST, clientSecret,
 				additionalParameters);
 	}
+
 }

+ 27 - 30
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/JwtClientAssertionAuthenticationConverter.java

@@ -13,9 +13,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package org.springframework.security.oauth2.server.authorization.web.authentication;
 
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
 import org.springframework.lang.Nullable;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
@@ -28,65 +31,59 @@ 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.Map;
-
 /**
- * Attempts to extract client assertion credentials from {@link HttpServletRequest}
+ * Attempts to extract a JWT client assertion credential from {@link HttpServletRequest}
  * and then converts to an {@link OAuth2ClientAuthenticationToken} used for authenticating the client.
  *
  * @author Rafal Lewczuk
- * @since 0.2.1
+ * @since 0.2.2
  * @see AuthenticationConverter
  * @see OAuth2ClientAuthenticationToken
  * @see OAuth2ClientAuthenticationFilter
  */
 public final class JwtClientAssertionAuthenticationConverter implements AuthenticationConverter {
-
-	private static final ClientAuthenticationMethod JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD
-			= new ClientAuthenticationMethod("urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
+	private static final ClientAuthenticationMethod JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD =
+			new ClientAuthenticationMethod("urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
 
 	@Nullable
 	@Override
 	public Authentication convert(HttpServletRequest request) {
-		MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
-
-		// client_assertion_type (REQUIRED), client_assertion (REQUIRED)
-		String clientJwtAssertionType = parameters.getFirst(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE);
-		String clientJwtAssertion = parameters.getFirst(OAuth2ParameterNames.CLIENT_ASSERTION);
-
-		if (!StringUtils.hasText(clientJwtAssertionType) || !StringUtils.hasText(clientJwtAssertion)) {
+		if (request.getParameter(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE) == null ||
+				request.getParameter(OAuth2ParameterNames.CLIENT_ASSERTION) == null) {
 			return null;
 		}
 
+		MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
+
+		// client_assertion_type (REQUIRED)
+		String clientAssertionType = parameters.getFirst(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE);
 		if (parameters.get(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE).size() != 1) {
 			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
 		}
-
-		if (parameters.get(OAuth2ParameterNames.CLIENT_ASSERTION).size() != 1) {
-			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
+		if (!JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD.getValue().equals(clientAssertionType)) {
+			return null;
 		}
 
-		if (!JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD.getValue().equals(clientJwtAssertionType)) {
+		// client_assertion (REQUIRED)
+		String jwtAssertion = parameters.getFirst(OAuth2ParameterNames.CLIENT_ASSERTION);
+		if (parameters.get(OAuth2ParameterNames.CLIENT_ASSERTION).size() != 1) {
 			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
 		}
 
 		// client_id (OPTIONAL as per specification but REQUIRED by this implementation)
 		String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID);
-		if (!StringUtils.hasText(clientId)) {
+		if (!StringUtils.hasText(clientId) ||
+				parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {
 			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
 		}
 
-		if (parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {
-			throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
-		}
-
-		Map<String, Object> additionalParameters = OAuth2EndpointUtils.extractAdditionalParameters(request,
-				OAuth2ParameterNames.CLIENT_ID,
+		Map<String, Object> additionalParameters = OAuth2EndpointUtils.getParametersIfMatchesAuthorizationCodeGrantRequest(request,
 				OAuth2ParameterNames.CLIENT_ASSERTION_TYPE,
-				OAuth2ParameterNames.CLIENT_ASSERTION);
+				OAuth2ParameterNames.CLIENT_ASSERTION,
+				OAuth2ParameterNames.CLIENT_ID);
 
-		return new OAuth2ClientAuthenticationToken(request.getRequestURI(), clientId,
-				JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD, clientJwtAssertion, additionalParameters);
+		return new OAuth2ClientAuthenticationToken(clientId, JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD,
+				jwtAssertion, additionalParameters);
 	}
+
 }

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

@@ -54,6 +54,17 @@ final class OAuth2EndpointUtils {
 		return parameters;
 	}
 
+	static Map<String, Object> getParametersIfMatchesAuthorizationCodeGrantRequest(HttpServletRequest request, String... exclusions) {
+		if (!matchesAuthorizationCodeGrantRequest(request)) {
+			return Collections.emptyMap();
+		}
+		Map<String, Object> parameters = new HashMap<>(getParameters(request).toSingleValueMap());
+		for (String exclusion : exclusions) {
+			parameters.remove(exclusion);
+		}
+		return parameters;
+	}
+
 	static boolean matchesAuthorizationCodeGrantRequest(HttpServletRequest request) {
 		return AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(
 				request.getParameter(OAuth2ParameterNames.GRANT_TYPE)) &&
@@ -70,14 +81,4 @@ final class OAuth2EndpointUtils {
 		throw new OAuth2AuthenticationException(error);
 	}
 
-	static Map<String, Object> extractAdditionalParameters(HttpServletRequest request, String...exclusions) {
-		Map<String, Object> additionalParameters = Collections.emptyMap();
-		if (OAuth2EndpointUtils.matchesAuthorizationCodeGrantRequest(request)) {
-			additionalParameters = new HashMap<>(OAuth2EndpointUtils.getParameters(request).toSingleValueMap());
-			for (String exclusion : exclusions) {
-				additionalParameters.remove(exclusion);
-			}
-		}
-		return additionalParameters;
-	}
 }

+ 76 - 195
oauth2-authorization-server/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcClientRegistrationTests.java

@@ -15,27 +15,22 @@
  */
 package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
 
-import com.nimbusds.jose.JOSEException;
-import com.nimbusds.jose.JWSAlgorithm;
-import com.nimbusds.jose.JWSHeader;
-import com.nimbusds.jose.JWSSigner;
-import com.nimbusds.jose.crypto.MACSigner;
-import com.nimbusds.jose.crypto.RSASSASigner;
-import com.nimbusds.jose.jwk.JWK;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Collections;
+
 import com.nimbusds.jose.jwk.JWKSet;
-import com.nimbusds.jose.jwk.KeyUse;
-import com.nimbusds.jose.jwk.RSAKey;
 import com.nimbusds.jose.jwk.source.JWKSource;
 import com.nimbusds.jose.proc.SecurityContext;
-import com.nimbusds.jwt.JWTClaimsSet;
-import com.nimbusds.jwt.SignedJWT;
 import okhttp3.mockwebserver.MockResponse;
 import okhttp3.mockwebserver.MockWebServer;
 import org.junit.After;
 import org.junit.AfterClass;
+import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Rule;
 import org.junit.Test;
+
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
 import org.springframework.http.HttpHeaders;
@@ -56,13 +51,11 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
 import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
 import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
 import org.springframework.security.config.test.SpringTestRule;
-import org.springframework.security.config.util.ValueCaptureMatcher;
 import org.springframework.security.crypto.password.NoOpPasswordEncoder;
 import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.security.oauth2.core.AuthorizationGrantType;
 import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
 import org.springframework.security.oauth2.core.OAuth2AccessToken;
-import org.springframework.security.oauth2.core.OAuth2TokenType;
 import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
 import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
 import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
@@ -71,7 +64,12 @@ import org.springframework.security.oauth2.core.oidc.OidcClientRegistration;
 import org.springframework.security.oauth2.core.oidc.http.converter.OidcClientRegistrationHttpMessageConverter;
 import org.springframework.security.oauth2.jose.TestJwks;
 import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
+import org.springframework.security.oauth2.jwt.JoseHeader;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.jwt.JwtClaimsSet;
 import org.springframework.security.oauth2.jwt.JwtDecoder;
+import org.springframework.security.oauth2.jwt.JwtEncoder;
+import org.springframework.security.oauth2.jwt.NimbusJwsEncoder;
 import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
 import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
 import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
@@ -79,20 +77,13 @@ import org.springframework.security.oauth2.server.authorization.client.JdbcRegis
 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 org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
 import org.springframework.security.web.SecurityFilterChain;
 import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.test.web.servlet.MockMvc;
 import org.springframework.test.web.servlet.MvcResult;
-
-import java.net.URLEncoder;
-import java.nio.charset.StandardCharsets;
-import java.security.KeyPair;
-import java.security.KeyPairGenerator;
-import java.security.interfaces.RSAPublicKey;
-import java.util.Base64;
-import java.util.Date;
-import java.util.UUID;
+import org.springframework.web.util.UriComponentsBuilder;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.hamcrest.CoreMatchers.containsString;
@@ -109,18 +100,16 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
  * @author Joe Grandja
  */
 public class OidcClientRegistrationTests {
-	private static final String DEFAULT_ISSUER = "https://auth-server:9000";
 	private static final String DEFAULT_TOKEN_ENDPOINT_URI = "/oauth2/token";
-	private static final String DEFAULT_INTROSPECTION_ENDPOINT_URI = "/oauth2/introspect";
-	private static final String DEFAULT_REVOCATION_ENDPOINT_URI = "/oauth2/revoke";
 	private static final String DEFAULT_OIDC_CLIENT_REGISTRATION_ENDPOINT_URI = "/connect/register";
-	private static final String JWT_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
 	private static final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter =
 			new OAuth2AccessTokenResponseHttpMessageConverter();
 	private static final HttpMessageConverter<OidcClientRegistration> clientRegistrationHttpMessageConverter =
 			new OidcClientRegistrationHttpMessageConverter();
 	private static EmbeddedDatabase db;
 	private static JWKSource<SecurityContext> jwkSource;
+	private static JWKSet clientJwkSet;
+	private static JwtEncoder jwtClientAssertionEncoder;
 
 	@Rule
 	public final SpringTestRule spring = new SpringTestRule();
@@ -134,10 +123,19 @@ public class OidcClientRegistrationTests {
 	@Autowired
 	private RegisteredClientRepository registeredClientRepository;
 
+	@Autowired
+	private ProviderSettings providerSettings;
+
+	private MockWebServer server;
+	private String clientJwkSetUrl;
+
+
 	@BeforeClass
 	public static void init() {
 		JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
 		jwkSource = (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
+		clientJwkSet = new JWKSet(TestJwks.generateRsaJwk().build());
+		jwtClientAssertionEncoder = new NimbusJwsEncoder((jwkSelector, securityContext) -> jwkSelector.select(clientJwkSet));
 		db = new EmbeddedDatabaseBuilder()
 				.generateUniqueName(true)
 				.setType(EmbeddedDatabaseType.HSQL)
@@ -147,8 +145,22 @@ public class OidcClientRegistrationTests {
 				.build();
 	}
 
+	@Before
+	public void setup() throws Exception {
+		this.server = new MockWebServer();
+		this.server.start();
+		this.clientJwkSetUrl = this.server.url("/jwks").toString();
+		// @formatter:off
+		MockResponse response = new MockResponse()
+				.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
+				.setBody(clientJwkSet.toString());
+		// @formatter:on
+		this.server.enqueue(response);
+	}
+
 	@After
-	public void tearDown() {
+	public void tearDown() throws Exception {
+		this.server.shutdown();
 		jdbcOperations.update("truncate table oauth2_authorization");
 		jdbcOperations.update("truncate table oauth2_registered_client");
 	}
@@ -247,153 +259,40 @@ public class OidcClientRegistrationTests {
 		assertThat(clientConfigurationResponse.getRegistrationAccessToken()).isNull();
 	}
 
-	@Test
-	public void whenClientRegisterationWithClientSecretJwtAuthenticationThenJwtClientAuthenticationSuccess() throws Exception {
-		this.spring.register(AuthorizationServerConfiguration.class).autowire();
-
-		// @formatter:off
-		OidcClientRegistration clientRegistration = OidcClientRegistration.builder()
-				.clientName("client-name")
-				.redirectUri("https://client.example.com")
-				.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
-				.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
-				.tokenEndpointAuthenticationSigningAlgorithm("HS256")
-				.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue())
-				.scope("scope1")
-				.scope("scope2")
-				.build();
-		// @formatter:on
-
-		OidcClientRegistration clientRegistrationResponse = registerClient(clientRegistration);
-		ValueCaptureMatcher<String> accessTokenCapture = new ValueCaptureMatcher<>();
-
-		// token creation with JWT assertion
-		String clientJwtAssertion = clientSecretJwtAssertion(clientRegistrationResponse, DEFAULT_TOKEN_ENDPOINT_URI);
-		this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
-				.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
-				.param(OAuth2ParameterNames.SCOPE, "scope1")
-				.param(OAuth2ParameterNames.CLIENT_ID, clientRegistrationResponse.getClientId())
-				.param(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, JWT_ASSERTION_TYPE)
-				.param(OAuth2ParameterNames.CLIENT_ASSERTION, clientJwtAssertion))
-				.andExpect(status().isOk())
-				.andExpect(jsonPath("$.access_token").isNotEmpty())
-				.andExpect(jsonPath("$.access_token").value(accessTokenCapture))
-				.andExpect(jsonPath("$.scope").value("scope1"));
-
-		String accessToken = accessTokenCapture.lastValue();
-
-		// token introspection with JWT assertion
-		clientJwtAssertion = clientSecretJwtAssertion(clientRegistrationResponse, DEFAULT_INTROSPECTION_ENDPOINT_URI);
-		this.mvc.perform(post(DEFAULT_INTROSPECTION_ENDPOINT_URI)
-				.param(OAuth2ParameterNames.CLIENT_ID, clientRegistrationResponse.getClientId())
-				.param(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, JWT_ASSERTION_TYPE)
-				.param(OAuth2ParameterNames.CLIENT_ASSERTION, clientJwtAssertion)
-				.param(OAuth2ParameterNames.TOKEN, accessToken)
-				.param(OAuth2ParameterNames.TOKEN_TYPE_HINT, OAuth2TokenType.ACCESS_TOKEN.getValue()))
-				.andExpect(status().isOk());
-
-		// token revocation with JWT assertion
-		clientJwtAssertion = clientSecretJwtAssertion(clientRegistrationResponse, DEFAULT_REVOCATION_ENDPOINT_URI);
-		this.mvc.perform(post(DEFAULT_REVOCATION_ENDPOINT_URI)
-				.param(OAuth2ParameterNames.CLIENT_ID, clientRegistrationResponse.getClientId())
-				.param(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, JWT_ASSERTION_TYPE)
-				.param(OAuth2ParameterNames.CLIENT_ASSERTION, clientJwtAssertion)
-				.param(OAuth2ParameterNames.TOKEN, accessToken)
-				.param(OAuth2ParameterNames.TOKEN_TYPE_HINT, OAuth2TokenType.ACCESS_TOKEN.getValue()))
-				.andExpect(status().isOk());
-	}
-
-	@Test
-	public void whenClientRegistrationWithPrivateKeyJwtAuthenticationThenJwtClientAuthenticationSuccess() throws Exception {
-		this.spring.register(AuthorizationServerConfiguration.class).autowire();
-
-		KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
-		gen.initialize(2048);
-		KeyPair keyPair = gen.generateKeyPair();
-
-		JWK jwk = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic())
-				.keyUse(KeyUse.SIGNATURE)
-				.keyID(UUID.randomUUID().toString())
-				.build();
-
-		String jwks = "{\"keys\":[" + jwk.toJSONString() + "]}";
-
-		try (MockWebServer server = new MockWebServer()) {
-			String jwkSetUrl = server.url("/.well-known/jwks.json").toString();
-
-			// @formatter:off
-			OidcClientRegistration clientRegistration = OidcClientRegistration.builder()
-					.clientName("client-name")
-					.redirectUri("https://client.example.com")
-					.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
-					.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
-					.tokenEndpointAuthenticationSigningAlgorithm("RS256")
-					.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue())
-					.jwkSetUrl(jwkSetUrl)
-					.scope("scope1")
-					.scope("scope2")
-					.build();
-			// @formatter:on
-
-			OidcClientRegistration clientRegistrationResponse = registerClient(clientRegistration);
-			ValueCaptureMatcher<String> accessTokenCapture = new ValueCaptureMatcher<>();
-
-			// token creation with JWT assertion
-			String clientJwtAssertion = privateKeyJwtAssertion(keyPair, clientRegistrationResponse, DEFAULT_TOKEN_ENDPOINT_URI);
-			server.enqueue(new MockResponse().setBody(jwks));
-			this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
-					.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
-					.param(OAuth2ParameterNames.SCOPE, "scope1")
-					.param(OAuth2ParameterNames.CLIENT_ID, clientRegistrationResponse.getClientId())
-					.param(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, JWT_ASSERTION_TYPE)
-					.param(OAuth2ParameterNames.CLIENT_ASSERTION, clientJwtAssertion))
-					.andExpect(status().isOk())
-					.andExpect(jsonPath("$.access_token").isNotEmpty())
-					.andExpect(jsonPath("$.access_token").value(accessTokenCapture))
-					.andExpect(jsonPath("$.scope").value("scope1"));
-
-			String accessToken = accessTokenCapture.lastValue();
-
-				// token introspection with JWT assertion
-				clientJwtAssertion = privateKeyJwtAssertion(keyPair, clientRegistrationResponse, DEFAULT_INTROSPECTION_ENDPOINT_URI);
-				server.enqueue(new MockResponse().setBody(jwks));
-				this.mvc.perform(post(DEFAULT_INTROSPECTION_ENDPOINT_URI)
-						.param(OAuth2ParameterNames.CLIENT_ID, clientRegistrationResponse.getClientId())
-						.param(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, JWT_ASSERTION_TYPE)
-						.param(OAuth2ParameterNames.CLIENT_ASSERTION, clientJwtAssertion)
-						.param(OAuth2ParameterNames.TOKEN, accessToken)
-						.param(OAuth2ParameterNames.TOKEN_TYPE_HINT, OAuth2TokenType.ACCESS_TOKEN.getValue()))
-						.andExpect(status().isOk());
-
-			// token revocation with JWT assertion
-			clientJwtAssertion = privateKeyJwtAssertion(keyPair, clientRegistrationResponse, DEFAULT_REVOCATION_ENDPOINT_URI);
-			server.enqueue(new MockResponse().setBody(jwks));
-			this.mvc.perform(post(DEFAULT_REVOCATION_ENDPOINT_URI)
-					.param(OAuth2ParameterNames.CLIENT_ID, clientRegistrationResponse.getClientId())
-					.param(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, JWT_ASSERTION_TYPE)
-					.param(OAuth2ParameterNames.CLIENT_ASSERTION, clientJwtAssertion)
-					.param(OAuth2ParameterNames.TOKEN, accessToken)
-					.param(OAuth2ParameterNames.TOKEN_TYPE_HINT, OAuth2TokenType.ACCESS_TOKEN.getValue()))
-					.andExpect(status().isOk());
-
-			server.shutdown();
-		}
-	}
-
 	private OidcClientRegistration registerClient(OidcClientRegistration clientRegistration) throws Exception {
 		// ***** (1) Obtain the "initial" access token used for registering the client
 
 		String clientRegistrationScope = "client.create";
-		RegisteredClient clientRegistrar = TestRegisteredClients.registeredClient2()
+		// @formatter:off
+		RegisteredClient clientRegistrar = RegisteredClient.withId("client-registrar-1")
+				.clientId("client-registrar-1")
+				.clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT)
+				.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
 				.scope(clientRegistrationScope)
+				.clientSettings(
+						ClientSettings.builder()
+								.jwkSetUrl(this.clientJwkSetUrl)
+								.tokenEndpointAuthenticationSigningAlgorithm(SignatureAlgorithm.RS256)
+								.build()
+				)
 				.build();
+		// @formatter:on
 		this.registeredClientRepository.save(clientRegistrar);
 
+		// @formatter:off
+		JoseHeader joseHeader = JoseHeader.withAlgorithm(SignatureAlgorithm.RS256)
+				.build();
+		JwtClaimsSet jwtClaimsSet = jwtClientAssertionClaims(clientRegistrar)
+				.build();
+		// @formatter:on
+		Jwt jwtAssertion = jwtClientAssertionEncoder.encode(joseHeader, jwtClaimsSet);
+
 		MvcResult mvcResult = this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
 				.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
 				.param(OAuth2ParameterNames.SCOPE, clientRegistrationScope)
-				.header(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth(
-						clientRegistrar.getClientId(), clientRegistrar.getClientSecret())))
+				.param(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
+				.param(OAuth2ParameterNames.CLIENT_ASSERTION, jwtAssertion.getTokenValue())
+				.param(OAuth2ParameterNames.CLIENT_ID, clientRegistrar.getClientId()))
 				.andExpect(status().isOk())
 				.andExpect(jsonPath("$.access_token").isNotEmpty())
 				.andExpect(jsonPath("$.scope").value(clientRegistrationScope))
@@ -419,12 +318,19 @@ public class OidcClientRegistrationTests {
 		return readClientRegistrationResponse(mvcResult.getResponse());
 	}
 
-	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());
-		String credentialsString = clientId + ":" + secret;
-		byte[] encodedBytes = Base64.getEncoder().encode(credentialsString.getBytes(StandardCharsets.UTF_8));
-		return new String(encodedBytes, StandardCharsets.UTF_8);
+	private JwtClaimsSet.Builder jwtClientAssertionClaims(RegisteredClient registeredClient) {
+		Instant issuedAt = Instant.now();
+		Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS);
+		return JwtClaimsSet.builder()
+				.issuer(registeredClient.getClientId())
+				.subject(registeredClient.getClientId())
+				.audience(Collections.singletonList(asUrl(this.providerSettings.getIssuer(), this.providerSettings.getTokenEndpoint())))
+				.issuedAt(issuedAt)
+				.expiresAt(expiresAt);
+	}
+
+	private static String asUrl(String uri, String path) {
+		return UriComponentsBuilder.fromUriString(uri).path(path).build().toUriString();
 	}
 
 	private static OAuth2AccessTokenResponse readAccessTokenResponse(MockHttpServletResponse response) throws Exception {
@@ -445,31 +351,6 @@ public class OidcClientRegistrationTests {
 		return clientRegistrationHttpMessageConverter.read(OidcClientRegistration.class, httpResponse);
 	}
 
-	private JWTClaimsSet jwtClientAuthenticationClaims(OidcClientRegistration clientRegistration, String endpointUri) {
-		return  new JWTClaimsSet.Builder()
-				.subject(clientRegistration.getClientId())
-				.issuer(clientRegistration.getClientId())
-				.expirationTime(new Date(new Date().getTime() + 60000))
-				.audience(DEFAULT_ISSUER + endpointUri)
-				.build();
-	}
-
-	private String clientSecretJwtAssertion(OidcClientRegistration clientRegistration, String endpointUri) throws JOSEException {
-		JWTClaimsSet claimsSet = jwtClientAuthenticationClaims(clientRegistration, endpointUri);
-		SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), claimsSet);
-		JWSSigner signer = new MACSigner(clientRegistration.getClientSecret().getBytes(StandardCharsets.UTF_8));
-		signedJWT.sign(signer);
-		return signedJWT.serialize();
-	}
-
-	private String privateKeyJwtAssertion(KeyPair keyPair, OidcClientRegistration clientRegistration, String endpointUri) throws JOSEException {
-		JWTClaimsSet claimsSet = jwtClientAuthenticationClaims(clientRegistration, endpointUri);
-		SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), claimsSet);
-		JWSSigner signer = new RSASSASigner(keyPair.getPrivate());
-		signedJWT.sign(signer);
-		return signedJWT.serialize();
-	}
-
 	@EnableWebSecurity
 	static class AuthorizationServerConfiguration {
 
@@ -529,7 +410,7 @@ public class OidcClientRegistrationTests {
 		@Bean
 		ProviderSettings providerSettings() {
 			return ProviderSettings.builder()
-					.issuer(DEFAULT_ISSUER)
+					.issuer("https://auth-server:9000")
 					.build();
 		}
 

+ 0 - 65
oauth2-authorization-server/src/test/java/org/springframework/security/config/util/ValueCaptureMatcher.java

@@ -1,65 +0,0 @@
-/*
- * Copyright 2020-2021 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.springframework.security.config.util;
-
-import org.assertj.core.util.Throwables;
-import org.hamcrest.BaseMatcher;
-import org.hamcrest.Description;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Hamcrest matcher that records matched values
- *
- * @author Rafal Lewczuk
- * @since 0.2.1
- * @param <T>
- */
-public class ValueCaptureMatcher<T> extends BaseMatcher<T> {
-
-	private ClassCastException castException;
-	private List<T> values = new ArrayList<>();
-
-	public T lastValue() {
-		return values.isEmpty() ? null : values.get(values.size()-1);
-	}
-
-	public List<T> getValues() {
-		return values;
-	}
-
-	@Override
-	public boolean matches(Object item) {
-		try {
-			values.add((T) item);
-		} catch (ClassCastException e) {
-			castException = e;
-			return false;
-		}
-		return true;
-	}
-
-	@Override
-	public void describeTo(Description description) {
-		if (castException != null) {
-			description.appendText("ClassCastException with message: ");
-			description.appendText(castException.getMessage());
-			description.appendText(String.format("%n%nStacktrace was: "));
-			description.appendText(Throwables.getStackTrace(castException));
-		}
-	}
-}

+ 22 - 11
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/OidcClientRegistrationTests.java

@@ -15,6 +15,7 @@
  */
 package org.springframework.security.oauth2.core.oidc;
 
+import java.net.URL;
 import java.time.Instant;
 import java.time.temporal.ChronoUnit;
 import java.util.Arrays;
@@ -46,8 +47,7 @@ public class OidcClientRegistrationTests {
 	// @formatter:on
 
 	@Test
-	public void buildWhenAllClaimsProvidedThenCreated() {
-
+	public void buildWhenAllClaimsProvidedThenCreated() throws Exception {
 		// @formatter:off
 		Instant clientIdIssuedAt = Instant.now();
 		Instant clientSecretExpiresAt = clientIdIssuedAt.plus(30, ChronoUnit.DAYS);
@@ -58,14 +58,14 @@ public class OidcClientRegistrationTests {
 				.clientSecretExpiresAt(clientSecretExpiresAt)
 				.clientName("client-name")
 				.redirectUri("https://client.example.com")
-				.jwkSetUrl("https://client.example.com/jwks")
-				.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue())
+				.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue())
 				.tokenEndpointAuthenticationSigningAlgorithm(MacAlgorithm.HS256.getName())
 				.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
 				.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
 				.responseType(OAuth2AuthorizationResponseType.CODE.getValue())
 				.scope("scope1")
 				.scope("scope2")
+				.jwkSetUrl("https://client.example.com/jwks")
 				.idTokenSignedResponseAlgorithm(SignatureAlgorithm.RS256.getName())
 				.registrationAccessToken("registration-access-token")
 				.registrationClientUrl("https://auth-server.com/connect/register?client_id=1")
@@ -78,12 +78,13 @@ public class OidcClientRegistrationTests {
 		assertThat(clientRegistration.getClientSecret()).isEqualTo("client-secret");
 		assertThat(clientRegistration.getClientSecretExpiresAt()).isEqualTo(clientSecretExpiresAt);
 		assertThat(clientRegistration.getClientName()).isEqualTo("client-name");
-		assertThat(clientRegistration.getJwkSetUrl().toString()).isEqualTo("https://client.example.com/jwks");
 		assertThat(clientRegistration.getRedirectUris()).containsOnly("https://client.example.com");
-		assertThat(clientRegistration.getTokenEndpointAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue());
+		assertThat(clientRegistration.getTokenEndpointAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue());
+		assertThat(clientRegistration.getTokenEndpointAuthenticationSigningAlgorithm()).isEqualTo(MacAlgorithm.HS256.getName());
 		assertThat(clientRegistration.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "client_credentials");
 		assertThat(clientRegistration.getResponseTypes()).containsOnly("code");
 		assertThat(clientRegistration.getScopes()).containsExactlyInAnyOrder("scope1", "scope2");
+		assertThat(clientRegistration.getJwkSetUrl()).isEqualTo(new URL("https://client.example.com/jwks"));
 		assertThat(clientRegistration.getIdTokenSignedResponseAlgorithm()).isEqualTo("RS256");
 		assertThat(clientRegistration.getRegistrationAccessToken()).isEqualTo("registration-access-token");
 		assertThat(clientRegistration.getRegistrationClientUrl().toString()).isEqualTo("https://auth-server.com/connect/register?client_id=1");
@@ -97,7 +98,7 @@ public class OidcClientRegistrationTests {
 	}
 
 	@Test
-	public void withClaimsWhenClaimsProvidedThenCreated() {
+	public void withClaimsWhenClaimsProvidedThenCreated() throws Exception {
 		Instant clientIdIssuedAt = Instant.now();
 		Instant clientSecretExpiresAt = clientIdIssuedAt.plus(30, ChronoUnit.DAYS);
 		HashMap<String, Object> claims = new HashMap<>();
@@ -107,13 +108,13 @@ public class OidcClientRegistrationTests {
 		claims.put(OidcClientMetadataClaimNames.CLIENT_SECRET_EXPIRES_AT, clientSecretExpiresAt);
 		claims.put(OidcClientMetadataClaimNames.CLIENT_NAME, "client-name");
 		claims.put(OidcClientMetadataClaimNames.REDIRECT_URIS, Collections.singletonList("https://client.example.com"));
-		claims.put(OidcClientMetadataClaimNames.JWKS_URI, "https://client.example.com/jwks");
-		claims.put(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD, ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue());
+		claims.put(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD, ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue());
 		claims.put(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_SIGNING_ALG, MacAlgorithm.HS256.getName());
 		claims.put(OidcClientMetadataClaimNames.GRANT_TYPES, Arrays.asList(
 				AuthorizationGrantType.AUTHORIZATION_CODE.getValue(), AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()));
 		claims.put(OidcClientMetadataClaimNames.RESPONSE_TYPES, Collections.singletonList("code"));
 		claims.put(OidcClientMetadataClaimNames.SCOPE, Arrays.asList("scope1", "scope2"));
+		claims.put(OidcClientMetadataClaimNames.JWKS_URI, "https://client.example.com/jwks");
 		claims.put(OidcClientMetadataClaimNames.ID_TOKEN_SIGNED_RESPONSE_ALG, SignatureAlgorithm.RS256.getName());
 		claims.put(OidcClientMetadataClaimNames.REGISTRATION_ACCESS_TOKEN, "registration-access-token");
 		claims.put(OidcClientMetadataClaimNames.REGISTRATION_CLIENT_URI, "https://auth-server.com/connect/register?client_id=1");
@@ -127,12 +128,12 @@ public class OidcClientRegistrationTests {
 		assertThat(clientRegistration.getClientSecretExpiresAt()).isEqualTo(clientSecretExpiresAt);
 		assertThat(clientRegistration.getClientName()).isEqualTo("client-name");
 		assertThat(clientRegistration.getRedirectUris()).containsOnly("https://client.example.com");
-		assertThat(clientRegistration.getJwkSetUrl().toString()).isEqualTo("https://client.example.com/jwks");
-		assertThat(clientRegistration.getTokenEndpointAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue());
+		assertThat(clientRegistration.getTokenEndpointAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue());
 		assertThat(clientRegistration.getTokenEndpointAuthenticationSigningAlgorithm()).isEqualTo(MacAlgorithm.HS256.getName());
 		assertThat(clientRegistration.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "client_credentials");
 		assertThat(clientRegistration.getResponseTypes()).containsOnly("code");
 		assertThat(clientRegistration.getScopes()).containsExactlyInAnyOrder("scope1", "scope2");
+		assertThat(clientRegistration.getJwkSetUrl()).isEqualTo(new URL("https://client.example.com/jwks"));
 		assertThat(clientRegistration.getIdTokenSignedResponseAlgorithm()).isEqualTo("RS256");
 		assertThat(clientRegistration.getRegistrationAccessToken()).isEqualTo("registration-access-token");
 		assertThat(clientRegistration.getRegistrationClientUrl().toString()).isEqualTo("https://auth-server.com/connect/register?client_id=1");
@@ -365,6 +366,16 @@ public class OidcClientRegistrationTests {
 		assertThat(clientRegistration.getScopes()).containsExactly("scope1");
 	}
 
+	@Test
+	public void buildWhenJwksUriNotUrlThenThrowIllegalArgumentException() {
+		OidcClientRegistration.Builder builder = this.minimalBuilder
+				.claim(OidcClientMetadataClaimNames.JWKS_URI, "not an url");
+
+		assertThatIllegalArgumentException()
+				.isThrownBy(builder::build)
+				.withMessage("jwksUri must be a valid URL");
+	}
+
 	@Test
 	public void claimWhenNameNullThenThrowIllegalArgumentException() {
 		assertThatIllegalArgumentException()

+ 15 - 13
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/core/oidc/http/converter/OidcClientRegistrationHttpMessageConverterTests.java

@@ -15,6 +15,7 @@
  */
 package org.springframework.security.oauth2.core.oidc.http.converter;
 
+import java.net.URL;
 import java.time.Instant;
 import java.util.Map;
 
@@ -30,6 +31,7 @@ import org.springframework.security.oauth2.core.AuthorizationGrantType;
 import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
 import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
 import org.springframework.security.oauth2.core.oidc.OidcClientRegistration;
+import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
 import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -86,7 +88,7 @@ public class OidcClientRegistrationHttpMessageConverterTests {
 	}
 
 	@Test
-	public void readInternalWhenValidParametersThenSuccess() {
+	public void readInternalWhenValidParametersThenSuccess() throws Exception {
 		// @formatter:off
 		String clientRegistrationRequest = "{\n"
 				+"		\"client_id\": \"client-id\",\n"
@@ -97,7 +99,8 @@ public class OidcClientRegistrationHttpMessageConverterTests {
 				+"		\"redirect_uris\": [\n"
 				+ "			\"https://client.example.com\"\n"
 				+ "		],\n"
-				+"		\"token_endpoint_auth_method\": \"client_secret_basic\",\n"
+				+"		\"token_endpoint_auth_method\": \"client_secret_jwt\",\n"
+				+"		\"token_endpoint_auth_signing_alg\": \"HS256\",\n"
 				+"		\"grant_types\": [\n"
 				+"			\"authorization_code\",\n"
 				+"			\"client_credentials\"\n"
@@ -106,9 +109,8 @@ public class OidcClientRegistrationHttpMessageConverterTests {
 				+"			\"code\"\n"
 				+"		],\n"
 				+"		\"scope\": \"scope1 scope2\",\n"
+				+"		\"jwks_uri\": \"https://client.example.com/jwks\",\n"
 				+"		\"id_token_signed_response_alg\": \"RS256\",\n"
-				+"      \"jwks_uri\": \"https://client.example.com/jwks\",\n"
-				+"      \"token_endpoint_auth_signing_alg\": \"HS256\",\n"
 				+"		\"a-claim\": \"a-value\"\n"
 				+"}\n";
 		// @formatter:on
@@ -123,13 +125,13 @@ public class OidcClientRegistrationHttpMessageConverterTests {
 		assertThat(clientRegistration.getClientSecretExpiresAt()).isEqualTo(Instant.ofEpochSecond(1607637467L));
 		assertThat(clientRegistration.getClientName()).isEqualTo("client-name");
 		assertThat(clientRegistration.getRedirectUris()).containsOnly("https://client.example.com");
-		assertThat(clientRegistration.getTokenEndpointAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue());
+		assertThat(clientRegistration.getTokenEndpointAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue());
+		assertThat(clientRegistration.getTokenEndpointAuthenticationSigningAlgorithm()).isEqualTo(MacAlgorithm.HS256.getName());
 		assertThat(clientRegistration.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "client_credentials");
 		assertThat(clientRegistration.getResponseTypes()).containsOnly("code");
 		assertThat(clientRegistration.getScopes()).containsExactlyInAnyOrder("scope1", "scope2");
+		assertThat(clientRegistration.getJwkSetUrl()).isEqualTo(new URL("https://client.example.com/jwks"));
 		assertThat(clientRegistration.getIdTokenSignedResponseAlgorithm()).isEqualTo("RS256");
-		assertThat(clientRegistration.getJwkSetUrl().toString()).isEqualTo("https://client.example.com/jwks");
-		assertThat(clientRegistration.getTokenEndpointAuthenticationSigningAlgorithm()).isEqualTo("HS256");
 		assertThat(clientRegistration.getClaimAsString("a-claim")).isEqualTo("a-value");
 	}
 
@@ -181,17 +183,17 @@ public class OidcClientRegistrationHttpMessageConverterTests {
 				.clientSecretExpiresAt(Instant.ofEpochSecond(1607637467))
 				.clientName("client-name")
 				.redirectUri("https://client.example.com")
-				.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue())
+				.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue())
+				.tokenEndpointAuthenticationSigningAlgorithm(MacAlgorithm.HS256.getName())
 				.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
 				.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
 				.responseType(OAuth2AuthorizationResponseType.CODE.getValue())
 				.scope("scope1")
 				.scope("scope2")
+				.jwkSetUrl("https://client.example.com/jwks")
 				.idTokenSignedResponseAlgorithm(SignatureAlgorithm.RS256.getName())
 				.registrationAccessToken("registration-access-token")
 				.registrationClientUrl("https://auth-server.com/connect/register?client_id=1")
-				.jwkSetUrl("https://client.example.com/jwks")
-				.tokenEndpointAuthenticationSigningAlgorithm("HS256")
 				.claim("a-claim", "a-value")
 				.build();
 		// @formatter:on
@@ -206,15 +208,15 @@ public class OidcClientRegistrationHttpMessageConverterTests {
 		assertThat(clientRegistrationResponse).contains("\"client_secret_expires_at\":1607637467");
 		assertThat(clientRegistrationResponse).contains("\"client_name\":\"client-name\"");
 		assertThat(clientRegistrationResponse).contains("\"redirect_uris\":[\"https://client.example.com\"]");
-		assertThat(clientRegistrationResponse).contains("\"token_endpoint_auth_method\":\"client_secret_basic\"");
+		assertThat(clientRegistrationResponse).contains("\"token_endpoint_auth_method\":\"client_secret_jwt\"");
+		assertThat(clientRegistrationResponse).contains("\"token_endpoint_auth_signing_alg\":\"HS256\"");
 		assertThat(clientRegistrationResponse).contains("\"grant_types\":[\"authorization_code\",\"client_credentials\"]");
 		assertThat(clientRegistrationResponse).contains("\"response_types\":[\"code\"]");
 		assertThat(clientRegistrationResponse).contains("\"scope\":\"scope1 scope2\"");
+		assertThat(clientRegistrationResponse).contains("\"jwks_uri\":\"https://client.example.com/jwks\"");
 		assertThat(clientRegistrationResponse).contains("\"id_token_signed_response_alg\":\"RS256\"");
 		assertThat(clientRegistrationResponse).contains("\"registration_access_token\":\"registration-access-token\"");
 		assertThat(clientRegistrationResponse).contains("\"registration_client_uri\":\"https://auth-server.com/connect/register?client_id=1\"");
-		assertThat(clientRegistrationResponse).contains("\"jwks_uri\":\"https://client.example.com/jwks\"");
-		assertThat(clientRegistrationResponse).contains("\"token_endpoint_auth_signing_alg\":\"HS256\"");
 		assertThat(clientRegistrationResponse).contains("\"a-claim\":\"a-value\"");
 	}
 

+ 23 - 0
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/jose/TestJwks.java

@@ -15,10 +15,13 @@
  */
 package org.springframework.security.oauth2.jose;
 
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
 import java.security.interfaces.ECPrivateKey;
 import java.security.interfaces.ECPublicKey;
 import java.security.interfaces.RSAPrivateKey;
 import java.security.interfaces.RSAPublicKey;
+import java.util.UUID;
 
 import javax.crypto.SecretKey;
 
@@ -33,6 +36,16 @@ import com.nimbusds.jose.jwk.RSAKey;
  */
 public final class TestJwks {
 
+	private static final KeyPairGenerator rsaKeyPairGenerator;
+	static {
+		try {
+			rsaKeyPairGenerator = KeyPairGenerator.getInstance("RSA");
+			rsaKeyPairGenerator.initialize(2048);
+		} catch (Exception ex) {
+			throw new IllegalStateException(ex);
+		}
+	}
+
 	// @formatter:off
 	public static final RSAKey DEFAULT_RSA_JWK =
 			jwk(
@@ -59,6 +72,16 @@ public final class TestJwks {
 	private TestJwks() {
 	}
 
+	public static RSAKey.Builder generateRsaJwk() {
+		KeyPair keyPair = rsaKeyPairGenerator.generateKeyPair();
+		RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
+		RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
+		// @formatter:off
+		return jwk(publicKey, privateKey)
+				.keyID(UUID.randomUUID().toString());
+		// @formatter:on
+	}
+
 	public static RSAKey.Builder jwk(RSAPublicKey publicKey, RSAPrivateKey privateKey) {
 		// @formatter:off
 		return new RSAKey.Builder(publicKey)

+ 280 - 158
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientAuthenticationProviderTests.java

@@ -15,10 +15,20 @@
  */
 package org.springframework.security.oauth2.server.authorization.authentication;
 
+import java.nio.charset.StandardCharsets;
 import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.jwk.OctetSequenceKey;
+import com.nimbusds.jose.jwk.source.JWKSource;
+import com.nimbusds.jose.proc.SecurityContext;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -31,25 +41,35 @@ import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
 import org.springframework.security.oauth2.core.OAuth2TokenType;
 import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
 import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
+import org.springframework.security.oauth2.jose.TestJwks;
+import org.springframework.security.oauth2.jose.TestKeys;
+import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
+import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
+import org.springframework.security.oauth2.jwt.BadJwtException;
+import org.springframework.security.oauth2.jwt.JoseHeader;
 import org.springframework.security.oauth2.jwt.Jwt;
-import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
+import org.springframework.security.oauth2.jwt.JwtClaimsSet;
+import org.springframework.security.oauth2.jwt.JwtEncoder;
+import org.springframework.security.oauth2.jwt.JwtValidationException;
+import org.springframework.security.oauth2.jwt.NimbusJwsEncoder;
 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.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 org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
-import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.web.util.UriComponentsBuilder;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
 
 /**
@@ -80,13 +100,14 @@ public class OAuth2ClientAuthenticationProviderTests {
 	private OAuth2AuthorizationService authorizationService;
 	private OAuth2ClientAuthenticationProvider authenticationProvider;
 	private PasswordEncoder passwordEncoder;
-	private JwtDecoderFactory<RegisteredClient> jwtDecoderFactory;
+	private ProviderSettings providerSettings;
 
 	@Before
 	public void setUp() {
 		this.registeredClientRepository = mock(RegisteredClientRepository.class);
 		this.authorizationService = mock(OAuth2AuthorizationService.class);
-		this.authenticationProvider = new OAuth2ClientAuthenticationProvider(this.registeredClientRepository, this.authorizationService);
+		this.authenticationProvider = new OAuth2ClientAuthenticationProvider(
+				this.registeredClientRepository, this.authorizationService);
 		this.passwordEncoder = spy(new PasswordEncoder() {
 			@Override
 			public String encode(CharSequence rawPassword) {
@@ -99,9 +120,8 @@ public class OAuth2ClientAuthenticationProviderTests {
 			}
 		});
 		this.authenticationProvider.setPasswordEncoder(this.passwordEncoder);
-		this.authenticationProvider.setProviderSettings(ProviderSettings.builder().issuer("https://auth-server.com").build());
-		this.jwtDecoderFactory = mock(JwtDecoderFactory.class);
-		ReflectionTestUtils.setField(this.authenticationProvider, "jwtDecoderFactory", this.jwtDecoderFactory);
+		this.providerSettings = ProviderSettings.builder().issuer("https://auth-server.com").build();
+		this.authenticationProvider.setProviderSettings(this.providerSettings);
 	}
 
 	@Test
@@ -147,6 +167,23 @@ public class OAuth2ClientAuthenticationProviderTests {
 				});
 	}
 
+	@Test
+	public void authenticateWhenUnsupportedClientAuthenticationMethodThenThrowOAuth2AuthenticationException() {
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+
+		OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
+				registeredClient.getClientId(), ClientAuthenticationMethod.CLIENT_SECRET_POST, registeredClient.getClientSecret(), null);
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+				.isInstanceOf(OAuth2AuthenticationException.class)
+				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
+				.satisfies(error -> {
+					assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
+					assertThat(error.getDescription()).contains("authentication_method");
+				});
+	}
+
 	@Test
 	public void authenticateWhenInvalidClientSecretThenThrowOAuth2AuthenticationException() {
 		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
@@ -220,146 +257,6 @@ public class OAuth2ClientAuthenticationProviderTests {
 		assertThat(authenticationResult.getRegisteredClient()).isEqualTo(registeredClient);
 	}
 
-	@Test
-	public void authenticateWhenJwtBearerAndClientNotSupportingItThenThrowOAuth2AuthenticationException() {
-		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
-		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
-				.thenReturn(registeredClient);
-		OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
-				registeredClient.getClientId(), JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD,
-				registeredClient.getClientSecret(), null);
-
-		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
-				.isInstanceOf(OAuth2AuthenticationException.class)
-				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
-				.extracting("errorCode")
-				.isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
-
-		verify(this.jwtDecoderFactory, never()).createDecoder(any());
-	}
-
-	@Test
-	public void authenticateWhenClientJwtAssertionAndPrivateJwtAndFailedCreateDecoderThenThrowOAuth2AuthenticationException() {
-		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
-				.clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT)
-				.build();
-		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
-				.thenReturn(registeredClient);
-		when(this.jwtDecoderFactory.createDecoder(any()))
-				.thenThrow(new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT));
-		OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
-				"https://auth-server.com", registeredClient.getClientId(), JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD,
-				registeredClient.getClientSecret(), null);
-
-		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
-				.isInstanceOf(OAuth2AuthenticationException.class)
-				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
-				.extracting("errorCode")
-				.isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
-	}
-
-	@Test
-	public void authenticateWhenClientJwtAssertionAndPrivateKeyJwtAndFailedVerifyTokenThenThrowOAuth2AuthenticationException() {
-		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
-				.clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT)
-				.build();
-		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
-				.thenReturn(registeredClient);
-		when(this.jwtDecoderFactory.createDecoder(any()))
-				.thenReturn(s -> { throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT); });
-		OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
-				"https://auth-server.com", registeredClient.getClientId(), JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD,
-				registeredClient.getClientSecret(), null);
-
-		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
-				.isInstanceOf(OAuth2AuthenticationException.class)
-				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
-				.extracting("errorCode")
-				.isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
-	}
-
-	@Test
-	public void authenticateWhenClientJwtAssertionAndBadAudienceThenThrowOAuth2AuthenticationException() {
-		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
-				.clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT).build();
-		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
-				.thenReturn(registeredClient);
-		when(this.jwtDecoderFactory.createDecoder(any()))
-				.thenReturn(s -> createJwtToken("client-1", "https://bad-server.com/oauth2/token"));
-		OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
-				"/oauth2/token", registeredClient.getClientId(),
-				JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD, registeredClient.getClientSecret(), null);
-
-		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
-				.isInstanceOf(OAuth2AuthenticationException.class)
-				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
-				.extracting("errorCode")
-				.isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
-	}
-
-	@Test
-	public void authenticateWhenClientJwtAssertionAndPrivateJwtVerificationSuccessThenAuthenticate() {
-		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
-				.clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT).build();
-		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
-				.thenReturn(registeredClient);
-		when(this.jwtDecoderFactory.createDecoder(any()))
-				.thenReturn(s -> createJwtToken("client-1", "https://auth-server.com/oauth2/token"));
-		OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
-				"https://auth-server.com/oauth2/token", registeredClient.getClientId(),
-				JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD, registeredClient.getClientSecret(), null);
-
-		OAuth2ClientAuthenticationToken authenticationResult =
-				(OAuth2ClientAuthenticationToken) this.authenticationProvider.authenticate(authentication);
-
-		assertThat(authenticationResult.isAuthenticated()).isTrue();
-		assertThat(authenticationResult.getPrincipal().toString()).isEqualTo(registeredClient.getClientId());
-		assertThat(authenticationResult.getCredentials().toString()).isEqualTo(registeredClient.getClientSecret());
-		assertThat(authenticationResult.getRegisteredClient()).isEqualTo(registeredClient);
-	}
-
-	@Test
-	public void authenticateWhenClientJwtAssertionAndClientSecretJwtAndFailedVerifyTokenThenThrowOAuth2Exception() {
-		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
-				.clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT)
-				.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
-				.build();
-		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
-				.thenReturn(registeredClient);
-		when(this.jwtDecoderFactory.createDecoder(any()))
-				.thenReturn(s -> { throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT); });
-		OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
-				"https://auth-server.com", registeredClient.getClientId(), JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD,
-				registeredClient.getClientSecret(), null);
-
-		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
-				.isInstanceOf(OAuth2AuthenticationException.class)
-				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
-				.extracting("errorCode")
-				.isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
-	}
-
-	@Test
-	public void authenticateWhenClientJwtAssertionAndClientSecretJwtVerificationSuccess() {
-		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
-				.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT).build();
-		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
-				.thenReturn(registeredClient);
-		when(this.jwtDecoderFactory.createDecoder(any()))
-				.thenReturn(s -> createJwtToken("client-1", "https://auth-server.com/oauth2/token"));
-		OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
-				"https://auth-server.com/oauth2/token", registeredClient.getClientId(), JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD,
-				registeredClient.getClientSecret(), null);
-
-		OAuth2ClientAuthenticationToken authenticationResult =
-				(OAuth2ClientAuthenticationToken) this.authenticationProvider.authenticate(authentication);
-
-		assertThat(authenticationResult.isAuthenticated()).isTrue();
-		assertThat(authenticationResult.getPrincipal().toString()).isEqualTo(registeredClient.getClientId());
-		assertThat(authenticationResult.getCredentials().toString()).isEqualTo(registeredClient.getClientSecret());
-		assertThat(authenticationResult.getRegisteredClient()).isEqualTo(registeredClient);
-	}
-
 	@Test
 	public void authenticateWhenPkceAndInvalidCodeThenThrowOAuth2AuthenticationException() {
 		RegisteredClient registeredClient = TestRegisteredClients.registeredPublicClient().build();
@@ -596,13 +493,34 @@ public class OAuth2ClientAuthenticationProviderTests {
 	}
 
 	@Test
-	public void authenticateWhenClientAuthenticationMethodNotConfiguredThenThrowOAuth2AuthenticationException() {
+	public void authenticateWhenJwtClientAssertionAndInvalidClientIdThenThrowOAuth2AuthenticationException() {
+		// @formatter:off
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
+				.build();
+		// @formatter:on
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+
+		OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
+				registeredClient.getClientId() + "-invalid", JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD, "jwt-assertion", null);
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+				.isInstanceOf(OAuth2AuthenticationException.class)
+				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
+				.satisfies(error -> {
+					assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
+					assertThat(error.getDescription()).contains(OAuth2ParameterNames.CLIENT_ID);
+				});
+	}
+
+	@Test
+	public void authenticateWhenJwtClientAssertionAndUnsupportedClientAuthenticationMethodThenThrowOAuth2AuthenticationException() {
 		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
 		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
 				.thenReturn(registeredClient);
 
 		OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
-				registeredClient.getClientId(), ClientAuthenticationMethod.CLIENT_SECRET_POST, registeredClient.getClientSecret(), null);
+				registeredClient.getClientId(), JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD, "jwt-assertion", null);
 		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
 				.isInstanceOf(OAuth2AuthenticationException.class)
 				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
@@ -612,6 +530,218 @@ public class OAuth2ClientAuthenticationProviderTests {
 				});
 	}
 
+	@Test
+	public void authenticateWhenJwtClientAssertionAndMissingJwkSetUrlThenThrowOAuth2AuthenticationException() {
+		// @formatter:off
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT)
+				.clientSettings(
+						ClientSettings.builder()
+								.tokenEndpointAuthenticationSigningAlgorithm(SignatureAlgorithm.RS256)
+								.build()
+				)
+				.build();
+		// @formatter:on
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+
+		OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
+				registeredClient.getClientId(), JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD, "jwt-assertion", null);
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+				.isInstanceOf(OAuth2AuthenticationException.class)
+				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
+				.satisfies(error -> {
+					assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
+					assertThat(error.getDescription()).isEqualTo("Failed to find a Signature Verifier for Client: '" +
+							registeredClient.getId() + "'. Check to ensure you have configured the JWK Set URL.");
+				});
+	}
+
+	@Test
+	public void authenticateWhenJwtClientAssertionAndMissingClientSecretThenThrowOAuth2AuthenticationException() {
+		// @formatter:off
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.clientSecret(null)
+				.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
+				.clientSettings(
+						ClientSettings.builder()
+								.tokenEndpointAuthenticationSigningAlgorithm(MacAlgorithm.HS256)
+								.build()
+				)
+				.build();
+		// @formatter:on
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+
+		OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
+				registeredClient.getClientId(), JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD, "jwt-assertion", null);
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+				.isInstanceOf(OAuth2AuthenticationException.class)
+				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
+				.satisfies(error -> {
+					assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
+					assertThat(error.getDescription()).isEqualTo("Failed to find a Signature Verifier for Client: '" +
+							registeredClient.getId() + "'. Check to ensure you have configured the client secret.");
+				});
+	}
+
+	@Test
+	public void authenticateWhenJwtClientAssertionAndMissingSigningAlgorithmThenThrowOAuth2AuthenticationException() {
+		// @formatter:off
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.clientSecret(TestKeys.DEFAULT_ENCODED_SECRET_KEY)
+				.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
+				.build();
+		// @formatter:on
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+
+		OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
+				registeredClient.getClientId(), JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD, "jwt-assertion", null);
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+				.isInstanceOf(OAuth2AuthenticationException.class)
+				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
+				.satisfies(error -> {
+					assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
+					assertThat(error.getDescription()).isEqualTo("Failed to find a Signature Verifier for Client: '" +
+							registeredClient.getId() + "'. Check to ensure you have configured a valid JWS Algorithm: 'null'.");
+				});
+	}
+
+	@Test
+	public void authenticateWhenJwtClientAssertionAndInvalidCredentialsThenThrowOAuth2AuthenticationException() {
+		// @formatter:off
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.clientSecret(TestKeys.DEFAULT_ENCODED_SECRET_KEY)
+				.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
+				.clientSettings(
+						ClientSettings.builder()
+								.tokenEndpointAuthenticationSigningAlgorithm(MacAlgorithm.HS256)
+								.build()
+				)
+				.build();
+		// @formatter:on
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+
+		OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
+				registeredClient.getClientId(), JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD, "invalid-jwt-assertion", null);
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+				.isInstanceOf(OAuth2AuthenticationException.class)
+				.hasCauseInstanceOf(BadJwtException.class)
+				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
+				.satisfies(error -> {
+					assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
+					assertThat(error.getDescription()).contains(OAuth2ParameterNames.CLIENT_ASSERTION);
+				});
+	}
+
+	@Test
+	public void authenticateWhenJwtClientAssertionAndInvalidClaimsThenThrowOAuth2AuthenticationException() {
+		// @formatter:off
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.clientSecret(TestKeys.DEFAULT_ENCODED_SECRET_KEY)
+				.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
+				.clientSettings(
+						ClientSettings.builder()
+								.tokenEndpointAuthenticationSigningAlgorithm(MacAlgorithm.HS256)
+								.build()
+				)
+				.build();
+		// @formatter:on
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+
+		// @formatter:off
+		JoseHeader joseHeader = JoseHeader.withAlgorithm(MacAlgorithm.HS256)
+				.build();
+		JwtClaimsSet jwtClaimsSet = JwtClaimsSet.builder()
+				.issuer("invalid-iss")
+				.subject("invalid-sub")
+				.audience(Collections.singletonList("invalid-aud"))
+				.build();
+		// @formatter:on
+
+		JwtEncoder jwsEncoder = createEncoder(TestKeys.DEFAULT_ENCODED_SECRET_KEY, "HmacSHA256");
+		Jwt jwtAssertion = jwsEncoder.encode(joseHeader, jwtClaimsSet);
+
+		OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
+				registeredClient.getClientId(), JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD, jwtAssertion.getTokenValue(), null);
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+				.isInstanceOf(OAuth2AuthenticationException.class)
+				.hasCauseInstanceOf(JwtValidationException.class)
+				.extracting(ex -> (OAuth2AuthenticationException) ex)
+				.satisfies(ex -> {
+					assertThat(ex.getError().getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
+					assertThat(ex.getError().getDescription()).contains(OAuth2ParameterNames.CLIENT_ASSERTION);
+					JwtValidationException jwtValidationException = (JwtValidationException) ex.getCause();
+					assertThat(jwtValidationException.getErrors()).hasSize(4);		// iss, sub, aud, exp
+				});
+	}
+
+	@Test
+	public void authenticateWhenJwtClientAssertionAndValidCredentialsThenAuthenticated() {
+		// @formatter:off
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
+				.clientSecret(TestKeys.DEFAULT_ENCODED_SECRET_KEY)
+				.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
+				.clientSettings(
+						ClientSettings.builder()
+								.tokenEndpointAuthenticationSigningAlgorithm(MacAlgorithm.HS256)
+								.build()
+				)
+				.build();
+		// @formatter:on
+		when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
+				.thenReturn(registeredClient);
+
+		// @formatter:off
+		JoseHeader joseHeader = JoseHeader.withAlgorithm(MacAlgorithm.HS256)
+				.build();
+		JwtClaimsSet jwtClaimsSet = jwtClientAssertionClaims(registeredClient)
+				.build();
+		// @formatter:on
+
+		JwtEncoder jwsEncoder = createEncoder(TestKeys.DEFAULT_ENCODED_SECRET_KEY, "HmacSHA256");
+		Jwt jwtAssertion = jwsEncoder.encode(joseHeader, jwtClaimsSet);
+
+		OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken(
+				registeredClient.getClientId(), JWT_CLIENT_ASSERTION_AUTHENTICATION_METHOD, jwtAssertion.getTokenValue(), null);
+		OAuth2ClientAuthenticationToken authenticationResult =
+				(OAuth2ClientAuthenticationToken) this.authenticationProvider.authenticate(authentication);
+
+		verifyNoInteractions(this.passwordEncoder);
+
+		assertThat(authenticationResult.isAuthenticated()).isTrue();
+		assertThat(authenticationResult.getPrincipal().toString()).isEqualTo(registeredClient.getClientId());
+		assertThat(authenticationResult.getCredentials()).isInstanceOf(Jwt.class);
+		assertThat(authenticationResult.getRegisteredClient()).isEqualTo(registeredClient);
+		assertThat(authenticationResult.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_JWT);
+	}
+
+	private JwtClaimsSet.Builder jwtClientAssertionClaims(RegisteredClient registeredClient) {
+		Instant issuedAt = Instant.now();
+		Instant expiresAt = issuedAt.plus(1, ChronoUnit.HOURS);
+		return JwtClaimsSet.builder()
+				.issuer(registeredClient.getClientId())
+				.subject(registeredClient.getClientId())
+				.audience(Collections.singletonList(asUrl(this.providerSettings.getIssuer(), this.providerSettings.getTokenEndpoint())))
+				.issuedAt(issuedAt)
+				.expiresAt(expiresAt);
+	}
+
+	private static JwtEncoder createEncoder(String secret, String algorithm) {
+		SecretKey secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), algorithm);
+		OctetSequenceKey secretKeyJwk = TestJwks.jwk(secretKey).build();
+		JWKSource<SecurityContext> jwkSource = (jwkSelector, securityContext) ->
+				jwkSelector.select(new JWKSet(secretKeyJwk));
+		return new NimbusJwsEncoder(jwkSource);
+	}
+
+	private static String asUrl(String uri, String path) {
+		return UriComponentsBuilder.fromUriString(uri).path(path).build().toUriString();
+	}
+
 	private static Map<String, Object> createAuthorizationCodeTokenParameters() {
 		Map<String, Object> parameters = new HashMap<>();
 		parameters.put(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
@@ -639,12 +769,4 @@ public class OAuth2ClientAuthenticationProviderTests {
 		return parameters;
 	}
 
-	private static Jwt createJwtToken(String subject, String audience) {
-		Map<String, Object> headers = new HashMap<>();
-		headers.put("kid", "123");
-		Map<String, Object> claims = new HashMap<>();
-		claims.put("sub", subject);
-		claims.put("aud", audience);
-		return new Jwt("123", Instant.now().minusSeconds(30), Instant.now().plusSeconds(30), headers, claims);
-	}
 }

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

@@ -32,23 +32,16 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
  */
 public class OAuth2ClientAuthenticationTokenTests {
 
-	@Test
-	public void constructorWhenRequestUriNullThenThrowIllegalArgumentException() {
-		assertThatThrownBy(() -> new OAuth2ClientAuthenticationToken(null, "clientId", ClientAuthenticationMethod.CLIENT_SECRET_BASIC, "secret", null))
-				.isInstanceOf(IllegalArgumentException.class)
-				.hasMessage("requestUri cannot be empty");
-	}
-
 	@Test
 	public void constructorWhenClientIdNullThenThrowIllegalArgumentException() {
-		assertThatThrownBy(() -> new OAuth2ClientAuthenticationToken("issuer", null, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, "secret", null))
+		assertThatThrownBy(() -> new OAuth2ClientAuthenticationToken(null, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, "secret", null))
 				.isInstanceOf(IllegalArgumentException.class)
 				.hasMessage("clientId cannot be empty");
 	}
 
 	@Test
 	public void constructorWhenClientAuthenticationMethodNullThenThrowIllegalArgumentException() {
-		assertThatThrownBy(() -> new OAuth2ClientAuthenticationToken("issuer", "clientId", null, "clientSecret", null))
+		assertThatThrownBy(() -> new OAuth2ClientAuthenticationToken("clientId", null, "clientSecret", null))
 				.isInstanceOf(IllegalArgumentException.class)
 				.hasMessage("clientAuthenticationMethod cannot be null");
 	}
@@ -62,10 +55,9 @@ public class OAuth2ClientAuthenticationTokenTests {
 
 	@Test
 	public void constructorWhenClientCredentialsProvidedThenCreated() {
-		OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken("issuer", "clientId",
+		OAuth2ClientAuthenticationToken authentication = new OAuth2ClientAuthenticationToken("clientId",
 				ClientAuthenticationMethod.CLIENT_SECRET_BASIC, "secret", null);
 		assertThat(authentication.isAuthenticated()).isFalse();
-		assertThat(authentication.getRequestUri()).isEqualTo("issuer");
 		assertThat(authentication.getPrincipal().toString()).isEqualTo("clientId");
 		assertThat(authentication.getCredentials()).isEqualTo("secret");
 		assertThat(authentication.getRegisteredClient()).isNull();

+ 0 - 436
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/RegisteredClientJwtAssertionDecoderFactoryTests.java

@@ -1,436 +0,0 @@
-/*
- * Copyright 2020-2021 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.springframework.security.oauth2.server.authorization.authentication;
-
-import com.nimbusds.jose.JOSEException;
-import com.nimbusds.jose.JWSAlgorithm;
-import com.nimbusds.jose.JWSHeader;
-import com.nimbusds.jose.JWSSigner;
-import com.nimbusds.jose.crypto.MACSigner;
-import com.nimbusds.jose.crypto.RSASSASigner;
-import com.nimbusds.jose.jwk.JWK;
-import com.nimbusds.jose.jwk.KeyUse;
-import com.nimbusds.jose.jwk.RSAKey;
-import com.nimbusds.jwt.JWTClaimsSet;
-import com.nimbusds.jwt.SignedJWT;
-import okhttp3.mockwebserver.MockResponse;
-import okhttp3.mockwebserver.MockWebServer;
-import org.junit.Before;
-import org.junit.Test;
-import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
-import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
-import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
-import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
-import org.springframework.security.oauth2.jwt.JwtDecoder;
-import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
-import org.springframework.security.oauth2.jwt.JwtException;
-import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
-import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
-import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
-import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
-import org.springframework.security.oauth2.server.authorization.config.ClientSettings;
-import org.springframework.test.util.ReflectionTestUtils;
-
-import java.nio.charset.StandardCharsets;
-import java.security.KeyPair;
-import java.security.KeyPairGenerator;
-import java.security.interfaces.RSAPublicKey;
-import java.time.Instant;
-import java.util.Date;
-import java.util.UUID;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.mockito.Mockito.mock;
-
-/**
- * Tests for {@link OAuth2ClientAuthenticationProvider.RegisteredClientJwtAssertionDecoderFactory}
- *
- * @author Rafal Lewczuk
- */
-public class RegisteredClientJwtAssertionDecoderFactoryTests {
-
-	private JwtDecoderFactory<RegisteredClient> registeredClientDecoderFactory;
-
-	@Before
-	public void setUp() {
-		OAuth2ClientAuthenticationProvider authenticationProvider = new OAuth2ClientAuthenticationProvider(
-				mock(RegisteredClientRepository.class), mock(OAuth2AuthorizationService.class));
-		this.registeredClientDecoderFactory = (JwtDecoderFactory<RegisteredClient>)
-				ReflectionTestUtils.getField(authenticationProvider, "jwtDecoderFactory");
-	}
-
-	@Test
-	public void createDecoderWhenRegisteredClientNullThenThrowIllegalArgumentException() {
-		assertThatIllegalArgumentException()
-				.isThrownBy(() -> registeredClientDecoderFactory.createDecoder(null))
-				.withMessage("registeredClient cannot be null");
-	}
-
-	@Test
-	public void createDecoderWhenClientAuthenticationMethodNotSupportedThenThrowOAuth2AuthenticationException() {
-		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
-
-		assertThatThrownBy(() -> this.registeredClientDecoderFactory.createDecoder(registeredClient))
-				.isInstanceOf(OAuth2AuthenticationException.class);
-	}
-
-	@Test
-	public void createDecoderWithClientSecretJwtWhenClientSecretNullThenThrowOAuth2Exception() {
-		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
-				.clientSecret(null)
-				.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
-				.clientSettings(ClientSettings.builder().tokenEndpointSigningAlgorithm(MacAlgorithm.HS256).build())
-				.build();
-
-		assertThatThrownBy(() -> this.registeredClientDecoderFactory.createDecoder(registeredClient))
-				.isInstanceOf(OAuth2AuthenticationException.class);
-	}
-
-	@Test
-	public void createDecoderWithClientSecretJwtClientThenReturnDecoder() {
-		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
-				.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
-				.clientSecret("0123456789abcdef0123456789ABCDEF")
-				.clientSettings(ClientSettings.builder().tokenEndpointSigningAlgorithm(MacAlgorithm.HS256).build())
-				.build();
-
-		JwtDecoder jwtDecoder = this.registeredClientDecoderFactory.createDecoder(registeredClient);
-
-		assertThat(jwtDecoder).isNotNull();
-	}
-
-	@Test
-	public void createDecoderWithClientSecretJwtTwiceThenReturnCachedDecoder() {
-		RegisteredClient.Builder registeredClientBuilder = TestRegisteredClients.registeredClient()
-				.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
-				.clientSecret("0123456789abcdef0123456789ABCDEF")
-				.clientSettings(ClientSettings.builder().tokenEndpointSigningAlgorithm(MacAlgorithm.HS256).build());
-
-		JwtDecoder decoder1 = this.registeredClientDecoderFactory.createDecoder(registeredClientBuilder.build());
-		JwtDecoder decoder2 = this.registeredClientDecoderFactory.createDecoder(registeredClientBuilder.build());
-
-		assertThat(decoder1).isNotNull();
-		assertThat(decoder2).isSameAs(decoder1);
-	}
-
-	@Test
-	public void createDecoderWithClientSecretJwtAndSecondWithChangedAlgorithmThenReturnRecreatedDecoder() {
-		RegisteredClient.Builder registeredClientBuilder = TestRegisteredClients.registeredClient()
-				.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
-				.clientSecret("0123456789abcdef0123456789ABCDEF");
-		RegisteredClient registeredClient1 = registeredClientBuilder.clientSettings(
-				ClientSettings.builder().tokenEndpointSigningAlgorithm(MacAlgorithm.HS256).build()).build();
-		RegisteredClient registeredClient = registeredClientBuilder.clientSettings(
-				ClientSettings.builder().tokenEndpointSigningAlgorithm(MacAlgorithm.HS512).build()).build();
-
-		JwtDecoder decoder1 = this.registeredClientDecoderFactory.createDecoder(registeredClient1);
-		JwtDecoder decoder2 = this.registeredClientDecoderFactory.createDecoder(registeredClient);
-
-		assertThat(decoder1).isNotNull();
-		assertThat(decoder2).isNotNull();
-		assertThat(decoder1).isNotSameAs(decoder2);
-	}
-
-	@Test
-	public void createDecoderWithClientSecretJwtAndSecondWithChangedSecretThenReturnRecreatedDecoder() {
-		RegisteredClient.Builder registeredClientBuilder = TestRegisteredClients.registeredClient()
-				.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
-				.clientSettings(ClientSettings.builder().tokenEndpointSigningAlgorithm(MacAlgorithm.HS256).build());
-		RegisteredClient registeredClient1 = registeredClientBuilder.clientSecret("0123456789abcdef0123456789ABCDEF").build();
-		RegisteredClient registeredClient2 = registeredClientBuilder.clientSecret("0123456789ABCDEF0123456789abcdef").build();
-
-		JwtDecoder decoder1 = this.registeredClientDecoderFactory.createDecoder(registeredClient1);
-		JwtDecoder decoder2 = this.registeredClientDecoderFactory.createDecoder(registeredClient2);
-
-		assertThat(decoder1).isNotNull();
-		assertThat(decoder2).isNotNull();
-		assertThat(decoder1).isNotSameAs(decoder2);
-	}
-
-	@Test
-	public void createDecoderWithPrivateKeyJwtMissingJwksUrlThenThrowOAuth2AuthenticationException() {
-		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
-				.clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT)
-				.clientSettings(ClientSettings.builder()
-						.tokenEndpointSigningAlgorithm(SignatureAlgorithm.RS256).build())
-				.build();
-
-		assertThatThrownBy(() -> this.registeredClientDecoderFactory.createDecoder(registeredClient))
-				.isInstanceOf(OAuth2AuthenticationException.class);
-	}
-
-	@Test
-	public void createDecoderWithPrivateKeyJwtThenReturnDecoder() {
-		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
-				.clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT)
-				.clientSettings(ClientSettings.builder()
-						.tokenEndpointSigningAlgorithm(SignatureAlgorithm.RS256).jwkSetUrl("https://client.example.com/jwks").build())
-				.build();
-
-		JwtDecoder jwtDecoder = this.registeredClientDecoderFactory.createDecoder(registeredClient);
-
-		assertThat(jwtDecoder).isNotNull();
-	}
-
-	@Test
-	public void createDecoderWithPrivateKeyJwtAndSecondWithChangedAlgorithmThenReturnRecreatedDecoder() {
-		RegisteredClient.Builder registeredClientBuilder = TestRegisteredClients.registeredClient()
-				.clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT);
-
-		RegisteredClient registeredClient1 = registeredClientBuilder.clientSettings(
-				ClientSettings.builder().tokenEndpointSigningAlgorithm(SignatureAlgorithm.RS256)
-						.jwkSetUrl("https://keysite.com/jwks").build()).build();
-		RegisteredClient registeredClient2 = registeredClientBuilder.clientSettings(
-				ClientSettings.builder().tokenEndpointSigningAlgorithm(SignatureAlgorithm.RS512)
-						.jwkSetUrl("https://keysite.com/jwks").build()).build();
-
-		JwtDecoder decoder1 = this.registeredClientDecoderFactory.createDecoder(registeredClient1);
-		JwtDecoder decoder2 = this.registeredClientDecoderFactory.createDecoder(registeredClient2);
-
-		assertThat(decoder1).isNotNull();
-		assertThat(decoder2).isNotNull();
-		assertThat(decoder1).isNotSameAs(decoder2);
-	}
-
-	@Test
-	public void createDecoderWithPrivateKeyJwtAndSecondWithChangedJwksUrlThenReturnRecreatedDecoder() {
-		RegisteredClient.Builder registeredClientBuilder = TestRegisteredClients.registeredClient()
-				.clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT);
-
-		RegisteredClient client1 = registeredClientBuilder.clientSettings(
-				ClientSettings.builder().tokenEndpointSigningAlgorithm(SignatureAlgorithm.RS256)
-						.jwkSetUrl("https://keysite1.com/jwks").build()).build();
-		RegisteredClient client2 = registeredClientBuilder.clientSettings(
-				ClientSettings.builder().tokenEndpointSigningAlgorithm(SignatureAlgorithm.RS256)
-						.jwkSetUrl("https://keysite2.com/jwks").build()).build();
-
-		OAuth2ClientAuthenticationToken token = new OAuth2ClientAuthenticationToken(
-				"https://auth-server/oauth2/token", "client-1", ClientAuthenticationMethod.CLIENT_SECRET_JWT, "jwt", null);
-
-		JwtDecoder decoder1 = this.registeredClientDecoderFactory.createDecoder(client1);
-		JwtDecoder decoder2 = this.registeredClientDecoderFactory.createDecoder(client2);
-
-		assertThat(decoder1).isNotNull();
-		assertThat(decoder2).isNotNull();
-		assertThat(decoder1).isNotSameAs(decoder2);
-	}
-
-	@Test
-	public void createDecoderWithPrivateKeyJwtNullAlgorithmThenReturnDefaultRS256Decoder() {
-		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
-				.clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT)
-				.clientSettings(
-						ClientSettings.builder()
-								.jwkSetUrl("https://keysite1.com/jwks")
-								.build())
-				.build();
-
-		JwtDecoder decoder = this.registeredClientDecoderFactory.createDecoder(registeredClient);
-		assertThat(decoder).isNotNull();
-	}
-
-	@Test
-	public void validateClientSecretJwtTokenWhenValidThenReturnJwtObject() throws Exception {
-		RegisteredClient registeredClient = defaultRegisteredClient();
-		JwtDecoder jwtDecoder = this.registeredClientDecoderFactory.createDecoder(registeredClient);
-		String clientJwtAssertion = clientSecretJwtAssertion(registeredClient,
-				new JWTClaimsSet.Builder()
-						.issuer(registeredClient.getClientId())
-						.subject(registeredClient.getClientId())
-						.expirationTime(Date.from(Instant.now()))
-						.build());
-
-		assertThat(jwtDecoder.decode(clientJwtAssertion)).isNotNull();
-	}
-
-	@Test
-	public void validateClientSecretJwtTokenWhenBadIssuerThenThrowJwtException() throws Exception {
-		RegisteredClient registeredClient = defaultRegisteredClient();
-		JwtDecoder jwtDecoder = this.registeredClientDecoderFactory.createDecoder(registeredClient);
-		String clientJwtAssertion = clientSecretJwtAssertion(registeredClient,
-				new JWTClaimsSet.Builder()
-						.issuer("bad-issuer")
-						.subject(registeredClient.getClientId())
-						.expirationTime(Date.from(Instant.now()))
-						.build());
-
-		assertThatThrownBy(() -> jwtDecoder.decode(clientJwtAssertion))
-				.isInstanceOf(JwtException.class)
-				.extracting("message")
-				.matches(s -> s.toString().contains("The iss claim is not valid"));
-	}
-
-	@Test
-	public void validateClientSecretJwtTokenWhenBadSubjectThenThrowJwtException() throws Exception {
-		RegisteredClient registeredClient = defaultRegisteredClient();
-		JwtDecoder jwtDecoder = this.registeredClientDecoderFactory.createDecoder(registeredClient);
-		String clientJwtAssertion = clientSecretJwtAssertion(registeredClient,
-				new JWTClaimsSet.Builder()
-						.issuer(registeredClient.getClientId())
-						.subject("bad-client")
-						.expirationTime(Date.from(Instant.now()))
-						.build());
-
-		assertThatThrownBy(() -> jwtDecoder.decode(clientJwtAssertion))
-				.isInstanceOf(JwtException.class)
-				.extracting("message")
-				.matches(s -> s.toString().contains("The sub claim is not valid"));
-	}
-
-	@Test
-	public void validateClientSecretJwtTokenWhenNoExpClaimThenThrowJwtException() throws Exception {
-		RegisteredClient registeredClient = defaultRegisteredClient();
-		JwtDecoder jwtDecoder = this.registeredClientDecoderFactory.createDecoder(registeredClient);
-		String clientJwtAssertion = clientSecretJwtAssertion(registeredClient,
-				new JWTClaimsSet.Builder()
-						.subject(registeredClient.getClientId())
-						.issuer(registeredClient.getClientId())
-						.build());
-
-		assertThatThrownBy(() -> jwtDecoder.decode(clientJwtAssertion))
-				.isInstanceOf(JwtException.class)
-				.extracting("message")
-				.matches(s -> s.toString().contains("The exp claim is not valid"));
-	}
-
-	@Test
-	public void validateClientSecretJwtTokenWhenExpiredThenThrowJwtException() throws Exception {
-		RegisteredClient registeredClient = defaultRegisteredClient();
-		JwtDecoder jwtDecoder = this.registeredClientDecoderFactory.createDecoder(registeredClient);
-		String clientJwtAssertion = clientSecretJwtAssertion(registeredClient,
-				new JWTClaimsSet.Builder()
-						.subject(registeredClient.getClientId())
-						.issuer(registeredClient.getClientId())
-						.expirationTime(Date.from(Instant.now().minusSeconds(240)))
-						.build());
-
-		assertThatThrownBy(() -> jwtDecoder.decode(clientJwtAssertion))
-				.isInstanceOf(JwtException.class)
-				.extracting("message")
-				.matches(s -> s.toString().contains("Jwt expired at"));
-	}
-
-	@Test
-	public void validateClientSecretJwtTokenWhenExpiredWithinSkewThenReturnJwtObject() throws Exception {
-		RegisteredClient registeredClient = defaultRegisteredClient();
-		JwtDecoder jwtDecoder = this.registeredClientDecoderFactory.createDecoder(registeredClient);
-		String clientJwtAssertion = clientSecretJwtAssertion(registeredClient,
-				new JWTClaimsSet.Builder()
-						.subject(registeredClient.getClientId())
-						.issuer(registeredClient.getClientId())
-						.expirationTime(Date.from(Instant.now().minusSeconds(30)))
-						.build());
-
-		assertThat(jwtDecoder.decode(clientJwtAssertion)).isNotNull();
-	}
-
-	@Test
-	public void validateClientSecretJwtTokenWhenInvalidNbfThenThrowJwtException() throws Exception {
-		RegisteredClient registeredClient = defaultRegisteredClient();
-		JwtDecoder jwtDecoder = this.registeredClientDecoderFactory.createDecoder(registeredClient);
-		String clientJwtAssertion = clientSecretJwtAssertion(registeredClient,
-				new JWTClaimsSet.Builder()
-						.subject(registeredClient.getClientId())
-						.issuer(registeredClient.getClientId())
-						.expirationTime(Date.from(Instant.now()))
-						.notBeforeTime(Date.from(Instant.now().plusSeconds(90)))
-						.build());
-
-		assertThatThrownBy(() -> jwtDecoder.decode(clientJwtAssertion))
-				.isInstanceOf(JwtException.class)
-				.extracting("message")
-				.matches(s -> s.toString().contains("Jwt used before"));
-	}
-
-	@Test
-	public void validateClientSecretJwtTokenWhenInvalidIatThenThrowJwtException() throws Exception {
-		RegisteredClient registeredClient = defaultRegisteredClient();
-		JwtDecoder jwtDecoder = this.registeredClientDecoderFactory.createDecoder(registeredClient);
-		String clientJwtAssertion = clientSecretJwtAssertion(registeredClient,
-				new JWTClaimsSet.Builder()
-						.subject(registeredClient.getClientId())
-						.issuer(registeredClient.getClientId())
-						.expirationTime(Date.from(Instant.now()))
-						.issueTime(Date.from(Instant.now().plusSeconds(90)))
-						.build());
-
-		assertThatThrownBy(() -> jwtDecoder.decode(clientJwtAssertion))
-				.isInstanceOf(JwtException.class)
-				.extracting("message")
-				.matches(s -> s.toString().contains("expiresAt must be after issuedAt"));
-	}
-
-	@Test
-	public void validatePrivateKeyJwtTokenWhenValidThenReturnJwtObject() throws Exception {
-		KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
-		gen.initialize(2048);
-		KeyPair keyPair = gen.generateKeyPair();
-
-		JWK jwk = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic())
-				.keyUse(KeyUse.SIGNATURE)
-				.keyID(UUID.randomUUID().toString())
-				.build();
-
-		String jwks = "{\"keys\":[" + jwk.toJSONString() + "]}";
-
-		try (MockWebServer server = new MockWebServer()) {
-			String jwkSetUrl = server.url("/.well-known/jwks.json").toString();
-			server.enqueue(new MockResponse().setBody(jwks));
-
-			RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
-					.clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT)
-					.clientSettings(ClientSettings.builder()
-							.tokenEndpointSigningAlgorithm(SignatureAlgorithm.RS256)
-							.jwkSetUrl(jwkSetUrl).build())
-					.build();
-
-			JwtDecoder jwtDecoder = this.registeredClientDecoderFactory.createDecoder(registeredClient);
-
-			JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
-					.issuer(registeredClient.getClientId())
-					.subject(registeredClient.getClientId())
-					.expirationTime(Date.from(Instant.now()))
-					.build();
-			SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), claimsSet);
-			JWSSigner signer = new RSASSASigner(keyPair.getPrivate());
-			signedJWT.sign(signer);
-			String clientJwtAssertion = signedJWT.serialize();
-
-			assertThat(jwtDecoder.decode(clientJwtAssertion)).isNotNull();
-
-			server.shutdown();
-		}
-
-	}
-
-	private RegisteredClient defaultRegisteredClient() {
-		return TestRegisteredClients.registeredClient()
-				.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
-				.clientSecret("0123456789abcdef0123456789ABCDEF")
-				.clientSettings(ClientSettings.builder().tokenEndpointSigningAlgorithm(MacAlgorithm.HS256).build())
-				.build();
-	}
-
-	private String clientSecretJwtAssertion(RegisteredClient registeredClient, JWTClaimsSet claimsSet) throws JOSEException {
-		SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), claimsSet);
-		JWSSigner signer = new MACSigner(registeredClient.getClientSecret().getBytes(StandardCharsets.UTF_8));
-		signedJWT.sign(signer);
-		String clientJwtAssertion = signedJWT.serialize();
-		return clientJwtAssertion;
-	}
-}

+ 2 - 9
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/client/JdbcRegisteredClientRepositoryTests.java

@@ -143,17 +143,10 @@ public class JdbcRegisteredClientRepositoryTests {
 
 	@Test
 	public void saveWhenNewThenSaved() {
-		RegisteredClient expectedRegisteredClient = TestRegisteredClients.registeredClient().build();
-		this.registeredClientRepository.save(expectedRegisteredClient);
-		RegisteredClient registeredClient = this.registeredClientRepository.findById(expectedRegisteredClient.getId());
-		assertThat(registeredClient).isEqualTo(expectedRegisteredClient);
-	}
-
-	@Test
-	public void saveWhenCustomTokenEndpointSigningAlgorithmsThenSaved() {
 		RegisteredClient expectedRegisteredClient = TestRegisteredClients.registeredClient()
 				.clientSettings(ClientSettings.builder()
-						.tokenEndpointSigningAlgorithm(MacAlgorithm.HS256).build())
+						.tokenEndpointAuthenticationSigningAlgorithm(MacAlgorithm.HS256).build()
+				)
 				.build();
 		this.registeredClientRepository.save(expectedRegisteredClient);
 		RegisteredClient registeredClient = this.registeredClientRepository.findById(expectedRegisteredClient.getId());

+ 9 - 10
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/ClientSettingsTests.java

@@ -16,8 +16,8 @@
 package org.springframework.security.oauth2.server.authorization.config;
 
 import org.junit.Test;
+
 import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
-import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
@@ -31,10 +31,9 @@ public class ClientSettingsTests {
 	@Test
 	public void buildWhenDefaultThenDefaultsAreSet() {
 		ClientSettings clientSettings = ClientSettings.builder().build();
-		assertThat(clientSettings.getSettings()).hasSize(3);
+		assertThat(clientSettings.getSettings()).hasSize(2);
 		assertThat(clientSettings.isRequireProofKey()).isFalse();
 		assertThat(clientSettings.isRequireAuthorizationConsent()).isFalse();
-		assertThat(clientSettings.getTokenEndpointSigningAlgorithm()).isEqualTo(SignatureAlgorithm.RS256);
 	}
 
 	@Test
@@ -54,19 +53,19 @@ public class ClientSettingsTests {
 	}
 
 	@Test
-	public void tokenEndpointAlgorithmWhenHS256ThenSet() {
+	public void tokenEndpointAuthenticationSigningAlgorithmWhenHS256ThenSet() {
 		ClientSettings clientSettings = ClientSettings.builder()
-				.tokenEndpointSigningAlgorithm(MacAlgorithm.HS256)
+				.tokenEndpointAuthenticationSigningAlgorithm(MacAlgorithm.HS256)
 				.build();
-		assertThat(clientSettings.getTokenEndpointSigningAlgorithm()).isEqualTo(MacAlgorithm.HS256);
+		assertThat(clientSettings.getTokenEndpointAuthenticationSigningAlgorithm()).isEqualTo(MacAlgorithm.HS256);
 	}
 
 	@Test
-	public void whenJwkSetUrlSetThenSet() {
+	public void jwkSetUrlWhenProvidedThenSet() {
 		ClientSettings clientSettings = ClientSettings.builder()
-				.jwkSetUrl("https://auth-server:9000/jwks")
+				.jwkSetUrl("https://client.example.com/jwks")
 				.build();
-		assertThat(clientSettings.getJwkSetUrl()).isEqualTo("https://auth-server:9000/jwks");
+		assertThat(clientSettings.getJwkSetUrl()).isEqualTo("https://client.example.com/jwks");
 	}
 
 	@Test
@@ -75,7 +74,7 @@ public class ClientSettingsTests {
 				.setting("name1", "value1")
 				.settings(settings -> settings.put("name2", "value2"))
 				.build();
-		assertThat(clientSettings.getSettings()).hasSize(5);
+		assertThat(clientSettings.getSettings()).hasSize(4);
 		assertThat(clientSettings.<String>getSetting("name1")).isEqualTo("value1");
 		assertThat(clientSettings.<String>getSetting("name2")).isEqualTo("value2");
 	}

+ 150 - 164
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProviderTests.java

@@ -37,6 +37,7 @@ import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
 import org.springframework.security.oauth2.core.OAuth2TokenType;
 import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
 import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.core.oidc.OidcClientMetadataClaimNames;
 import org.springframework.security.oauth2.core.oidc.OidcClientRegistration;
 import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
 import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
@@ -52,6 +53,7 @@ import org.springframework.security.oauth2.server.authorization.TestOAuth2Author
 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 org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
 import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
 import org.springframework.web.util.UriComponentsBuilder;
@@ -286,8 +288,11 @@ public class OidcClientRegistrationAuthenticationProviderTests {
 
 		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
 				.isInstanceOf(OAuth2AuthenticationException.class)
-				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError()).extracting("errorCode")
-				.isEqualTo("invalid_redirect_uri");
+				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
+				.satisfies(error -> {
+					assertThat(error.getErrorCode()).isEqualTo("invalid_redirect_uri");
+					assertThat(error.getDescription()).contains(OidcClientMetadataClaimNames.REDIRECT_URIS);
+				});
 		verify(this.authorizationService).findByToken(
 				eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN));
 	}
@@ -318,12 +323,139 @@ public class OidcClientRegistrationAuthenticationProviderTests {
 
 		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
 				.isInstanceOf(OAuth2AuthenticationException.class)
-				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError()).extracting("errorCode")
-				.isEqualTo("invalid_redirect_uri");
+				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
+				.satisfies(error -> {
+					assertThat(error.getErrorCode()).isEqualTo("invalid_redirect_uri");
+					assertThat(error.getDescription()).contains(OidcClientMetadataClaimNames.REDIRECT_URIS);
+				});
 		verify(this.authorizationService).findByToken(
 				eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN));
 	}
 
+	@Test
+	public void authenticateWhenClientRegistrationRequestAndInvalidTokenEndpointAuthenticationMethodThenThrowOAuth2AuthenticationException() {
+		Jwt jwt = createJwtClientRegistration();
+		OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
+				jwt.getTokenValue(), jwt.getIssuedAt(),
+				jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(
+				registeredClient, jwtAccessToken, jwt.getClaims()).build();
+		when(this.authorizationService.findByToken(
+				eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN)))
+				.thenReturn(authorization);
+
+		JwtAuthenticationToken principal = new JwtAuthenticationToken(
+				jwt, AuthorityUtils.createAuthorityList("SCOPE_client.create"));
+		// @formatter:off
+		OidcClientRegistration.Builder builder = OidcClientRegistration.builder()
+				.redirectUri("https://client.example.com");
+		// @formatter:on
+
+		String invalidClientMetadataErrorCode = "invalid_client_metadata";
+
+		// @formatter:off
+		builder
+				.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue())
+				.tokenEndpointAuthenticationSigningAlgorithm(MacAlgorithm.HS256.getName());
+		assertWhenClientRegistrationRequestInvalidThenThrowOAuth2AuthenticationException(principal, builder.build(),
+				invalidClientMetadataErrorCode, OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD);
+		// @formatter:on
+
+		// @formatter:off
+		builder
+				.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue())
+				.tokenEndpointAuthenticationSigningAlgorithm("none");
+		assertWhenClientRegistrationRequestInvalidThenThrowOAuth2AuthenticationException(principal, builder.build(),
+				invalidClientMetadataErrorCode, OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD);
+		// @formatter:on
+
+		// @formatter:off
+		builder
+				.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue())
+				.tokenEndpointAuthenticationSigningAlgorithm(SignatureAlgorithm.RS256.getName());
+		assertWhenClientRegistrationRequestInvalidThenThrowOAuth2AuthenticationException(principal, builder.build(),
+				invalidClientMetadataErrorCode, OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD);
+		// @formatter:on
+
+		// @formatter:off
+		builder
+				.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue())
+				.jwkSetUrl("https://client.example.com/jwks")
+				.tokenEndpointAuthenticationSigningAlgorithm(MacAlgorithm.HS256.getName());
+		assertWhenClientRegistrationRequestInvalidThenThrowOAuth2AuthenticationException(principal, builder.build(),
+				invalidClientMetadataErrorCode, OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD);
+		// @formatter:on
+
+		// @formatter:off
+		builder
+				.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue())
+				.tokenEndpointAuthenticationSigningAlgorithm(SignatureAlgorithm.RS256.getName());
+		assertWhenClientRegistrationRequestInvalidThenThrowOAuth2AuthenticationException(principal, builder.build(),
+				invalidClientMetadataErrorCode, OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD);
+		// @formatter:on
+	}
+
+	private void assertWhenClientRegistrationRequestInvalidThenThrowOAuth2AuthenticationException(
+			Authentication principal, OidcClientRegistration clientRegistration, String errorCode, String errorDescription) {
+
+		OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken(
+				principal, clientRegistration);
+
+		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
+				.isInstanceOf(OAuth2AuthenticationException.class)
+				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
+				.satisfies(error -> {
+					assertThat(error.getErrorCode()).isEqualTo(errorCode);
+					assertThat(error.getDescription()).contains(errorDescription);
+				});
+	}
+
+	@Test
+	public void authenticateWhenClientRegistrationRequestAndTokenEndpointAuthenticationSigningAlgorithmNotProvidedThenDefaults() {
+		Jwt jwt = createJwtClientRegistration();
+		OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
+				jwt.getTokenValue(), jwt.getIssuedAt(),
+				jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
+		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
+		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(
+				registeredClient, jwtAccessToken, jwt.getClaims()).build();
+		when(this.authorizationService.findByToken(
+				eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN)))
+				.thenReturn(authorization);
+		when(this.jwtEncoder.encode(any(), any())).thenReturn(createJwtClientConfiguration());
+
+		JwtAuthenticationToken principal = new JwtAuthenticationToken(
+				jwt, AuthorityUtils.createAuthorityList("SCOPE_client.create"));
+		// @formatter:off
+		OidcClientRegistration.Builder builder = OidcClientRegistration.builder()
+				.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
+				.redirectUri("https://client.example.com")
+				.scope("scope1");
+		// @formatter:on
+
+		// @formatter:off
+		builder
+				.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue());
+		// @formatter:on
+		OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken(
+				principal, builder.build());
+		OidcClientRegistrationAuthenticationToken authenticationResult =
+				(OidcClientRegistrationAuthenticationToken) this.authenticationProvider.authenticate(authentication);
+		assertThat(authenticationResult.getClientRegistration().getTokenEndpointAuthenticationSigningAlgorithm())
+				.isEqualTo(MacAlgorithm.HS256.getName());
+
+		// @formatter:off
+		builder
+				.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue())
+				.jwkSetUrl("https://client.example.com/jwks");
+		// @formatter:on
+		authentication = new OidcClientRegistrationAuthenticationToken(principal, builder.build());
+		authenticationResult = (OidcClientRegistrationAuthenticationToken) this.authenticationProvider.authenticate(authentication);
+		assertThat(authenticationResult.getClientRegistration().getTokenEndpointAuthenticationSigningAlgorithm())
+				.isEqualTo(SignatureAlgorithm.RS256.getName());
+	}
+
 	@Test
 	public void authenticateWhenClientRegistrationRequestAndValidAccessTokenThenReturnClientRegistration() {
 		Jwt jwt = createJwtClientRegistration();
@@ -425,165 +557,6 @@ public class OidcClientRegistrationAuthenticationProviderTests {
 		assertThat(clientRegistrationResult.getRegistrationAccessToken()).isEqualTo(jwt.getTokenValue());
 	}
 
-	private OidcClientRegistrationAuthenticationToken jwtClientAuthenticationRegistration(
-			String tokenAuthenticationMethod, String tokenSigningAlgorithm, String jwkSetUrl) {
-		Jwt jwt = createJwtClientRegistration();
-		OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
-				jwt.getTokenValue(), jwt.getIssuedAt(),
-				jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
-		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
-		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(
-				registeredClient, jwtAccessToken, jwt.getClaims()).build();
-		when(this.authorizationService.findByToken(
-				eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN)))
-				.thenReturn(authorization);
-		when(this.jwtEncoder.encode(any(), any())).thenReturn(createJwtClientConfiguration());
-
-		JwtAuthenticationToken principal = new JwtAuthenticationToken(
-				jwt, AuthorityUtils.createAuthorityList("SCOPE_client.create"));
-		// @formatter:off
-		OidcClientRegistration.Builder clientRegistrationBuilder = OidcClientRegistration.builder()
-				.clientId("client-id")
-				.clientName("client-name")
-				.redirectUri("https://client.example.com")
-				.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
-				.scope("scope1")
-				.tokenEndpointAuthenticationMethod(tokenAuthenticationMethod);
-		// @formatter:on
-
-		if (tokenSigningAlgorithm != null) {
-			clientRegistrationBuilder = clientRegistrationBuilder.tokenEndpointAuthenticationSigningAlgorithm(tokenSigningAlgorithm);
-		}
-
-		if (jwkSetUrl != null) {
-			clientRegistrationBuilder = clientRegistrationBuilder.jwkSetUrl(jwkSetUrl);
-		}
-
-		return new OidcClientRegistrationAuthenticationToken(principal, clientRegistrationBuilder.build());
-	}
-
-	@Test
-	public void authenticateWhenClientRegistrationRequestAndPrivateKeyJwtAndAlgorithmNoneThenThrowOAuth2AuthenticationException() {
-		OidcClientRegistrationAuthenticationToken authentication = jwtClientAuthenticationRegistration(
-				ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue(), "none", "https://client.example.com/jwks");
-
-		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
-				.isInstanceOf(OAuth2AuthenticationException.class)
-				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError()).extracting("errorCode")
-				.isEqualTo("invalid_client_metadata");
-	}
-
-	@Test
-	public void authenticateWhenClientRegistrationRequestAndPrivateKeyJwtAndMacAlgorithmThenThrowOAuth2AuthenticationException() {
-		OidcClientRegistrationAuthenticationToken authentication = jwtClientAuthenticationRegistration(
-				ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue(), "HS256", "https://client.example.com/jwks");
-
-		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
-				.isInstanceOf(OAuth2AuthenticationException.class)
-				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError()).extracting("errorCode")
-				.isEqualTo("invalid_client_metadata");
-	}
-
-	@Test
-	public void authenticateWhenClientRegistrationRequestAndPrivateKeyJwtAndNoJwkSetUrlThenThrowOAuth2AuthenticationException() {
-		OidcClientRegistrationAuthenticationToken authentication = jwtClientAuthenticationRegistration(
-				ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue(), "RS256", null);
-
-		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
-				.isInstanceOf(OAuth2AuthenticationException.class)
-				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError()).extracting("errorCode")
-				.isEqualTo("invalid_client_metadata");
-	}
-
-	@Test
-	public void authenticateWhenClientRegistrationRequestAndClientSecretJwtAndPkiAlgorithmThenThrowOAuth2AuthenticationException() {
-		OidcClientRegistrationAuthenticationToken authentication = jwtClientAuthenticationRegistration(
-				ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue(), "RS256", "https://client.example.com/jwks");
-
-		assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
-				.isInstanceOf(OAuth2AuthenticationException.class)
-				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError()).extracting("errorCode")
-				.isEqualTo("invalid_client_metadata");
-	}
-
-	@Test
-	public void authenticateWhenClientRegistrationAndProperClientSecretJwtRegistrationThenRegistered() {
-		OidcClientRegistrationAuthenticationToken authentication = jwtClientAuthenticationRegistration(
-				ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue(), "HS512", null);
-
-		Authentication authenticationResult = this.authenticationProvider.authenticate(authentication);
-
-		assertThat(authenticationResult).isNotNull();
-
-		ArgumentCaptor<RegisteredClient> registeredClientCaptor = ArgumentCaptor.forClass(RegisteredClient.class);
-		verify(this.registeredClientRepository).save(registeredClientCaptor.capture());
-		RegisteredClient registeredClientResult = registeredClientCaptor.getValue();
-
-		assertThat(registeredClientResult).isNotNull();
-		assertThat(registeredClientResult.getClientSecret()).hasSizeGreaterThan(32);
-		assertThat(registeredClientResult.getClientSettings().getTokenEndpointSigningAlgorithm()).isEqualTo(MacAlgorithm.HS512);
-	}
-
-	@Test
-	public void authenticateWhenClientRegistrationAndClientSecretJwtAndNullAlgorithmThenDefaultAlgorithmHS256() {
-		OidcClientRegistrationAuthenticationToken authentication = jwtClientAuthenticationRegistration(
-				ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue(), null, null);
-
-		Authentication authenticationResult = this.authenticationProvider.authenticate(authentication);
-
-		assertThat(authenticationResult).isNotNull();
-
-		ArgumentCaptor<RegisteredClient> registeredClientCaptor = ArgumentCaptor.forClass(RegisteredClient.class);
-		verify(this.registeredClientRepository).save(registeredClientCaptor.capture());
-		RegisteredClient registeredClientResult = registeredClientCaptor.getValue();
-
-		assertThat(registeredClientResult).isNotNull();
-		assertThat(registeredClientResult.getClientSecret()).hasSizeGreaterThan(32);
-		assertThat(registeredClientResult.getClientSettings().getTokenEndpointSigningAlgorithm()).isEqualTo(MacAlgorithm.HS256);
-		assertThat(registeredClientResult.getClientAuthenticationMethods()).contains(ClientAuthenticationMethod.CLIENT_SECRET_JWT);
-	}
-
-	@Test
-	public void authenticateWhenClientRegistrationAndProperPrivateKeyJwtRegistrationThenRegistered() {
-		OidcClientRegistrationAuthenticationToken authentication = jwtClientAuthenticationRegistration(
-				ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue(), "RS512", "https://client.example.com/jwks");
-
-		OidcClientRegistrationAuthenticationToken authenticationResult =
-				(OidcClientRegistrationAuthenticationToken) this.authenticationProvider.authenticate(authentication);
-
-		assertThat(authenticationResult).isNotNull();
-
-		ArgumentCaptor<RegisteredClient> registeredClientCaptor = ArgumentCaptor.forClass(RegisteredClient.class);
-		verify(this.registeredClientRepository).save(registeredClientCaptor.capture());
-		RegisteredClient registeredClientResult = registeredClientCaptor.getValue();
-
-		assertThat(registeredClientResult).isNotNull();
-		assertThat(registeredClientResult.getClientSettings().getJwkSetUrl()).isEqualTo("https://client.example.com/jwks");
-		assertThat(registeredClientResult.getClientSettings().getTokenEndpointSigningAlgorithm()).isEqualTo(SignatureAlgorithm.RS512);
-
-		assertThat(authenticationResult.getClientRegistration().getTokenEndpointAuthenticationSigningAlgorithm()).isEqualTo("RS512");
-		assertThat(authenticationResult.getClientRegistration().getJwkSetUrl().toString()).isEqualTo("https://client.example.com/jwks");
-		assertThat(registeredClientResult.getClientAuthenticationMethods()).contains(ClientAuthenticationMethod.PRIVATE_KEY_JWT);
-	}
-
-	@Test
-	public void authenticateWhenClientRegistrationAndPrivateKeyJwtAndNullAlgorithmThenDefaultAlgorithmRS256() {
-		OidcClientRegistrationAuthenticationToken authentication = jwtClientAuthenticationRegistration(
-				ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue(), null, "https://client.example.com/jwks");
-
-		Authentication authenticationResult = this.authenticationProvider.authenticate(authentication);
-
-		assertThat(authenticationResult).isNotNull();
-
-		ArgumentCaptor<RegisteredClient> registeredClientCaptor = ArgumentCaptor.forClass(RegisteredClient.class);
-		verify(this.registeredClientRepository).save(registeredClientCaptor.capture());
-		RegisteredClient registeredClientResult = registeredClientCaptor.getValue();
-
-		assertThat(registeredClientResult).isNotNull();
-		assertThat(registeredClientResult.getClientSettings().getJwkSetUrl()).isEqualTo("https://client.example.com/jwks");
-		assertThat(registeredClientResult.getClientSettings().getTokenEndpointSigningAlgorithm()).isEqualTo(SignatureAlgorithm.RS256);
-	}
-
 	@Test
 	public void authenticateWhenClientConfigurationRequestAndAccessTokenNotAuthorizedThenThrowOAuth2AuthenticationException() {
 		Jwt jwt = createJwt(Collections.singleton("unauthorized.scope"));
@@ -707,7 +680,16 @@ public class OidcClientRegistrationAuthenticationProviderTests {
 				jwt.getTokenValue(), jwt.getIssuedAt(),
 				jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
 		RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
-				.clientName("client-name")
+				.clientAuthenticationMethods((clientAuthenticationMethods) -> {
+					clientAuthenticationMethods.clear();
+					clientAuthenticationMethods.add(ClientAuthenticationMethod.PRIVATE_KEY_JWT);
+				})
+				.clientSettings(
+						ClientSettings.builder()
+								.tokenEndpointAuthenticationSigningAlgorithm(SignatureAlgorithm.RS512)
+								.jwkSetUrl("https://client.example.com/jwks")
+								.build()
+				)
 				.build();
 		OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(
 				registeredClient, jwtAccessToken, jwt.getClaims()).build();
@@ -755,6 +737,10 @@ public class OidcClientRegistrationAuthenticationProviderTests {
 				.containsExactlyInAnyOrderElementsOf(registeredClient.getScopes());
 		assertThat(clientRegistrationResult.getTokenEndpointAuthenticationMethod())
 				.isEqualTo(registeredClient.getClientAuthenticationMethods().iterator().next().getValue());
+		assertThat(clientRegistrationResult.getTokenEndpointAuthenticationSigningAlgorithm())
+				.isEqualTo(registeredClient.getClientSettings().getTokenEndpointAuthenticationSigningAlgorithm().getName());
+		assertThat(clientRegistrationResult.getJwkSetUrl().toString())
+				.isEqualTo(registeredClient.getClientSettings().getJwkSetUrl());
 		assertThat(clientRegistrationResult.getIdTokenSignedResponseAlgorithm())
 				.isEqualTo(registeredClient.getTokenSettings().getIdTokenSignatureAlgorithm().getName());
 

+ 0 - 1
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilterTests.java

@@ -15,7 +15,6 @@
  */
 package org.springframework.security.oauth2.server.authorization.web;
 
-
 import javax.servlet.FilterChain;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;

+ 46 - 66
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/authentication/JwtClientAssertionAuthenticationConverterTests.java

@@ -13,45 +13,35 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package org.springframework.security.oauth2.server.authorization.web.authentication;
 
 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.*;
+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 JwtClientAssertionAuthenticationConverter}
+ * Tests for {@link JwtClientAssertionAuthenticationConverter}.
  *
  * @author Rafal Lewczuk
  */
 public class JwtClientAssertionAuthenticationConverterTests {
-
-	private JwtClientAssertionAuthenticationConverter converter = new JwtClientAssertionAuthenticationConverter();
-
 	private static final String JWT_BEARER_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
-
-	private void shouldThrow(MockHttpServletRequest request, String errorCode) {
-		assertThatThrownBy(() -> this.converter.convert(request))
-				.isInstanceOf(OAuth2AuthenticationException.class)
-				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
-				.extracting("errorCode")
-				.isEqualTo(errorCode);
-	}
+	private final JwtClientAssertionAuthenticationConverter converter = new JwtClientAssertionAuthenticationConverter();
 
 	@Test
-	public void convertWhenClientAssertionTypeNullThenReturnNull() {
+	public void convertWhenMissingClientAssertionTypeThenReturnNull() {
 		MockHttpServletRequest request = new MockHttpServletRequest();
-		request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION, "some_jwt_assertion");
+		request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION, "jwt-assertion");
 		Authentication authentication = this.converter.convert(request);
 		assertThat(authentication).isNull();
 	}
@@ -65,84 +55,74 @@ public class JwtClientAssertionAuthenticationConverterTests {
 	}
 
 	@Test
-	public void convertWhenMissingClientIdThenInvalidRequestError() {
-		MockHttpServletRequest request = new MockHttpServletRequest();
-		request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, JWT_BEARER_TYPE);
-		request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION, "some_jwt_assertion");
-		shouldThrow(request, OAuth2ErrorCodes.INVALID_REQUEST);
-	}
-
-	@Test
-	public void convertWhenMultipleClientIdThenInvalidRequestError() {
+	public void convertWhenMultipleClientAssertionTypeThenInvalidRequestError() {
 		MockHttpServletRequest request = new MockHttpServletRequest();
-		request.addParameter(OAuth2ParameterNames.CLIENT_ID, "some_client");
-		request.addParameter(OAuth2ParameterNames.CLIENT_ID, "other_client");
 		request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, JWT_BEARER_TYPE);
-		request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION, "some_jwt_assertion");
-		shouldThrow(request, OAuth2ErrorCodes.INVALID_REQUEST);
+		request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, "other-client-assertion-type");
+		request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION, "jwt-assertion");
+		assertThrown(request, OAuth2ErrorCodes.INVALID_REQUEST);
 	}
 
 	@Test
-	public void convertWhenBadAssertionTypeThenInvalidRequestError() {
+	public void convertWhenNotJwtAssertionTypeThenReturnNull() {
 		MockHttpServletRequest request = new MockHttpServletRequest();
-		request.addParameter(OAuth2ParameterNames.CLIENT_ID, "some_client");
-		request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, "borken");
-		request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION, "some_jwt_assertion");
-		shouldThrow(request, OAuth2ErrorCodes.INVALID_REQUEST);
+		request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, "other-client-assertion-type");
+		request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION, "other-assertion");
+		Authentication authentication = this.converter.convert(request);
+		assertThat(authentication).isNull();
 	}
 
 	@Test
-	public void convertWhenMissingClientJwtAssertionTypeThenDoNotProcessClientIdAndReturnNull() {
+	public void convertWhenMultipleClientAssertionThenInvalidRequestError() {
 		MockHttpServletRequest request = new MockHttpServletRequest();
-		request.addParameter(OAuth2ParameterNames.CLIENT_ID, "some_client");
-		request.addParameter(OAuth2ParameterNames.CLIENT_ID, "throw_something_when_client_id_is_processed");
-		Authentication authentication = this.converter.convert(request);
-		assertThat(authentication).isNull();
+		request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, JWT_BEARER_TYPE);
+		request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION, "jwt-assertion");
+		request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION, "other-jwt-assertion");
+		assertThrown(request, OAuth2ErrorCodes.INVALID_REQUEST);
 	}
 
 	@Test
-	public void convertWhenMultipleAssertionsThenInvalidRequestError() {
+	public void convertWhenMissingClientIdThenInvalidRequestError() {
 		MockHttpServletRequest request = new MockHttpServletRequest();
-		request.addParameter(OAuth2ParameterNames.CLIENT_ID, "some_client");
 		request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, JWT_BEARER_TYPE);
-		request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION, "some_jwt_assertion");
-		request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION, "other_jwt_assertion");
-		shouldThrow(request, OAuth2ErrorCodes.INVALID_REQUEST);
+		request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION, "jwt-assertion");
+		assertThrown(request, OAuth2ErrorCodes.INVALID_REQUEST);
 	}
 
 	@Test
-	public void convertWhenValidAssertionJwt() {
+	public void convertWhenMultipleClientIdThenInvalidRequestError() {
 		MockHttpServletRequest request = new MockHttpServletRequest();
-		request.addParameter(OAuth2ParameterNames.CLIENT_ID, "some_client");
 		request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, JWT_BEARER_TYPE);
-		request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION, "some_jwt_assertion");
-		request.setRequestURI("/oauth2/token");
-		OAuth2ClientAuthenticationToken authentication = (OAuth2ClientAuthenticationToken) this.converter.convert(request);
-		assertThat(authentication).isNotNull();
-		assertThat(authentication.getRequestUri()).isEqualTo("/oauth2/token");
-		assertThat(authentication.getPrincipal()).isEqualTo("some_client");
-		assertThat(authentication.getCredentials()).isEqualTo("some_jwt_assertion");
+		request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION, "jwt-assertion");
+		request.addParameter(OAuth2ParameterNames.CLIENT_ID, "client-1");
+		request.addParameter(OAuth2ParameterNames.CLIENT_ID, "client-2");
+		assertThrown(request, OAuth2ErrorCodes.INVALID_REQUEST);
 	}
 
 	@Test
-	public void convertWhenConfidentialClientWithPkceParametersThenAdditionalParametersIncluded() {
+	public void convertWhenJwtAssertionThenReturnClientAuthenticationToken() {
 		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, JWT_BEARER_TYPE);
+		request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION, "jwt-assertion");
+		request.addParameter(OAuth2ParameterNames.CLIENT_ID, "client-1");
 		request.addParameter(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
 		request.addParameter(OAuth2ParameterNames.CODE, "code");
-		request.addParameter(PkceParameterNames.CODE_VERIFIER, "code-verifier-1");
-		request.addParameter(OAuth2ParameterNames.CLIENT_ID, "some_client");
-		request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, JWT_BEARER_TYPE);
-		request.addParameter(OAuth2ParameterNames.CLIENT_ASSERTION, "some_jwt_assertion");
-		request.setRequestURI("/oauth2/token");
 		OAuth2ClientAuthenticationToken authentication = (OAuth2ClientAuthenticationToken) this.converter.convert(request);
-		assertThat(authentication).isNotNull();
-		assertThat(authentication.getRequestUri()).isEqualTo("/oauth2/token");
-		assertThat(authentication.getPrincipal()).isEqualTo("some_client");
-		assertThat(authentication.getCredentials()).isEqualTo("some_jwt_assertion");
+		assertThat(authentication.getPrincipal()).isEqualTo("client-1");
+		assertThat(authentication.getCredentials()).isEqualTo("jwt-assertion");
+		assertThat(authentication.getClientAuthenticationMethod().getValue()).isEqualTo(JWT_BEARER_TYPE);
 		assertThat(authentication.getAdditionalParameters())
 				.containsOnly(
 						entry(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue()),
-						entry(OAuth2ParameterNames.CODE, "code"),
-						entry(PkceParameterNames.CODE_VERIFIER, "code-verifier-1"));
+						entry(OAuth2ParameterNames.CODE, "code"));
 	}
+
+	private void assertThrown(MockHttpServletRequest request, String errorCode) {
+		assertThatThrownBy(() -> this.converter.convert(request))
+				.isInstanceOf(OAuth2AuthenticationException.class)
+				.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
+				.extracting("errorCode")
+				.isEqualTo(errorCode);
+	}
+
 }