Prechádzať zdrojové kódy

Support symmetric key for JwtDecoder

Fixes gh-5465
Joe Grandja 6 rokov pred
rodič
commit
bed3371b80
15 zmenil súbory, kde vykonal 1024 pridanie a 161 odobranie
  1. 92 11
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenDecoderFactory.java
  2. 90 7
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/ReactiveOidcIdTokenDecoderFactory.java
  3. 65 9
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenDecoderFactoryTests.java
  4. 65 9
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/ReactiveOidcIdTokenDecoderFactoryTests.java
  5. 33 0
      oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/JwsAlgorithm.java
  6. 77 0
      oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/MacAlgorithm.java
  7. 107 0
      oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/SignatureAlgorithm.java
  8. 87 30
      oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java
  9. 5 4
      oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupport.java
  10. 118 39
      oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java
  11. 32 0
      oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/TestKeys.java
  12. 41 0
      oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/jws/MacAlgorithmTests.java
  13. 47 0
      oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/jws/SignatureAlgorithmTests.java
  14. 80 22
      oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderTests.java
  15. 85 30
      oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoderTests.java

+ 92 - 11
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenDecoderFactory.java

@@ -15,15 +15,14 @@
  */
 package org.springframework.security.oauth2.client.oidc.authentication;
 
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.function.Function;
-
 import org.springframework.security.oauth2.client.registration.ClientRegistration;
 import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
 import org.springframework.security.oauth2.core.OAuth2Error;
 import org.springframework.security.oauth2.core.OAuth2TokenValidator;
 import org.springframework.security.oauth2.core.oidc.OidcIdToken;
+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.JwtDecoder;
 import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
@@ -31,7 +30,15 @@ import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
 import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
 
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+
 import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withJwkSetUri;
+import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withSecretKey;
 
 /**
  * A {@link JwtDecoderFactory factory} that provides a {@link JwtDecoder}
@@ -47,14 +54,45 @@ import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withJwkSe
  */
 public final class OidcIdTokenDecoderFactory implements JwtDecoderFactory<ClientRegistration> {
 	private static final String MISSING_SIGNATURE_VERIFIER_ERROR_CODE = "missing_signature_verifier";
+	private static Map<JwsAlgorithm, String> jcaAlgorithmMappings = new HashMap<JwsAlgorithm, String>() {
+		{
+			put(MacAlgorithm.HS256, "HmacSHA256");
+			put(MacAlgorithm.HS384, "HmacSHA384");
+			put(MacAlgorithm.HS512, "HmacSHA512");
+		}
+	};
 	private final Map<String, JwtDecoder> jwtDecoders = new ConcurrentHashMap<>();
 	private Function<ClientRegistration, OAuth2TokenValidator<Jwt>> jwtValidatorFactory = OidcIdTokenValidator::new;
+	private Function<ClientRegistration, JwsAlgorithm> jwsAlgorithmResolver = clientRegistration -> SignatureAlgorithm.RS256;
 
 	@Override
 	public JwtDecoder createDecoder(ClientRegistration clientRegistration) {
 		Assert.notNull(clientRegistration, "clientRegistration cannot be null");
 		return this.jwtDecoders.computeIfAbsent(clientRegistration.getRegistrationId(), key -> {
-			if (!StringUtils.hasText(clientRegistration.getProviderDetails().getJwkSetUri())) {
+			NimbusJwtDecoder jwtDecoder = buildDecoder(clientRegistration);
+			OAuth2TokenValidator<Jwt> jwtValidator = this.jwtValidatorFactory.apply(clientRegistration);
+			jwtDecoder.setJwtValidator(jwtValidator);
+			return jwtDecoder;
+		});
+	}
+
+	private NimbusJwtDecoder buildDecoder(ClientRegistration clientRegistration) {
+		JwsAlgorithm jwsAlgorithm = this.jwsAlgorithmResolver.apply(clientRegistration);
+		if (jwsAlgorithm != null && SignatureAlgorithm.class.isAssignableFrom(jwsAlgorithm.getClass())) {
+			// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
+			//
+			// 6. If the ID Token is received via direct communication between the Client
+			// and the Token Endpoint (which it is in this flow),
+			// the TLS server validation MAY be used to validate the issuer in place of checking the token signature.
+			// The Client MUST validate the signature of all other ID Tokens according to JWS [JWS]
+			// using the algorithm specified in the JWT alg Header Parameter.
+			// The Client MUST use the keys provided by the Issuer.
+			//
+			// 7. The alg value SHOULD be the default of RS256 or the algorithm sent by the Client
+			// in the id_token_signed_response_alg parameter during Registration.
+
+			String jwkSetUri = clientRegistration.getProviderDetails().getJwkSetUri();
+			if (!StringUtils.hasText(jwkSetUri)) {
 				OAuth2Error oauth2Error = new OAuth2Error(
 						MISSING_SIGNATURE_VERIFIER_ERROR_CODE,
 						"Failed to find a Signature Verifier for Client Registration: '" +
@@ -64,12 +102,42 @@ public final class OidcIdTokenDecoderFactory implements JwtDecoderFactory<Client
 				);
 				throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
 			}
-			String jwkSetUri = clientRegistration.getProviderDetails().getJwkSetUri();
-			NimbusJwtDecoder jwtDecoder = withJwkSetUri(jwkSetUri).build();
-			OAuth2TokenValidator<Jwt> jwtValidator = this.jwtValidatorFactory.apply(clientRegistration);
-			jwtDecoder.setJwtValidator(jwtValidator);
-			return jwtDecoder;
-		});
+			return withJwkSetUri(jwkSetUri).jwsAlgorithm(jwsAlgorithm).build();
+		} else if (jwsAlgorithm != null && MacAlgorithm.class.isAssignableFrom(jwsAlgorithm.getClass())) {
+			// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
+			//
+			// 8. If the JWT alg Header Parameter uses a MAC based algorithm such as HS256, HS384, or HS512,
+			// the octets of the UTF-8 representation of the client_secret
+			// corresponding to the client_id contained in the aud (audience) Claim
+			// are used as the key to validate the signature.
+			// For MAC based algorithms, the behavior is unspecified if the aud is multi-valued or
+			// if an azp value is present that is different than the aud value.
+
+			String clientSecret = clientRegistration.getClientSecret();
+			if (!StringUtils.hasText(clientSecret)) {
+				OAuth2Error oauth2Error = new OAuth2Error(
+						MISSING_SIGNATURE_VERIFIER_ERROR_CODE,
+						"Failed to find a Signature Verifier for Client Registration: '" +
+								clientRegistration.getRegistrationId() +
+								"'. Check to ensure you have configured the client secret.",
+						null
+				);
+				throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
+			}
+			SecretKeySpec secretKeySpec = new SecretKeySpec(
+					clientSecret.getBytes(StandardCharsets.UTF_8), jcaAlgorithmMappings.get(jwsAlgorithm));
+			return withSecretKey(secretKeySpec).macAlgorithm((MacAlgorithm) jwsAlgorithm).build();
+		}
+
+		OAuth2Error oauth2Error = new OAuth2Error(
+				MISSING_SIGNATURE_VERIFIER_ERROR_CODE,
+				"Failed to find a Signature Verifier for Client Registration: '" +
+						clientRegistration.getRegistrationId() +
+						"'. Check to ensure you have configured a valid JWS Algorithm: '" +
+						jwsAlgorithm + "'",
+				null
+		);
+		throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
 	}
 
 	/**
@@ -82,4 +150,17 @@ public final class OidcIdTokenDecoderFactory implements JwtDecoderFactory<Client
 		Assert.notNull(jwtValidatorFactory, "jwtValidatorFactory cannot be null");
 		this.jwtValidatorFactory = jwtValidatorFactory;
 	}
+
+	/**
+	 * Sets the resolver that provides the expected {@link JwsAlgorithm JWS algorithm}
+	 * used for the signature or MAC on the {@link OidcIdToken ID Token}.
+	 * The default resolves to {@link SignatureAlgorithm#RS256 RS256} for all {@link ClientRegistration clients}.
+	 *
+	 * @param jwsAlgorithmResolver the resolver that provides the expected {@link JwsAlgorithm JWS algorithm}
+	 *                             for a specific {@link ClientRegistration client}
+	 */
+	public final void setJwsAlgorithmResolver(Function<ClientRegistration, JwsAlgorithm> jwsAlgorithmResolver) {
+		Assert.notNull(jwsAlgorithmResolver, "jwsAlgorithmResolver cannot be null");
+		this.jwsAlgorithmResolver = jwsAlgorithmResolver;
+	}
 }

+ 90 - 7
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/ReactiveOidcIdTokenDecoderFactory.java

@@ -20,6 +20,9 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
 import org.springframework.security.oauth2.core.OAuth2Error;
 import org.springframework.security.oauth2.core.OAuth2TokenValidator;
 import org.springframework.security.oauth2.core.oidc.OidcIdToken;
+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.NimbusReactiveJwtDecoder;
 import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
@@ -27,10 +30,16 @@ import org.springframework.security.oauth2.jwt.ReactiveJwtDecoderFactory;
 import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
 
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Function;
 
