From bed3371b8085472b3c35d2dfcd025c6763d4f24b Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Thu, 11 Apr 2019 05:05:16 -0400 Subject: [PATCH] Support symmetric key for JwtDecoder Fixes gh-5465 --- .../OidcIdTokenDecoderFactory.java | 103 ++++++++++-- .../ReactiveOidcIdTokenDecoderFactory.java | 97 ++++++++++- .../OidcIdTokenDecoderFactoryTests.java | 74 ++++++++- ...eactiveOidcIdTokenDecoderFactoryTests.java | 74 ++++++++- .../oauth2/jose/jws/JwsAlgorithm.java | 33 ++++ .../oauth2/jose/jws/MacAlgorithm.java | 77 +++++++++ .../oauth2/jose/jws/SignatureAlgorithm.java | 107 ++++++++++++ .../security/oauth2/jwt/NimbusJwtDecoder.java | 117 +++++++++---- .../jwt/NimbusJwtDecoderJwkSupport.java | 9 +- .../oauth2/jwt/NimbusReactiveJwtDecoder.java | 157 +++++++++++++----- .../security/oauth2/jose/TestKeys.java | 32 ++++ .../oauth2/jose/jws/MacAlgorithmTests.java | 41 +++++ .../jose/jws/SignatureAlgorithmTests.java | 47 ++++++ .../oauth2/jwt/NimbusJwtDecoderTests.java | 102 +++++++++--- .../jwt/NimbusReactiveJwtDecoderTests.java | 115 +++++++++---- 15 files changed, 1024 insertions(+), 161 deletions(-) create mode 100644 oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/JwsAlgorithm.java create mode 100644 oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/MacAlgorithm.java create mode 100644 oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/SignatureAlgorithm.java create mode 100644 oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/TestKeys.java create mode 100644 oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/jws/MacAlgorithmTests.java create mode 100644 oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/jws/SignatureAlgorithmTests.java diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenDecoderFactory.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenDecoderFactory.java index d327f006c4..37876d8489 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenDecoderFactory.java +++ b/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 { private static final String MISSING_SIGNATURE_VERIFIER_ERROR_CODE = "missing_signature_verifier"; + private static Map jcaAlgorithmMappings = new HashMap() { + { + put(MacAlgorithm.HS256, "HmacSHA256"); + put(MacAlgorithm.HS384, "HmacSHA384"); + put(MacAlgorithm.HS512, "HmacSHA512"); + } + }; private final Map jwtDecoders = new ConcurrentHashMap<>(); private Function> jwtValidatorFactory = OidcIdTokenValidator::new; + private Function 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 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 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 jwsAlgorithmResolver) { + Assert.notNull(jwsAlgorithmResolver, "jwsAlgorithmResolver cannot be null"); + this.jwsAlgorithmResolver = jwsAlgorithmResolver; + } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/ReactiveOidcIdTokenDecoderFactory.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/ReactiveOidcIdTokenDecoderFactory.java index ae93a75092..d5b65722be 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/ReactiveOidcIdTokenDecoderFactory.java +++ b/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 { private static final String MISSING_SIGNATURE_VERIFIER_ERROR_CODE = "missing_signature_verifier"; + private static Map jcaAlgorithmMappings = new HashMap() { + { + put(MacAlgorithm.HS256, "HmacSHA256"); + put(MacAlgorithm.HS384, "HmacSHA384"); + put(MacAlgorithm.HS512, "HmacSHA512"); + } + }; private final Map jwtDecoders = new ConcurrentHashMap<>(); private Function> jwtValidatorFactory = OidcIdTokenValidator::new; + private Function 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 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 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 jwsAlgorithmResolver) { + Assert.notNull(jwsAlgorithmResolver, "jwsAlgorithmResolver cannot be null"); + this.jwsAlgorithmResolver = jwsAlgorithmResolver; + } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenDecoderFactoryTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenDecoderFactoryTests.java index d87e6cb55a..b8da6af4af 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenDecoderFactoryTests.java +++ b/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> 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> 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 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)); } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/ReactiveOidcIdTokenDecoderFactoryTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/ReactiveOidcIdTokenDecoderFactoryTests.java index 65d68a9a75..f1d15284e3 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/ReactiveOidcIdTokenDecoderFactoryTests.java +++ b/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> 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> 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 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)); } } diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/JwsAlgorithm.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/JwsAlgorithm.java new file mode 100644 index 0000000000..d815208940 --- /dev/null +++ b/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 JSON Web Algorithms (JWA) + * @see JSON Web Signature (JWS) + * @see Cryptographic Algorithms for Digital Signatures and MACs + */ +public interface JwsAlgorithm { + + String getName(); + +} diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/MacAlgorithm.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/MacAlgorithm.java new file mode 100644 index 0000000000..cd18ded687 --- /dev/null +++ b/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 JSON Web Algorithms (JWA) + * @see JSON Web Signature (JWS) + * @see Cryptographic Algorithms for Digital Signatures and MACs + */ +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; + } +} diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/SignatureAlgorithm.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jose/jws/SignatureAlgorithm.java new file mode 100644 index 0000000000..980fb81366 --- /dev/null +++ b/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 JSON Web Algorithms (JWA) + * @see JSON Web Signature (JWS) + * @see Cryptographic Algorithms for Digital Signatures and MACs + */ +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; + } +} diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java index f41278c5e3..0ffcfe4bb8 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java +++ b/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 * JWK Set 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 * RS256, RS384, or RS512. * - * @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 + * algorithm + * when generating the MAC. + * + * The value should be one of + * HS256, HS384 or HS512. + * + * @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 processor() { + JWKSource jwkSource = new ImmutableSecret<>(this.secretKey); + JWSKeySelector jwsKeySelector = + new JWSVerificationKeySelector<>(this.jwsAlgorithm, jwkSource); + DefaultJWTProcessor jwtProcessor = new DefaultJWTProcessor<>(); + jwtProcessor.setJWSKeySelector(jwsKeySelector); + + // Spring Security validates the claim set independent from Nimbus + jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> { }); + + return jwtProcessor; + } + } } diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupport.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupport.java index 0df754124a..0f51c879eb 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderJwkSupport.java +++ b/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(); } diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java index 4a12d23619..5eb815a5e3 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java +++ b/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 "decodes" 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). * *

* NOTE: This implementation uses the Nimbus JOSE + JWT SDK internally. * * @author Rob Winch + * @author Joe Grandja * @since 5.1 * @see ReactiveJwtDecoder * @see JSON Web Token (JWT) @@ -75,22 +78,34 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder { private final Converter> jwtProcessor; private OAuth2TokenValidator jwtValidator = JwtValidators.createDefault(); - private Converter, Map> claimSetConverter = MappedJwtClaimSetConverter - .withDefaults(Collections.emptyMap()); - - public NimbusReactiveJwtDecoder(RSAPublicKey publicKey) { - this.jwtProcessor = withPublicKey(publicKey).processor(); - } + private Converter, Map> 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> 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 * RS256, RS384, or RS512. * - * @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 + * algorithm + * when generating the MAC. + * + * The value should be one of + * HS256, HS384 or HS512. + * + * @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> processor() { + JWKSource jwkSource = new ImmutableSecret<>(this.secretKey); + JWSKeySelector jwsKeySelector = + new JWSVerificationKeySelector<>(this.jwsAlgorithm, jwkSource); + DefaultJWTProcessor 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> jwkSource; + private final Function> jwkSource; private JWSAlgorithm jwsAlgorithm = JWSAlgorithm.RS256; private JwkSourceReactiveJwtDecoderBuilder(Function> 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; } diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/TestKeys.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/TestKeys.java new file mode 100644 index 0000000000..525bf98fe1 --- /dev/null +++ b/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"); + +} diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/jws/MacAlgorithmTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/jws/MacAlgorithmTests.java new file mode 100644 index 0000000000..cea7ef5054 --- /dev/null +++ b/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(); + } +} diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/jws/SignatureAlgorithmTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jose/jws/SignatureAlgorithmTests.java new file mode 100644 index 0000000000..4b2c19f018 --- /dev/null +++ b/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(); + } +} diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderTests.java index 6db71fda49..646c0d4bd4 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderTests.java +++ b/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 withSigning(String jwkResponse) { RestOperations restOperations = mock(RestOperations.class); when(restOperations.exchange(any(RequestEntity.class), eq(String.class))) diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoderTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoderTests.java index 96bdddc75f..25640c41c0 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoderTests.java +++ b/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);