+import static org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.withJwkSetUri;
+import static org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.withSecretKey;
+
 /**
  * A {@link ReactiveJwtDecoderFactory factory} that provides a {@link ReactiveJwtDecoder}
  * used for {@link OidcIdToken} signature verification.
@@ -45,14 +54,45 @@ import java.util.function.Function;
  */
 public final class ReactiveOidcIdTokenDecoderFactory implements ReactiveJwtDecoderFactory<ClientRegistration> {
 	private static final String MISSING_SIGNATURE_VERIFIER_ERROR_CODE = "missing_signature_verifier";
+	private static Map<JwsAlgorithm, String> jcaAlgorithmMappings = new HashMap<JwsAlgorithm, String>() {
+		{
+			put(MacAlgorithm.HS256, "HmacSHA256");
+			put(MacAlgorithm.HS384, "HmacSHA384");
+			put(MacAlgorithm.HS512, "HmacSHA512");
+		}
+	};
 	private final Map<String, ReactiveJwtDecoder> jwtDecoders = new ConcurrentHashMap<>();
 	private Function<ClientRegistration, OAuth2TokenValidator<Jwt>> jwtValidatorFactory = OidcIdTokenValidator::new;
+	private Function<ClientRegistration, JwsAlgorithm> jwsAlgorithmResolver = clientRegistration -> SignatureAlgorithm.RS256;
 
 	@Override
 	public ReactiveJwtDecoder createDecoder(ClientRegistration clientRegistration) {
 		Assert.notNull(clientRegistration, "clientRegistration cannot be null");
 		return this.jwtDecoders.computeIfAbsent(clientRegistration.getRegistrationId(), key -> {
-			if (!StringUtils.hasText(clientRegistration.getProviderDetails().getJwkSetUri())) {
+			NimbusReactiveJwtDecoder jwtDecoder = buildDecoder(clientRegistration);
+			OAuth2TokenValidator<Jwt> jwtValidator = this.jwtValidatorFactory.apply(clientRegistration);
+			jwtDecoder.setJwtValidator(jwtValidator);
+			return jwtDecoder;
+		});
+	}
+
+	private NimbusReactiveJwtDecoder buildDecoder(ClientRegistration clientRegistration) {
+		JwsAlgorithm jwsAlgorithm = this.jwsAlgorithmResolver.apply(clientRegistration);
+		if (jwsAlgorithm != null && SignatureAlgorithm.class.isAssignableFrom(jwsAlgorithm.getClass())) {
+			// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
+			//
+			// 6. If the ID Token is received via direct communication between the Client
+			// and the Token Endpoint (which it is in this flow),
+			// the TLS server validation MAY be used to validate the issuer in place of checking the token signature.
+			// The Client MUST validate the signature of all other ID Tokens according to JWS [JWS]
+			// using the algorithm specified in the JWT alg Header Parameter.
+			// The Client MUST use the keys provided by the Issuer.
+			//
+			// 7. The alg value SHOULD be the default of RS256 or the algorithm sent by the Client
+			// in the id_token_signed_response_alg parameter during Registration.
+
+			String jwkSetUri = clientRegistration.getProviderDetails().getJwkSetUri();
+			if (!StringUtils.hasText(jwkSetUri)) {
 				OAuth2Error oauth2Error = new OAuth2Error(
 						MISSING_SIGNATURE_VERIFIER_ERROR_CODE,
 						"Failed to find a Signature Verifier for Client Registration: '" +
@@ -62,12 +102,42 @@ public final class ReactiveOidcIdTokenDecoderFactory implements ReactiveJwtDecod
 				);
 				throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
 			}
-			NimbusReactiveJwtDecoder jwtDecoder = new NimbusReactiveJwtDecoder(
-					clientRegistration.getProviderDetails().getJwkSetUri());
-			OAuth2TokenValidator<Jwt> jwtValidator = this.jwtValidatorFactory.apply(clientRegistration);
-			jwtDecoder.setJwtValidator(jwtValidator);
-			return jwtDecoder;
-		});
+			return withJwkSetUri(jwkSetUri).jwsAlgorithm(jwsAlgorithm).build();
+		} else if (jwsAlgorithm != null && MacAlgorithm.class.isAssignableFrom(jwsAlgorithm.getClass())) {
+			// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
+			//
+			// 8. If the JWT alg Header Parameter uses a MAC based algorithm such as HS256, HS384, or HS512,
+			// the octets of the UTF-8 representation of the client_secret
+			// corresponding to the client_id contained in the aud (audience) Claim
+			// are used as the key to validate the signature.
+			// For MAC based algorithms, the behavior is unspecified if the aud is multi-valued or
+			// if an azp value is present that is different than the aud value.
+
+			String clientSecret = clientRegistration.getClientSecret();
+			if (!StringUtils.hasText(clientSecret)) {
+				OAuth2Error oauth2Error = new OAuth2Error(
+						MISSING_SIGNATURE_VERIFIER_ERROR_CODE,
+						"Failed to find a Signature Verifier for Client Registration: '" +
+								clientRegistration.getRegistrationId() +
+								"'. Check to ensure you have configured the client secret.",
+						null
+				);
+				throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
+			}
+			SecretKeySpec secretKeySpec = new SecretKeySpec(
+					clientSecret.getBytes(StandardCharsets.UTF_8), jcaAlgorithmMappings.get(jwsAlgorithm));
+			return withSecretKey(secretKeySpec).macAlgorithm((MacAlgorithm) jwsAlgorithm).build();
+		}
+
+		OAuth2Error oauth2Error = new OAuth2Error(
+				MISSING_SIGNATURE_VERIFIER_ERROR_CODE,
+				"Failed to find a Signature Verifier for Client Registration: '" +
+						clientRegistration.getRegistrationId() +
+						"'. Check to ensure you have configured a valid JWS Algorithm: '" +
+						jwsAlgorithm + "'",
+				null
+		);
+		throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
 	}
 
 	/**
@@ -80,4 +150,17 @@ public final class ReactiveOidcIdTokenDecoderFactory implements ReactiveJwtDecod
 		Assert.notNull(jwtValidatorFactory, "jwtValidatorFactory cannot be null");
 		this.jwtValidatorFactory = jwtValidatorFactory;
 	}
+
+	/**
+	 * Sets the resolver that provides the expected {@link JwsAlgorithm JWS algorithm}
+	 * used for the signature or MAC on the {@link OidcIdToken ID Token}.
+	 * The default resolves to {@link SignatureAlgorithm#RS256 RS256} for all {@link ClientRegistration clients}.
+	 *
+	 * @param jwsAlgorithmResolver the resolver that provides the expected {@link JwsAlgorithm JWS algorithm}
+	 *                             for a specific {@link ClientRegistration client}
+	 */
+	public final void setJwsAlgorithmResolver(Function<ClientRegistration, JwsAlgorithm> jwsAlgorithmResolver) {
+		Assert.notNull(jwsAlgorithmResolver, "jwsAlgorithmResolver cannot be null");
+		this.jwsAlgorithmResolver = jwsAlgorithmResolver;
+	}
 }

+ 65 - 9
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenDecoderFactoryTests.java

@@ -21,13 +21,15 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
 import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
 import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
 import org.springframework.security.oauth2.core.OAuth2TokenValidator;
+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 java.util.function.Function;
 
 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.Mockito.*;
 
 /**
@@ -42,8 +44,6 @@ public class OidcIdTokenDecoderFactoryTests {
 
 	private OidcIdTokenDecoderFactory idTokenDecoderFactory;
 
-	private Function<ClientRegistration, OAuth2TokenValidator<Jwt>> defaultJwtValidatorFactory = OidcIdTokenValidator::new;
-
 	@Before
 	public void setUp() {
 		this.idTokenDecoderFactory = new OidcIdTokenDecoderFactory();
@@ -55,6 +55,12 @@ public class OidcIdTokenDecoderFactoryTests {
 				.isInstanceOf(IllegalArgumentException.class);
 	}
 
+	@Test
+	public void setJwsAlgorithmResolverWhenNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> this.idTokenDecoderFactory.setJwsAlgorithmResolver(null))
+				.isInstanceOf(IllegalArgumentException.class);
+	}
+
 	@Test
 	public void createDecoderWhenClientRegistrationNullThenThrowIllegalArgumentException() {
 		assertThatThrownBy(() -> this.idTokenDecoderFactory.createDecoder(null))
@@ -62,9 +68,42 @@ public class OidcIdTokenDecoderFactoryTests {
 	}
 
 	@Test
-	public void createDecoderWhenJwkSetUriEmptyThenThrowOAuth2AuthenticationException() {
+	public void createDecoderWhenJwsAlgorithmDefaultAndJwkSetUriEmptyThenThrowOAuth2AuthenticationException() {
+		assertThatThrownBy(() -> this.idTokenDecoderFactory.createDecoder(this.registration.jwkSetUri(null).build()))
+				.isInstanceOf(OAuth2AuthenticationException.class)
+				.hasMessage("[missing_signature_verifier] Failed to find a Signature Verifier " +
+						"for Client Registration: 'registration-id'. " +
+						"Check to ensure you have configured the JwkSet URI.");
+	}
+
+	@Test
+	public void createDecoderWhenJwsAlgorithmEcAndJwkSetUriEmptyThenThrowOAuth2AuthenticationException() {
+		this.idTokenDecoderFactory.setJwsAlgorithmResolver(clientRegistration -> SignatureAlgorithm.ES256);
 		assertThatThrownBy(() -> this.idTokenDecoderFactory.createDecoder(this.registration.jwkSetUri(null).build()))
-				.isInstanceOf(OAuth2AuthenticationException.class);
+				.isInstanceOf(OAuth2AuthenticationException.class)
+				.hasMessage("[missing_signature_verifier] Failed to find a Signature Verifier " +
+						"for Client Registration: 'registration-id'. " +
+						"Check to ensure you have configured the JwkSet URI.");
+	}
+
+	@Test
+	public void createDecoderWhenJwsAlgorithmHmacAndClientSecretNullThenThrowOAuth2AuthenticationException() {
+		this.idTokenDecoderFactory.setJwsAlgorithmResolver(clientRegistration -> MacAlgorithm.HS256);
+		assertThatThrownBy(() -> this.idTokenDecoderFactory.createDecoder(this.registration.clientSecret(null).build()))
+				.isInstanceOf(OAuth2AuthenticationException.class)
+				.hasMessage("[missing_signature_verifier] Failed to find a Signature Verifier " +
+						"for Client Registration: 'registration-id'. " +
+						"Check to ensure you have configured the client secret.");
+	}
+
+	@Test
+	public void createDecoderWhenJwsAlgorithmNullThenThrowOAuth2AuthenticationException() {
+		this.idTokenDecoderFactory.setJwsAlgorithmResolver(clientRegistration -> null);
+		assertThatThrownBy(() -> this.idTokenDecoderFactory.createDecoder(this.registration.build()))
+				.isInstanceOf(OAuth2AuthenticationException.class)
+				.hasMessage("[missing_signature_verifier] Failed to find a Signature Verifier " +
+						"for Client Registration: 'registration-id'. " +
+						"Check to ensure you have configured a valid JWS Algorithm: 'null'");
 	}
 
 	@Test
@@ -78,11 +117,28 @@ public class OidcIdTokenDecoderFactoryTests {
 		Function<ClientRegistration, OAuth2TokenValidator<Jwt>> customJwtValidatorFactory = mock(Function.class);
 		this.idTokenDecoderFactory.setJwtValidatorFactory(customJwtValidatorFactory);
 
-		when(customJwtValidatorFactory.apply(any(ClientRegistration.class)))
-				.thenReturn(this.defaultJwtValidatorFactory.apply(this.registration.build()));
+		ClientRegistration clientRegistration = this.registration.build();
+
+		when(customJwtValidatorFactory.apply(same(clientRegistration)))
+				.thenReturn(new OidcIdTokenValidator(clientRegistration));
+
+		this.idTokenDecoderFactory.createDecoder(clientRegistration);
+
+		verify(customJwtValidatorFactory).apply(same(clientRegistration));
+	}
+
+	@Test
+	public void createDecoderWhenCustomJwsAlgorithmResolverSetThenApplied() {
+		Function<ClientRegistration, JwsAlgorithm> customJwsAlgorithmResolver = mock(Function.class);
+		this.idTokenDecoderFactory.setJwsAlgorithmResolver(customJwsAlgorithmResolver);
+
+		ClientRegistration clientRegistration = this.registration.build();
+
+		when(customJwsAlgorithmResolver.apply(same(clientRegistration)))
+				.thenReturn(MacAlgorithm.HS256);
 
-		this.idTokenDecoderFactory.createDecoder(this.registration.build());
+		this.idTokenDecoderFactory.createDecoder(clientRegistration);
 
-		verify(customJwtValidatorFactory).apply(any(ClientRegistration.class));
+		verify(customJwsAlgorithmResolver).apply(same(clientRegistration));
 	}
 }

+ 65 - 9
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/ReactiveOidcIdTokenDecoderFactoryTests.java

@@ -21,13 +21,15 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
 import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
 import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
 import org.springframework.security.oauth2.core.OAuth2TokenValidator;
+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 java.util.function.Function;
 
 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.Mockito.*;
 
 /**
@@ -42,8 +44,6 @@ public class ReactiveOidcIdTokenDecoderFactoryTests {
 
 	private ReactiveOidcIdTokenDecoderFactory idTokenDecoderFactory;
 
-	private Function<ClientRegistration, OAuth2TokenValidator<Jwt>> defaultJwtValidatorFactory = OidcIdTokenValidator::new;
-
 	@Before
 	public void setUp() {
 		this.idTokenDecoderFactory = new ReactiveOidcIdTokenDecoderFactory();
@@ -55,6 +55,12 @@ public class ReactiveOidcIdTokenDecoderFactoryTests {
 				.isInstanceOf(IllegalArgumentException.class);
 	}
 
+	@Test
+	public void setJwsAlgorithmResolverWhenNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> this.idTokenDecoderFactory.setJwsAlgorithmResolver(null))
+				.isInstanceOf(IllegalArgumentException.class);
+	}
+
 	@Test
 	public void createDecoderWhenClientRegistrationNullThenThrowIllegalArgumentException() {
 		assertThatThrownBy(() -> this.idTokenDecoderFactory.createDecoder(null))
@@ -62,9 +68,42 @@ public class ReactiveOidcIdTokenDecoderFactoryTests {
 	}
 
 	@Test
-	public void createDecoderWhenJwkSetUriEmptyThenThrowOAuth2AuthenticationException() {
+	public void createDecoderWhenJwsAlgorithmDefaultAndJwkSetUriEmptyThenThrowOAuth2AuthenticationException() {
+		assertThatThrownBy(() -> this.idTokenDecoderFactory.createDecoder(this.registration.jwkSetUri(null).build()))
+				.isInstanceOf(OAuth2AuthenticationException.class)
+				.hasMessage("[missing_signature_verifier] Failed to find a Signature Verifier " +
+						"for Client Registration: 'registration-id'. " +
+						"Check to ensure you have configured the JwkSet URI.");
+	}
+
+	@Test
+	public void createDecoderWhenJwsAlgorithmEcAndJwkSetUriEmptyThenThrowOAuth2AuthenticationException() {
+		this.idTokenDecoderFactory.setJwsAlgorithmResolver(clientRegistration -> SignatureAlgorithm.ES256);
 		assertThatThrownBy(() -> this.idTokenDecoderFactory.createDecoder(this.registration.jwkSetUri(null).build()))
-				.isInstanceOf(OAuth2AuthenticationException.class);
+				.isInstanceOf(OAuth2AuthenticationException.class)
+				.hasMessage("[missing_signature_verifier] Failed to find a Signature Verifier " +
+						"for Client Registration: 'registration-id'. " +
+						"Check to ensure you have configured the JwkSet URI.");
+	}
+
+	@Test
+	public void createDecoderWhenJwsAlgorithmHmacAndClientSecretNullThenThrowOAuth2AuthenticationException() {
+		this.idTokenDecoderFactory.setJwsAlgorithmResolver(clientRegistration -> MacAlgorithm.HS256);
+		assertThatThrownBy(() -> this.idTokenDecoderFactory.createDecoder(this.registration.clientSecret(null).build()))
+				.isInstanceOf(OAuth2AuthenticationException.class)
+				.hasMessage("[missing_signature_verifier] Failed to find a Signature Verifier " +
+						"for Client Registration: 'registration-id'. " +
+						"Check to ensure you have configured the client secret.");
+	}
+
+	@Test
+	public void createDecoderWhenJwsAlgorithmNullThenThrowOAuth2AuthenticationException() {
+		this.idTokenDecoderFactory.setJwsAlgorithmResolver(clientRegistration -> null);
+		assertThatThrownBy(() -> this.idTokenDecoderFactory.createDecoder(this.registration.build()))
+				.isInstanceOf(OAuth2AuthenticationException.class)
+				.hasMessage("[missing_signature_verifier] Failed to find a Signature Verifier " +
+						"for Client Registration: 'registration-id'. " +
+						"Check to ensure you have configured a valid JWS Algorithm: 'null'");
 	}
 
 	@Test
@@ -78,11 +117,28 @@ public class ReactiveOidcIdTokenDecoderFactoryTests {
 		Function<ClientRegistration, OAuth2TokenValidator<Jwt>> customJwtValidatorFactory = mock(Function.class);
 		this.idTokenDecoderFactory.setJwtValidatorFactory(customJwtValidatorFactory);
 
-		when(customJwtValidatorFactory.apply(any(ClientRegistration.class)))
-				.thenReturn(this.defaultJwtValidatorFactory.apply(this.registration.build()));
+		ClientRegistration clientRegistration = this.registration.build();
+
+		when(customJwtValidatorFactory.apply(same(clientRegistration)))
+				.thenReturn(new OidcIdTokenValidator(clientRegistration));
+
+		this.idTokenDecoderFactory.createDecoder(clientRegistration);
+
+		verify(customJwtValidatorFactory).apply(same(clientRegistration));
+	}
+
+	@Test
+	public void createDecoderWhenCustomJwsAlgorithmResolverSetThenApplied() {
+		Function<ClientRegistration, JwsAlgorithm> customJwsAlgorithmResolver = mock(Function.class);
+		this.idTokenDecoderFactory.setJwsAlgorithmResolver(customJwsAlgorithmResolver);
+
+		ClientRegistration clientRegistration = this.registration.build();
+
+		when(customJwsAlgorithmResolver.apply(same(clientRegistration)))
+				.thenReturn(MacAlgorithm.HS256);
 
-		this.idTokenDecoderFactory.createDecoder(this.registration.build());
+		this.idTokenDecoderFactory.createDecoder(clientRegistration);
 
-		verify(customJwtValidatorFactory).apply(any(ClientRegistration.class));
+		verify(customJwsAlgorithmResolver).apply(same(clientRegistration));
 	}
 }

+ 33 - 0
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/JwsAlgorithm.java

@@ -0,0 +1,33 @@
+/*
+ * Copyright 2002-2019 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.jose.jws;
+
+/**
+ * Super interface for cryptographic algorithms defined by the JSON Web Algorithms (JWA) specification
+ * and used by JSON Web Signature (JWS) to digitally sign or create a MAC
+ * of the contents of the JWS Protected Header and JWS Payload.
+ *
+ * @author Joe Grandja
+ * @since 5.2
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc7518">JSON Web Algorithms (JWA)</a>
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515">JSON Web Signature (JWS)</a>
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc7518#section-3">Cryptographic Algorithms for Digital Signatures and MACs</a>
+ */
+public interface JwsAlgorithm {
+
+	String getName();
+
+}

+ 77 - 0
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/MacAlgorithm.java

@@ -0,0 +1,77 @@
+/*
+ * Copyright 2002-2019 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.jose.jws;
+
+import java.util.stream.Stream;
+
+/**
+ * An enumeration of the cryptographic algorithms defined by the JSON Web Algorithms (JWA) specification
+ * and used by JSON Web Signature (JWS) to create a MAC of the contents of the JWS Protected Header and JWS Payload.
+ *
+ * @author Joe Grandja
+ * @since 5.2
+ * @see JwsAlgorithm
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc7518">JSON Web Algorithms (JWA)</a>
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515">JSON Web Signature (JWS)</a>
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc7518#section-3">Cryptographic Algorithms for Digital Signatures and MACs</a>
+ */
+public enum MacAlgorithm implements JwsAlgorithm {
+
+	/**
+	 * HMAC using SHA-256 (Required)
+	 */
+	HS256(JwsAlgorithms.HS256),
+
+	/**
+	 * HMAC using SHA-384 (Optional)
+	 */
+	HS384(JwsAlgorithms.HS384),
+
+	/**
+	 * HMAC using SHA-512 (Optional)
+	 */
+	HS512(JwsAlgorithms.HS512);
+
+
+	private final String name;
+
+	MacAlgorithm(String name) {
+		this.name = name;
+	}
+
+	/**
+	 * Attempt to resolve the provided algorithm name to a {@code MacAlgorithm}.
+	 *
+	 * @param name the algorithm name
+	 * @return the resolved {@code MacAlgorithm}, or {@code null} if not found
+	 */
+	public static MacAlgorithm from(String name) {
+		return Stream.of(values())
+				.filter(algorithm -> algorithm.getName().equals(name))
+				.findFirst()
+				.orElse(null);
+	}
+
+	/**
+	 * Returns the algorithm name.
+	 *
+	 * @return the algorithm name
+	 */
+	@Override
+	public String getName() {
+		return this.name;
+	}
+}

+ 107 - 0
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/SignatureAlgorithm.java

@@ -0,0 +1,107 @@
+/*
+ * Copyright 2002-2019 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.jose.jws;
+
+import java.util.stream.Stream;
+
+/**
+ * An enumeration of the cryptographic algorithms defined by the JSON Web Algorithms (JWA) specification
+ * and used by JSON Web Signature (JWS) to digitally sign the contents of the JWS Protected Header and JWS Payload.
+ *
+ * @author Joe Grandja
+ * @since 5.2
+ * @see JwsAlgorithm
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc7518">JSON Web Algorithms (JWA)</a>
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515">JSON Web Signature (JWS)</a>
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc7518#section-3">Cryptographic Algorithms for Digital Signatures and MACs</a>
+ */
+public enum SignatureAlgorithm implements JwsAlgorithm {
+
+	/**
+	 * RSASSA-PKCS1-v1_5 using SHA-256 (Recommended)
+	 */
+	RS256(JwsAlgorithms.RS256),
+
+	/**
+	 * RSASSA-PKCS1-v1_5 using SHA-384 (Optional)
+	 */
+	RS384(JwsAlgorithms.RS384),
+
+	/**
+	 * RSASSA-PKCS1-v1_5 using SHA-512 (Optional)
+	 */
+	RS512(JwsAlgorithms.RS512),
+
+	/**
+	 * ECDSA using P-256 and SHA-256 (Recommended+)
+	 */
+	ES256(JwsAlgorithms.ES256),
+
+	/**
+	 * ECDSA using P-384 and SHA-384 (Optional)
+	 */
+	ES384(JwsAlgorithms.ES384),
+
+	/**
+	 * ECDSA using P-521 and SHA-512 (Optional)
+	 */
+	ES512(JwsAlgorithms.ES512),
+
+	/**
+	 * RSASSA-PSS using SHA-256 and MGF1 with SHA-256 (Optional)
+	 */
+	PS256(JwsAlgorithms.PS256),
+
+	/**
+	 * RSASSA-PSS using SHA-384 and MGF1 with SHA-384 (Optional)
+	 */
+	PS384(JwsAlgorithms.PS384),
+
+	/**
+	 * RSASSA-PSS using SHA-512 and MGF1 with SHA-512 (Optional)
+	 */
+	PS512(JwsAlgorithms.PS512);
+
+
+	private final String name;
+
+	SignatureAlgorithm(String name) {
+		this.name = name;
+	}
+
+	/**
+	 * Attempt to resolve the provided algorithm name to a {@code SignatureAlgorithm}.
+	 *
+	 * @param name the algorithm name
+	 * @return the resolved {@code SignatureAlgorithm}, or {@code null} if not found
+	 */
+	public static SignatureAlgorithm from(String name) {
+		return Stream.of(values())
+				.filter(algorithm -> algorithm.getName().equals(name))
+				.findFirst()
+				.orElse(null);
+	}
+
+	/**
+	 * Returns the algorithm name.
+	 *
+	 * @return the algorithm name
+	 */
+	@Override
+	public String getName() {
+		return this.name;
+	}
+}

+ 87 - 30
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java

@@ -16,21 +16,12 @@
 
 package org.springframework.security.oauth2.jwt;
 
-import java.io.IOException;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.security.interfaces.RSAPublicKey;
-import java.text.ParseException;
-import java.time.Instant;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.Map;
-
 import com.nimbusds.jose.JWSAlgorithm;
 import com.nimbusds.jose.RemoteKeySourceException;
 import com.nimbusds.jose.jwk.JWKSet;
 import com.nimbusds.jose.jwk.RSAKey;
 import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
+import com.nimbusds.jose.jwk.source.ImmutableSecret;
 import com.nimbusds.jose.jwk.source.JWKSource;
 import com.nimbusds.jose.jwk.source.RemoteJWKSet;
 import com.nimbusds.jose.proc.JWSKeySelector;
@@ -45,7 +36,6 @@ import com.nimbusds.jwt.SignedJWT;
 import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
 import com.nimbusds.jwt.proc.DefaultJWTProcessor;
 import com.nimbusds.jwt.proc.JWTProcessor;
-
 import org.springframework.core.convert.converter.Converter;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.HttpMethod;
@@ -54,15 +44,29 @@ import org.springframework.http.RequestEntity;
 import org.springframework.http.ResponseEntity;
 import org.springframework.security.oauth2.core.OAuth2TokenValidator;
 import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
-import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
+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.util.Assert;
 import org.springframework.web.client.RestOperations;
 import org.springframework.web.client.RestTemplate;
 
+import javax.crypto.SecretKey;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.security.interfaces.RSAPublicKey;
+import java.text.ParseException;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
 /**
  * A low-level Nimbus implementation of {@link JwtDecoder} which takes a raw Nimbus configuration.
  *
  * @author Josh Cummings
+ * @author Joe Grandja
  * @since 5.2
  */
 public final class NimbusJwtDecoder implements JwtDecoder {
@@ -178,8 +182,6 @@ public final class NimbusJwtDecoder implements JwtDecoder {
 	 *
 	 * @param jwkSetUri the JWK Set uri to use
 	 * @return a {@link JwkSetUriJwtDecoderBuilder} for further configurations
-	 *
-	 * @since 5.2
 	 */
 	public static JwkSetUriJwtDecoderBuilder withJwkSetUri(String jwkSetUri) {
 		return new JwkSetUriJwtDecoderBuilder(jwkSetUri);
@@ -190,18 +192,24 @@ public final class NimbusJwtDecoder implements JwtDecoder {
 	 *
 	 * @param key the public key to use
 	 * @return a {@link PublicKeyJwtDecoderBuilder} for further configurations
-	 *
-	 * @since 5.2
 	 */
 	public static PublicKeyJwtDecoderBuilder withPublicKey(RSAPublicKey key) {
 		return new PublicKeyJwtDecoderBuilder(key);
 	}
 
+	/**
+	 * Use the given {@code SecretKey} to validate the MAC on a JSON Web Signature (JWS).
+	 *
+	 * @param secretKey the {@code SecretKey} used to validate the MAC
+	 * @return a {@link SecretKeyJwtDecoderBuilder} for further configurations
+	 */
+	public static SecretKeyJwtDecoderBuilder withSecretKey(SecretKey secretKey) {
+		return new SecretKeyJwtDecoderBuilder(secretKey);
+	}
+
 	/**
 	 * A builder for creating {@link NimbusJwtDecoder} instances based on a
 	 * <a target="_blank" href="https://tools.ietf.org/html/rfc7517#section-5">JWK Set</a> uri.
-	 *
-	 * @since 5.2
 	 */
 	public static final class JwkSetUriJwtDecoderBuilder {
 		private String jwkSetUri;
@@ -220,9 +228,9 @@ public final class NimbusJwtDecoder implements JwtDecoder {
 		 * @param jwsAlgorithm the algorithm to use
 		 * @return a {@link JwkSetUriJwtDecoderBuilder} for further configurations
 		 */
-		public JwkSetUriJwtDecoderBuilder jwsAlgorithm(String jwsAlgorithm) {
-			Assert.hasText(jwsAlgorithm, "jwsAlgorithm cannot be empty");
-			this.jwsAlgorithm = JWSAlgorithm.parse(jwsAlgorithm);
+		public JwkSetUriJwtDecoderBuilder jwsAlgorithm(JwsAlgorithm jwsAlgorithm) {
+			Assert.notNull(jwsAlgorithm, "jwsAlgorithm cannot be null");
+			this.jwsAlgorithm = JWSAlgorithm.parse(jwsAlgorithm.getName());
 			return this;
 		}
 
@@ -303,10 +311,7 @@ public final class NimbusJwtDecoder implements JwtDecoder {
 	}
 
 	/**
-	 * A builder for creating {@link NimbusJwtDecoder} instances based on a
-	 * public key.
-	 *
-	 * @since 5.2
+	 * A builder for creating {@link NimbusJwtDecoder} instances based on a public key.
 	 */
 	public static final class PublicKeyJwtDecoderBuilder {
 		private JWSAlgorithm jwsAlgorithm;
@@ -314,7 +319,7 @@ public final class NimbusJwtDecoder implements JwtDecoder {
 
 		private PublicKeyJwtDecoderBuilder(RSAPublicKey key) {
 			Assert.notNull(key, "key cannot be null");
-			this.jwsAlgorithm = JWSAlgorithm.parse(JwsAlgorithms.RS256);
+			this.jwsAlgorithm = JWSAlgorithm.RS256;
 			this.key = rsaKey(key);
 		}
 
@@ -330,12 +335,12 @@ public final class NimbusJwtDecoder implements JwtDecoder {
 		 * The value should be one of
 		 * <a href="https://tools.ietf.org/html/rfc7518#section-3.3" target="_blank">RS256, RS384, or RS512</a>.
 		 *
-		 * @param jwsAlgorithm the algorithm to use
+		 * @param signatureAlgorithm the algorithm to use
 		 * @return a {@link PublicKeyJwtDecoderBuilder} for further configurations
 		 */
-		public PublicKeyJwtDecoderBuilder jwsAlgorithm(String jwsAlgorithm) {
-			Assert.hasText(jwsAlgorithm, "jwsAlgorithm cannot be empty");
-			this.jwsAlgorithm = JWSAlgorithm.parse(jwsAlgorithm);
+		public PublicKeyJwtDecoderBuilder signatureAlgorithm(SignatureAlgorithm signatureAlgorithm) {
+			Assert.notNull(signatureAlgorithm, "signatureAlgorithm cannot be null");
+			this.jwsAlgorithm = JWSAlgorithm.parse(signatureAlgorithm.getName());
 			return this;
 		}
 
@@ -368,4 +373,56 @@ public final class NimbusJwtDecoder implements JwtDecoder {
 			return new NimbusJwtDecoder(processor());
 		}
 	}
+
+	/**
+	 * A builder for creating {@link NimbusJwtDecoder} instances based on a {@code SecretKey}.
+	 */
+	public static final class SecretKeyJwtDecoderBuilder {
+		private final SecretKey secretKey;
+		private JWSAlgorithm jwsAlgorithm = JWSAlgorithm.HS256;
+
+		private SecretKeyJwtDecoderBuilder(SecretKey secretKey) {
+			Assert.notNull(secretKey, "secretKey cannot be null");
+			this.secretKey = secretKey;
+		}
+
+		/**
+		 * Use the given
+		 * <a href="https://tools.ietf.org/html/rfc7515#section-4.1.1" target="_blank">algorithm</a>
+		 * when generating the MAC.
+		 *
+		 * The value should be one of
+		 * <a href="https://tools.ietf.org/html/rfc7518#section-3.2" target="_blank">HS256, HS384 or HS512</a>.
+		 *
+		 * @param macAlgorithm the MAC algorithm to use
+		 * @return a {@link SecretKeyJwtDecoderBuilder} for further configurations
+		 */
+		public SecretKeyJwtDecoderBuilder macAlgorithm(MacAlgorithm macAlgorithm) {
+			Assert.notNull(macAlgorithm, "macAlgorithm cannot be null");
+			this.jwsAlgorithm = JWSAlgorithm.parse(macAlgorithm.getName());
+			return this;
+		}
+
+		/**
+		 * Build the configured {@link NimbusJwtDecoder}.
+		 *
+		 * @return the configured {@link NimbusJwtDecoder}
+		 */
+		public NimbusJwtDecoder build() {
+			return new NimbusJwtDecoder(processor());
+		}
+
+		JWTProcessor<SecurityContext> processor() {
+			JWKSource<SecurityContext> jwkSource = new ImmutableSecret<>(this.secretKey);
+			JWSKeySelector<SecurityContext> jwsKeySelector =
+					new JWSVerificationKeySelector<>(this.jwsAlgorithm, jwkSource);
+			DefaultJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
+			jwtProcessor.setJWSKeySelector(jwsKeySelector);
+
+			// Spring Security validates the claim set independent from Nimbus
+			jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> { });
+
+			return jwtProcessor;
+		}
+	}
 }

+ 5 - 4
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupport.java

@@ -15,15 +15,16 @@
  */
 package org.springframework.security.oauth2.jwt;
 
-import java.util.Collections;
-import java.util.Map;
-
 import org.springframework.core.convert.converter.Converter;
 import org.springframework.security.oauth2.core.OAuth2TokenValidator;
 import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
+import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
 import org.springframework.util.Assert;
 import org.springframework.web.client.RestOperations;
 
+import java.util.Collections;
+import java.util.Map;
+
 import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withJwkSetUri;
 
 /**
@@ -75,7 +76,7 @@ public final class NimbusJwtDecoderJwkSupport implements JwtDecoder {
 		Assert.hasText(jwkSetUrl, "jwkSetUrl cannot be empty");
 		Assert.hasText(jwsAlgorithm, "jwsAlgorithm cannot be empty");
 
-		this.jwtDecoderBuilder = withJwkSetUri(jwkSetUrl).jwsAlgorithm(jwsAlgorithm);
+		this.jwtDecoderBuilder = withJwkSetUri(jwkSetUrl).jwsAlgorithm(SignatureAlgorithm.from(jwsAlgorithm));
 		this.delegate = makeDelegate();
 	}
 

+ 118 - 39
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java

@@ -15,13 +15,6 @@
  */
 package org.springframework.security.oauth2.jwt;
 
-import java.security.interfaces.RSAPublicKey;
-import java.time.Instant;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.function.Function;
-
 import com.nimbusds.jose.JOSEException;
 import com.nimbusds.jose.JWSAlgorithm;
 import com.nimbusds.jose.JWSHeader;
@@ -31,6 +24,7 @@ import com.nimbusds.jose.jwk.JWKSelector;
 import com.nimbusds.jose.jwk.JWKSet;
 import com.nimbusds.jose.jwk.RSAKey;
 import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
+import com.nimbusds.jose.jwk.source.ImmutableSecret;
 import com.nimbusds.jose.jwk.source.JWKSecurityContextJWKSet;
 import com.nimbusds.jose.jwk.source.JWKSource;
 import com.nimbusds.jose.proc.BadJOSEException;
@@ -44,26 +38,35 @@ import com.nimbusds.jwt.JWTParser;
 import com.nimbusds.jwt.SignedJWT;
 import com.nimbusds.jwt.proc.DefaultJWTProcessor;
 import com.nimbusds.jwt.proc.JWTProcessor;
-import reactor.core.publisher.Flux;
-import reactor.core.publisher.Mono;
-
 import org.springframework.core.convert.converter.Converter;
 import org.springframework.security.oauth2.core.OAuth2TokenValidator;
 import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
-import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
+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.util.Assert;
 import org.springframework.web.reactive.function.client.WebClient;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import javax.crypto.SecretKey;
+import java.security.interfaces.RSAPublicKey;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.function.Function;
 
 /**
  * An implementation of a {@link ReactiveJwtDecoder} that &quot;decodes&quot; a
  * JSON Web Token (JWT) and additionally verifies it's digital signature if the JWT is a
- * JSON Web Signature (JWS). The public key used for verification is obtained from the
- * JSON Web Key (JWK) Set {@code URL} supplied via the constructor.
+ * JSON Web Signature (JWS).
  *
  * <p>
  * <b>NOTE:</b> This implementation uses the Nimbus JOSE + JWT SDK internally.
  *
  * @author Rob Winch
+ * @author Joe Grandja
  * @since 5.1
  * @see ReactiveJwtDecoder
  * @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519">JSON Web Token (JWT)</a>
@@ -75,22 +78,34 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
 	private final Converter<SignedJWT, Mono<JWTClaimsSet>> jwtProcessor;
 
 	private OAuth2TokenValidator<Jwt> jwtValidator = JwtValidators.createDefault();
-	private Converter<Map<String, Object>, Map<String, Object>> claimSetConverter = MappedJwtClaimSetConverter
-			.withDefaults(Collections.emptyMap());
-
-	public NimbusReactiveJwtDecoder(RSAPublicKey publicKey) {
-		this.jwtProcessor = withPublicKey(publicKey).processor();
-	}
+	private Converter<Map<String, Object>, Map<String, Object>> claimSetConverter =
+			MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap());
 
 	/**
-	 * Constructs a {@code NimbusJwtDecoderJwkSupport} using the provided parameters.
+	 * Constructs a {@code NimbusReactiveJwtDecoder} using the provided parameters.
 	 *
 	 * @param jwkSetUrl the JSON Web Key (JWK) Set {@code URL}
 	 */
 	public NimbusReactiveJwtDecoder(String jwkSetUrl) {
-		this.jwtProcessor = withJwkSetUri(jwkSetUrl).processor();
+		this(withJwkSetUri(jwkSetUrl).processor());
+	}
+
+	/**
+	 * Constructs a {@code NimbusReactiveJwtDecoder} using the provided parameters.
+	 *
+	 * @param publicKey the {@code RSAPublicKey} used to verify the signature
+	 * @since 5.2
+	 */
+	public NimbusReactiveJwtDecoder(RSAPublicKey publicKey) {
+		this(withPublicKey(publicKey).processor());
 	}
 
+	/**
+	 * Constructs a {@code NimbusReactiveJwtDecoder} using the provided parameters.
+	 *
+	 * @param jwtProcessor the {@link Converter} used to process and verify the signed Jwt and return the Jwt Claim Set
+	 * @since 5.2
+	 */
 	public NimbusReactiveJwtDecoder(Converter<SignedJWT, Mono<JWTClaimsSet>> jwtProcessor) {
 		this.jwtProcessor = jwtProcessor;
 	}
@@ -188,6 +203,18 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
 		return new PublicKeyReactiveJwtDecoderBuilder(key);
 	}
 
+	/**
+	 * Use the given {@code SecretKey} to validate the MAC on a JSON Web Signature (JWS).
+	 *
+	 * @param secretKey the {@code SecretKey} used to validate the MAC
+	 * @return a {@link SecretKeyReactiveJwtDecoderBuilder} for further configurations
+	 *
+	 * @since 5.2
+	 */
+	public static SecretKeyReactiveJwtDecoderBuilder withSecretKey(SecretKey secretKey) {
+		return new SecretKeyReactiveJwtDecoderBuilder(secretKey);
+	}
+
 	/**
 	 * Use the given {@link Function} to validate JWTs
 	 *
@@ -207,8 +234,7 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
 	 * @since 5.2
 	 */
 	public static final class JwkSetUriReactiveJwtDecoderBuilder {
-
-		private String jwkSetUri;
+		private final String jwkSetUri;
 		private JWSAlgorithm jwsAlgorithm = JWSAlgorithm.RS256;
 		private WebClient webClient = WebClient.create();
 
@@ -224,9 +250,9 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
 		 * @param jwsAlgorithm the algorithm to use
 		 * @return a {@link JwkSetUriReactiveJwtDecoderBuilder} for further configurations
 		 */
-		public JwkSetUriReactiveJwtDecoderBuilder jwsAlgorithm(String jwsAlgorithm) {
-			Assert.hasText(jwsAlgorithm, "jwsAlgorithm cannot be empty");
-			this.jwsAlgorithm = JWSAlgorithm.parse(jwsAlgorithm);
+		public JwkSetUriReactiveJwtDecoderBuilder jwsAlgorithm(JwsAlgorithm jwsAlgorithm) {
+			Assert.notNull(jwsAlgorithm, "jwsAlgorithm cannot be null");
+			this.jwsAlgorithm = JWSAlgorithm.parse(jwsAlgorithm.getName());
 			return this;
 		}
 
@@ -284,19 +310,18 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
 	}
 
 	/**
-	 * A builder for creating Nimbus {@link JWTProcessor} instances based on a
-	 * public key.
+	 * A builder for creating {@link NimbusReactiveJwtDecoder} instances based on a public key.
 	 *
 	 * @since 5.2
 	 */
 	public static final class PublicKeyReactiveJwtDecoderBuilder {
+		private final RSAKey key;
 		private JWSAlgorithm jwsAlgorithm;
-		private RSAKey key;
 
 		private PublicKeyReactiveJwtDecoderBuilder(RSAPublicKey key) {
 			Assert.notNull(key, "key cannot be null");
-			this.jwsAlgorithm = JWSAlgorithm.parse(JwsAlgorithms.RS256);
 			this.key = rsaKey(key);
+			this.jwsAlgorithm = JWSAlgorithm.RS256;
 		}
 
 		private static RSAKey rsaKey(RSAPublicKey publicKey) {
@@ -310,12 +335,12 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
 		 * The value should be one of
 		 * <a href="https://tools.ietf.org/html/rfc7518#section-3.3" target="_blank">RS256, RS384, or RS512</a>.
 		 *
-		 * @param jwsAlgorithm the algorithm to use
+		 * @param signatureAlgorithm the algorithm to use
 		 * @return a {@link PublicKeyReactiveJwtDecoderBuilder} for further configurations
 		 */
-		public PublicKeyReactiveJwtDecoderBuilder jwsAlgorithm(String jwsAlgorithm) {
-			Assert.hasText(jwsAlgorithm, "jwsAlgorithm cannot be empty");
-			this.jwsAlgorithm = JWSAlgorithm.parse(jwsAlgorithm);
+		public PublicKeyReactiveJwtDecoderBuilder signatureAlgorithm(SignatureAlgorithm signatureAlgorithm) {
+			Assert.notNull(signatureAlgorithm, "signatureAlgorithm cannot be null");
+			this.jwsAlgorithm = JWSAlgorithm.parse(signatureAlgorithm.getName());
 			return this;
 		}
 
@@ -349,17 +374,71 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
 		}
 	}
 
+	/**
+	 * A builder for creating {@link NimbusReactiveJwtDecoder} instances based on a {@code SecretKey}.
+	 *
+	 * @since 5.2
+	 */
+	public static final class SecretKeyReactiveJwtDecoderBuilder {
+		private final SecretKey secretKey;
+		private JWSAlgorithm jwsAlgorithm = JWSAlgorithm.HS256;
+
+		private SecretKeyReactiveJwtDecoderBuilder(SecretKey secretKey) {
+			Assert.notNull(secretKey, "secretKey cannot be null");
+			this.secretKey = secretKey;
+		}
+
+		/**
+		 * Use the given
+		 * <a href="https://tools.ietf.org/html/rfc7515#section-4.1.1" target="_blank">algorithm</a>
+		 * when generating the MAC.
+		 *
+		 * The value should be one of
+		 * <a href="https://tools.ietf.org/html/rfc7518#section-3.2" target="_blank">HS256, HS384 or HS512</a>.
+		 *
+		 * @param macAlgorithm the MAC algorithm to use
+		 * @return a {@link SecretKeyReactiveJwtDecoderBuilder} for further configurations
+		 */
+		public SecretKeyReactiveJwtDecoderBuilder macAlgorithm(MacAlgorithm macAlgorithm) {
+			Assert.notNull(macAlgorithm, "macAlgorithm cannot be null");
+			this.jwsAlgorithm = JWSAlgorithm.parse(macAlgorithm.getName());
+			return this;
+		}
+
+		/**
+		 * Build the configured {@link NimbusReactiveJwtDecoder}.
+		 *
+		 * @return the configured {@link NimbusReactiveJwtDecoder}
+		 */
+		public NimbusReactiveJwtDecoder build() {
+			return new NimbusReactiveJwtDecoder(processor());
+		}
+
+		Converter<SignedJWT, Mono<JWTClaimsSet>> processor() {
+			JWKSource<SecurityContext> jwkSource = new ImmutableSecret<>(this.secretKey);
+			JWSKeySelector<SecurityContext> jwsKeySelector =
+					new JWSVerificationKeySelector<>(this.jwsAlgorithm, jwkSource);
+			DefaultJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
+			jwtProcessor.setJWSKeySelector(jwsKeySelector);
+
+			// Spring Security validates the claim set independent from Nimbus
+			jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> { });
+
+			return signedJWT -> Mono.just(signedJWT).map(jwt -> createClaimsSet(jwtProcessor, jwt, null));
+		}
+	}
+
 	/**
 	 * A builder for creating {@link NimbusReactiveJwtDecoder} instances.
 	 *
 	 * @since 5.2
 	 */
 	public static final class JwkSourceReactiveJwtDecoderBuilder {
-		private Function<JWT, Flux<JWK>> jwkSource;
+		private final Function<JWT, Flux<JWK>> jwkSource;
 		private JWSAlgorithm jwsAlgorithm = JWSAlgorithm.RS256;
 
 		private JwkSourceReactiveJwtDecoderBuilder(Function<JWT, Flux<JWK>> jwkSource) {
-			Assert.notNull(jwkSource, "jwkSource cannot be empty");
+			Assert.notNull(jwkSource, "jwkSource cannot be null");
 			this.jwkSource = jwkSource;
 		}
 
@@ -370,9 +449,9 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
 		 * @param jwsAlgorithm the algorithm to use
 		 * @return a {@link JwkSourceReactiveJwtDecoderBuilder} for further configurations
 		 */
-		public JwkSourceReactiveJwtDecoderBuilder jwsAlgorithm(String jwsAlgorithm) {
-			Assert.hasText(jwsAlgorithm, "jwsAlgorithm cannot be empty");
-			this.jwsAlgorithm = JWSAlgorithm.parse(jwsAlgorithm);
+		public JwkSourceReactiveJwtDecoderBuilder jwsAlgorithm(JwsAlgorithm jwsAlgorithm) {
+			Assert.notNull(jwsAlgorithm, "jwsAlgorithm cannot be null");
+			this.jwsAlgorithm = JWSAlgorithm.parse(jwsAlgorithm.getName());
 			return this;
 		}
 

+ 32 - 0
oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/TestKeys.java

@@ -0,0 +1,32 @@
+/*
+ * Copyright 2002-2019 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.jose;
+
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+import java.util.Base64;
+
+/**
+ * @author Joe Grandja
+ * @since 5.2
+ */
+public class TestKeys {
+	public static final String DEFAULT_ENCODED_SECRET_KEY = "bCzY/M48bbkwBEWjmNSIEPfwApcvXOnkCxORBEbPr+4=";
+
+	public static final SecretKey DEFAULT_SECRET_KEY =
+			new SecretKeySpec(Base64.getDecoder().decode(DEFAULT_ENCODED_SECRET_KEY), "AES");
+
+}

+ 41 - 0
oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/jws/MacAlgorithmTests.java

@@ -0,0 +1,41 @@
+/*
+ * Copyright 2002-2019 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.jose.jws;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link MacAlgorithm}
+ *
+ * @author Joe Grandja
+ * @since 5.2
+ */
+public class MacAlgorithmTests {
+
+	@Test
+	public void fromWhenAlgorithmValidThenResolves() {
+		assertThat(MacAlgorithm.from(JwsAlgorithms.HS256)).isEqualTo(MacAlgorithm.HS256);
+		assertThat(MacAlgorithm.from(JwsAlgorithms.HS384)).isEqualTo(MacAlgorithm.HS384);
+		assertThat(MacAlgorithm.from(JwsAlgorithms.HS512)).isEqualTo(MacAlgorithm.HS512);
+	}
+
+	@Test
+	public void fromWhenAlgorithmInvalidThenDoesNotResolve() {
+		assertThat(MacAlgorithm.from("invalid")).isNull();
+	}
+}

+ 47 - 0
oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/jws/SignatureAlgorithmTests.java

@@ -0,0 +1,47 @@
+/*
+ * Copyright 2002-2019 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.jose.jws;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link SignatureAlgorithm}
+ *
+ * @author Joe Grandja
+ * @since 5.2
+ */
+public class SignatureAlgorithmTests {
+
+	@Test
+	public void fromWhenAlgorithmValidThenResolves() {
+		assertThat(SignatureAlgorithm.from(JwsAlgorithms.RS256)).isEqualTo(SignatureAlgorithm.RS256);
+		assertThat(SignatureAlgorithm.from(JwsAlgorithms.RS384)).isEqualTo(SignatureAlgorithm.RS384);
+		assertThat(SignatureAlgorithm.from(JwsAlgorithms.RS512)).isEqualTo(SignatureAlgorithm.RS512);
+		assertThat(SignatureAlgorithm.from(JwsAlgorithms.ES256)).isEqualTo(SignatureAlgorithm.ES256);
+		assertThat(SignatureAlgorithm.from(JwsAlgorithms.ES384)).isEqualTo(SignatureAlgorithm.ES384);
+		assertThat(SignatureAlgorithm.from(JwsAlgorithms.ES512)).isEqualTo(SignatureAlgorithm.ES512);
+		assertThat(SignatureAlgorithm.from(JwsAlgorithms.PS256)).isEqualTo(SignatureAlgorithm.PS256);
+		assertThat(SignatureAlgorithm.from(JwsAlgorithms.PS384)).isEqualTo(SignatureAlgorithm.PS384);
+		assertThat(SignatureAlgorithm.from(JwsAlgorithms.PS512)).isEqualTo(SignatureAlgorithm.PS512);
+	}
+
+	@Test
+	public void fromWhenAlgorithmInvalidThenDoesNotResolve() {
+		assertThat(SignatureAlgorithm.from("invalid")).isNull();
+	}
+}

+ 80 - 22
oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderTests.java

@@ -16,19 +16,11 @@
 
 package org.springframework.security.oauth2.jwt;
 
-import java.security.KeyFactory;
-import java.security.NoSuchAlgorithmException;
-import java.security.interfaces.RSAPublicKey;
-import java.security.spec.EncodedKeySpec;
-import java.security.spec.InvalidKeySpecException;
-import java.security.spec.X509EncodedKeySpec;
-import java.text.ParseException;
-import java.util.Arrays;
-import java.util.Base64;
-import java.util.Collections;
-import java.util.Map;
-
 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.proc.BadJOSEException;
 import com.nimbusds.jose.proc.SecurityContext;
 import com.nimbusds.jwt.JWTClaimsSet;
@@ -40,7 +32,6 @@ import okhttp3.mockwebserver.MockWebServer;
 import org.assertj.core.api.Assertions;
 import org.junit.BeforeClass;
 import org.junit.Test;
-
 import org.springframework.core.convert.converter.Converter;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.RequestEntity;
@@ -48,9 +39,26 @@ import org.springframework.http.ResponseEntity;
 import org.springframework.security.oauth2.core.OAuth2Error;
 import org.springframework.security.oauth2.core.OAuth2TokenValidator;
 import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
-import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
+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.web.client.RestOperations;
 
+import javax.crypto.SecretKey;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.EncodedKeySpec;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+import java.text.ParseException;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Map;
+
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode;
 import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
@@ -58,13 +66,13 @@ import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
-import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withJwkSetUri;
-import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withPublicKey;
+import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.*;
 
 /**
  * Tests for {@link NimbusJwtDecoder}
  *
  * @author Josh Cummings
+ * @author Joe Grandja
  */
 public class NimbusJwtDecoderTests {
 	private static final String JWK_SET = "{\"keys\":[{\"p\":\"49neceJFs8R6n7WamRGy45F5Tv0YM-R2ODK3eSBUSLOSH2tAqjEVKOkLE5fiNA3ygqq15NcKRadB2pTVf-Yb5ZIBuKzko8bzYIkIqYhSh_FAdEEr0vHF5fq_yWSvc6swsOJGqvBEtuqtJY027u-G2gAQasCQdhyejer68zsTn8M\",\"kty\":\"RSA\",\"q\":\"tWR-ysspjZ73B6p2vVRVyHwP3KQWL5KEQcdgcmMOE_P_cPs98vZJfLhxobXVmvzuEWBpRSiqiuyKlQnpstKt94Cy77iO8m8ISfF3C9VyLWXi9HUGAJb99irWABFl3sNDff5K2ODQ8CmuXLYM25OwN3ikbrhEJozlXg_NJFSGD4E\",\"d\":\"FkZHYZlw5KSoqQ1i2RA2kCUygSUOf1OqMt3uomtXuUmqKBm_bY7PCOhmwbvbn4xZYEeHuTR8Xix-0KpHe3NKyWrtRjkq1T_un49_1LLVUhJ0dL-9_x0xRquVjhl_XrsRXaGMEHs8G9pLTvXQ1uST585gxIfmCe0sxPZLvwoic-bXf64UZ9BGRV3lFexWJQqCZp2S21HfoU7wiz6kfLRNi-K4xiVNB1gswm_8o5lRuY7zB9bRARQ3TS2G4eW7p5sxT3CgsGiQD3_wPugU8iDplqAjgJ5ofNJXZezoj0t6JMB_qOpbrmAM1EnomIPebSLW7Ky9SugEd6KMdL5lW6AuAQ\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"qi\":\"wdkFu_tV2V1l_PWUUimG516Zvhqk2SWDw1F7uNDD-Lvrv_WNRIJVzuffZ8WYiPy8VvYQPJUrT2EXL8P0ocqwlaSTuXctrORcbjwgxDQDLsiZE0C23HYzgi0cofbScsJdhcBg7d07LAf7cdJWG0YVl1FkMCsxUlZ2wTwHfKWf-v4\",\"dp\":\"uwnPxqC-IxG4r33-SIT02kZC1IqC4aY7PWq0nePiDEQMQWpjjNH50rlq9EyLzbtdRdIouo-jyQXB01K15-XXJJ60dwrGLYNVqfsTd0eGqD1scYJGHUWG9IDgCsxyEnuG3s0AwbW2UolWVSsU2xMZGb9PurIUZECeD1XDZwMp2s0\",\"dq\":\"hra786AunB8TF35h8PpROzPoE9VJJMuLrc6Esm8eZXMwopf0yhxfN2FEAvUoTpLJu93-UH6DKenCgi16gnQ0_zt1qNNIVoRfg4rw_rjmsxCYHTVL3-RDeC8X_7TsEySxW0EgFTHh-nr6I6CQrAJjPM88T35KHtdFATZ7BCBB8AE\",\"n\":\"oXJ8OyOv_eRnce4akdanR4KYRfnC2zLV4uYNQpcFn6oHL0dj7D6kxQmsXoYgJV8ZVDn71KGmuLvolxsDncc2UrhyMBY6DVQVgMSVYaPCTgW76iYEKGgzTEw5IBRQL9w3SRJWd3VJTZZQjkXef48Ocz06PGF3lhbz4t5UEZtdF4rIe7u-977QwHuh7yRPBQ3sII-cVoOUMgaXB9SHcGF2iZCtPzL_IffDUcfhLQteGebhW8A6eUHgpD5A1PQ-JCw_G7UOzZAjjDjtNM2eqm8j-Ms_gqnm4MiCZ4E-9pDN77CAAPVN7kuX6ejs9KBXpk01z48i9fORYk9u7rAkh1HuQw\"}]}";
@@ -217,11 +225,9 @@ public class NimbusJwtDecoderTests {
 	}
 
 	@Test
-	public void jwsAlgorithmWhenNullOrEmptyThenThrowsException() {
+	public void jwsAlgorithmWhenNullThenThrowsException() {
 		NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder builder = withJwkSetUri(JWK_SET_URI);
 		Assertions.assertThatCode(() -> builder.jwsAlgorithm(null)).isInstanceOf(IllegalArgumentException.class);
-		Assertions.assertThatCode(() -> builder.jwsAlgorithm("")).isInstanceOf(IllegalArgumentException.class);
-		Assertions.assertThatCode(() -> builder.jwsAlgorithm("RS4096")).doesNotThrowAnyException();
 	}
 
 	@Test
@@ -239,7 +245,7 @@ public class NimbusJwtDecoderTests {
 	@Test
 	public void buildWhenSignatureAlgorithmMismatchesKeyTypeThenThrowsException() {
 		Assertions.assertThatCode(() -> withPublicKey(key())
-				.jwsAlgorithm(JwsAlgorithms.ES256)
+				.signatureAlgorithm(SignatureAlgorithm.ES256)
 				.build())
 				.isInstanceOf(IllegalStateException.class);
 	}
@@ -254,7 +260,7 @@ public class NimbusJwtDecoderTests {
 
 	@Test
 	public void decodeWhenUsingPublicKeyWithRs512ThenSuccessfullyDecodes() throws Exception {
-		NimbusJwtDecoder decoder = withPublicKey(key()).jwsAlgorithm(JwsAlgorithms.RS512).build();
+		NimbusJwtDecoder decoder = withPublicKey(key()).signatureAlgorithm(SignatureAlgorithm.RS512).build();
 		assertThat(decoder.decode(RS512_SIGNED_JWT))
 				.extracting(Jwt::getSubject)
 				.isEqualTo("test-subject");
@@ -262,17 +268,69 @@ public class NimbusJwtDecoderTests {
 
 	@Test
 	public void decodeWhenSignatureMismatchesAlgorithmThenThrowsException() throws Exception {
-		NimbusJwtDecoder decoder = withPublicKey(key()).jwsAlgorithm(JwsAlgorithms.RS512).build();
+		NimbusJwtDecoder decoder = withPublicKey(key()).signatureAlgorithm(SignatureAlgorithm.RS512).build();
 		Assertions.assertThatCode(() -> decoder.decode(RS256_SIGNED_JWT))
 				.isInstanceOf(JwtException.class);
 	}
 
+	@Test
+	public void withSecretKeyWhenNullThenThrowsIllegalArgumentException() {
+		assertThatThrownBy(() -> withSecretKey(null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("secretKey cannot be null");
+	}
+
+	@Test
+	public void withSecretKeyWhenMacAlgorithmNullThenThrowsIllegalArgumentException() {
+		SecretKey secretKey = TestKeys.DEFAULT_SECRET_KEY;
+		assertThatThrownBy(() -> withSecretKey(secretKey).macAlgorithm(null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("macAlgorithm cannot be null");
+	}
+
+	@Test
+	public void decodeWhenUsingSecretKeyThenSuccessfullyDecodes() throws Exception {
+		SecretKey secretKey = TestKeys.DEFAULT_SECRET_KEY;
+		MacAlgorithm macAlgorithm = MacAlgorithm.HS256;
+		JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+				.subject("test-subject")
+				.expirationTime(Date.from(Instant.now().plusSeconds(60)))
+				.build();
+		SignedJWT signedJWT = signedJwt(secretKey, macAlgorithm, claimsSet);
+		NimbusJwtDecoder decoder = withSecretKey(secretKey).macAlgorithm(macAlgorithm).build();
+		assertThat(decoder.decode(signedJWT.serialize()))
+				.extracting(Jwt::getSubject)
+				.isEqualTo("test-subject");
+	}
+
+	@Test
+	public void decodeWhenUsingSecretKeyAndIncorrectAlgorithmThenThrowsJwtException() throws Exception {
+		SecretKey secretKey = TestKeys.DEFAULT_SECRET_KEY;
+		MacAlgorithm macAlgorithm = MacAlgorithm.HS256;
+		JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+				.subject("test-subject")
+				.expirationTime(Date.from(Instant.now().plusSeconds(60)))
+				.build();
+		SignedJWT signedJWT = signedJwt(secretKey, macAlgorithm, claimsSet);
+		NimbusJwtDecoder decoder = withSecretKey(secretKey).macAlgorithm(MacAlgorithm.HS512).build();
+		assertThatThrownBy(() -> decoder.decode(signedJWT.serialize()))
+				.isInstanceOf(JwtException.class)
+				.hasMessage("An error occurred while attempting to decode the Jwt: Signed JWT rejected: Another algorithm expected, or no matching key(s) found");
+	}
+
 	private RSAPublicKey key() throws InvalidKeySpecException {
 		byte[] decoded = Base64.getDecoder().decode(VERIFY_KEY.getBytes());
 		EncodedKeySpec spec = new X509EncodedKeySpec(decoded);
 		return (RSAPublicKey) kf.generatePublic(spec);
 	}
 
+	private SignedJWT signedJwt(SecretKey secretKey, MacAlgorithm jwsAlgorithm, JWTClaimsSet claimsSet) throws Exception {
+		SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.parse(jwsAlgorithm.getName())), claimsSet);
+		JWSSigner signer = new MACSigner(secretKey);
+		signedJWT.sign(signer);
+		return signedJWT;
+	}
+
 	private static JWTProcessor<SecurityContext> withSigning(String jwkResponse) {
 		RestOperations restOperations = mock(RestOperations.class);
 		when(restOperations.exchange(any(RequestEntity.class), eq(String.class)))

+ 85 - 30
oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoderTests.java

@@ -16,50 +16,55 @@
 
 package org.springframework.security.oauth2.jwt;
 
-import java.net.UnknownHostException;
-import java.security.KeyFactory;
-import java.security.NoSuchAlgorithmException;
-import java.security.interfaces.RSAPublicKey;
-import java.security.spec.EncodedKeySpec;
-import java.security.spec.InvalidKeySpecException;
-import java.security.spec.X509EncodedKeySpec;
-import java.text.ParseException;
-import java.time.Instant;
-import java.util.Base64;
-import java.util.Collections;
-import java.util.Map;
-
+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.jwk.JWKSet;
+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.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
-import reactor.core.publisher.Flux;
-import reactor.core.publisher.Mono;
-
 import org.springframework.core.convert.converter.Converter;
 import org.springframework.security.oauth2.core.OAuth2Error;
 import org.springframework.security.oauth2.core.OAuth2TokenValidator;
 import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
-import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
+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.web.reactive.function.client.WebClient;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import javax.crypto.SecretKey;
+import java.net.UnknownHostException;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.EncodedKeySpec;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+import java.text.ParseException;
+import java.time.Instant;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Map;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatCode;
 import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-import static org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.withJwkSetUri;
-import static org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.withJwkSource;
-import static org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.withPublicKey;
+import static org.mockito.Mockito.*;
+import static org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.*;
 
 /**
  * @author Rob Winch
+ * @author Joe Grandja
  * @since 5.1
  */
 public class NimbusReactiveJwtDecoderTests {
@@ -236,11 +241,9 @@ public class NimbusReactiveJwtDecoderTests {
 	}
 
 	@Test
-	public void jwsAlgorithmWhenNullOrEmptyThenThrowsException() {
+	public void jwsAlgorithmWhenNullThenThrowsException() {
 		NimbusReactiveJwtDecoder.JwkSetUriReactiveJwtDecoderBuilder builder = withJwkSetUri(this.jwkSetUri);
 		assertThatCode(() -> builder.jwsAlgorithm(null)).isInstanceOf(IllegalArgumentException.class);
-		assertThatCode(() -> builder.jwsAlgorithm("")).isInstanceOf(IllegalArgumentException.class);
-		assertThatCode(() -> builder.jwsAlgorithm("RS4096")).doesNotThrowAnyException();
 	}
 
 	@Test
@@ -269,7 +272,7 @@ public class NimbusReactiveJwtDecoderTests {
 	@Test
 	public void buildWhenSignatureAlgorithmMismatchesKeyTypeThenThrowsException() {
 		assertThatCode(() -> withPublicKey(key())
-				.jwsAlgorithm(JwsAlgorithms.ES256)
+				.signatureAlgorithm(SignatureAlgorithm.ES256)
 				.build())
 				.isInstanceOf(IllegalStateException.class);
 	}
@@ -285,7 +288,7 @@ public class NimbusReactiveJwtDecoderTests {
 	@Test
 	public void decodeWhenUsingPublicKeyWithRs512ThenSuccessfullyDecodes() throws Exception {
 		NimbusReactiveJwtDecoder decoder =
-				withPublicKey(key()).jwsAlgorithm(JwsAlgorithms.RS512).build();
+				withPublicKey(key()).signatureAlgorithm(SignatureAlgorithm.RS512).build();
 		assertThat(decoder.decode(this.rsa512).block())
 				.extracting(Jwt::getSubject)
 				.isEqualTo("test-subject");
@@ -294,7 +297,7 @@ public class NimbusReactiveJwtDecoderTests {
 	@Test
 	public void decodeWhenSignatureMismatchesAlgorithmThenThrowsException() throws Exception {
 		NimbusReactiveJwtDecoder decoder =
-				withPublicKey(key()).jwsAlgorithm(JwsAlgorithms.RS512).build();
+				withPublicKey(key()).signatureAlgorithm(SignatureAlgorithm.RS512).build();
 		assertThatCode(() -> decoder.decode(this.rsa256).block())
 				.isInstanceOf(JwtException.class);
 	}
@@ -316,6 +319,58 @@ public class NimbusReactiveJwtDecoderTests {
 				.isNotNull();
 	}
 
+	@Test
+	public void withSecretKeyWhenSecretKeyNullThenThrowsIllegalArgumentException() {
+		assertThatThrownBy(() -> withSecretKey(null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("secretKey cannot be null");
+	}
+
+	@Test
+	public void withSecretKeyWhenMacAlgorithmNullThenThrowsIllegalArgumentException() {
+		SecretKey secretKey = TestKeys.DEFAULT_SECRET_KEY;
+		assertThatThrownBy(() -> withSecretKey(secretKey).macAlgorithm(null))
+				.isInstanceOf(IllegalArgumentException.class)
+				.hasMessage("macAlgorithm cannot be null");
+	}
+
+	@Test
+	public void decodeWhenSecretKeyThenSuccess() throws Exception {
+		SecretKey secretKey = TestKeys.DEFAULT_SECRET_KEY;
+		MacAlgorithm macAlgorithm = MacAlgorithm.HS256;
+		JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+				.subject("test-subject")
+				.expirationTime(Date.from(Instant.now().plusSeconds(60)))
+				.build();
+		SignedJWT signedJWT = signedJwt(secretKey, macAlgorithm, claimsSet);
+
+		this.decoder = withSecretKey(secretKey).macAlgorithm(macAlgorithm).build();
+		Jwt jwt = this.decoder.decode(signedJWT.serialize()).block();
+		assertThat(jwt.getSubject()).isEqualTo("test-subject");
+	}
+
+	@Test
+	public void decodeWhenSecretKeyAndAlgorithmMismatchThenThrowsJwtException() throws Exception {
+		SecretKey secretKey = TestKeys.DEFAULT_SECRET_KEY;
+		MacAlgorithm macAlgorithm = MacAlgorithm.HS256;
+		JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+				.subject("test-subject")
+				.expirationTime(Date.from(Instant.now().plusSeconds(60)))
+				.build();
+		SignedJWT signedJWT = signedJwt(secretKey, macAlgorithm, claimsSet);
+
+		this.decoder = withSecretKey(secretKey).macAlgorithm(MacAlgorithm.HS512).build();
+		assertThatThrownBy(() -> this.decoder.decode(signedJWT.serialize()).block())
+				.isInstanceOf(JwtException.class);
+	}
+
+	private SignedJWT signedJwt(SecretKey secretKey, MacAlgorithm jwsAlgorithm, JWTClaimsSet claimsSet) throws Exception {
+		SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.parse(jwsAlgorithm.getName())), claimsSet);
+		JWSSigner signer = new MACSigner(secretKey);
+		signedJWT.sign(signer);
+		return signedJWT;
+	}
+
 	private JWKSet parseJWKSet(String jwkSet) {
 		try {
 			return JWKSet.parse(jwkSet);