diff --git a/README.md b/README.md index 5aac0bb..0238573 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,8 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { > If you need further customization (like a leeway for JWT verification) use the `JwtWebSecurityConfigurer` signatures which accept a `JwtAuthenticationProvider`. +> If you need to configure several allowed issuers use the `JwtWebSecurityConfigurer` signatures which accept a `String[] issuers`. + Then using Spring Security `HttpSecurity` you can specify which paths requires authentication: diff --git a/lib/src/main/java/com/auth0/spring/security/api/JwtAuthenticationProvider.java b/lib/src/main/java/com/auth0/spring/security/api/JwtAuthenticationProvider.java index de276f2..1e9d938 100644 --- a/lib/src/main/java/com/auth0/spring/security/api/JwtAuthenticationProvider.java +++ b/lib/src/main/java/com/auth0/spring/security/api/JwtAuthenticationProvider.java @@ -21,23 +21,31 @@ public class JwtAuthenticationProvider implements AuthenticationProvider { private static Logger logger = LoggerFactory.getLogger(JwtAuthenticationProvider.class); private final byte[] secret; - private final String issuer; + private final String[] issuers; private final String audience; private final JwkProvider jwkProvider; private long leeway = 0; public JwtAuthenticationProvider(byte[] secret, String issuer, String audience) { + this(secret, new String[]{issuer}, audience); + } + + public JwtAuthenticationProvider(JwkProvider jwkProvider, String issuer, String audience) { + this(jwkProvider, new String[]{issuer}, audience); + } + + public JwtAuthenticationProvider(byte[] secret, String[] issuers, String audience) { this.secret = secret; - this.issuer = issuer; + this.issuers = issuers; this.audience = audience; this.jwkProvider = null; } - public JwtAuthenticationProvider(JwkProvider jwkProvider, String issuer, String audience) { + public JwtAuthenticationProvider(JwkProvider jwkProvider, String[] issuers, String audience) { this.jwkProvider = jwkProvider; this.secret = null; - this.issuer = issuer; + this.issuers = issuers; this.audience = audience; } @@ -76,7 +84,7 @@ public JwtAuthenticationProvider withJwtVerifierLeeway(long leeway) { private JWTVerifier jwtVerifier(JwtAuthentication authentication) throws AuthenticationException { if (secret != null) { - return providerForHS256(secret, issuer, audience, leeway); + return providerForHS256(secret, issuers, audience, leeway); } final String kid = authentication.getKeyId(); if (kid == null) { @@ -87,7 +95,7 @@ private JWTVerifier jwtVerifier(JwtAuthentication authentication) throws Authent } try { final Jwk jwk = jwkProvider.get(kid); - return providerForRS256((RSAPublicKey) jwk.getPublicKey(), issuer, audience, leeway); + return providerForRS256((RSAPublicKey) jwk.getPublicKey(), issuers, audience, leeway); } catch (SigningKeyNotFoundException e) { throw new AuthenticationServiceException("Could not retrieve jwks from issuer", e); } catch (InvalidPublicKeyException e) { @@ -97,17 +105,17 @@ private JWTVerifier jwtVerifier(JwtAuthentication authentication) throws Authent } } - private static JWTVerifier providerForRS256(RSAPublicKey publicKey, String issuer, String audience, long leeway) { + private static JWTVerifier providerForRS256(RSAPublicKey publicKey, String[] issuers, String audience, long leeway) { return JWT.require(Algorithm.RSA256(publicKey, null)) - .withIssuer(issuer) + .withIssuer(issuers) .withAudience(audience) .acceptLeeway(leeway) .build(); } - private static JWTVerifier providerForHS256(byte[] secret, String issuer, String audience, long leeway) { + private static JWTVerifier providerForHS256(byte[] secret, String[] issuers, String audience, long leeway) { return JWT.require(Algorithm.HMAC256(secret)) - .withIssuer(issuer) + .withIssuer(issuers) .withAudience(audience) .acceptLeeway(leeway) .build(); diff --git a/lib/src/main/java/com/auth0/spring/security/api/JwtWebSecurityConfigurer.java b/lib/src/main/java/com/auth0/spring/security/api/JwtWebSecurityConfigurer.java index 573eaad..9d32f99 100644 --- a/lib/src/main/java/com/auth0/spring/security/api/JwtWebSecurityConfigurer.java +++ b/lib/src/main/java/com/auth0/spring/security/api/JwtWebSecurityConfigurer.java @@ -13,12 +13,12 @@ public class JwtWebSecurityConfigurer { final String audience; - final String issuer; + final String[] issuers; final AuthenticationProvider provider; - private JwtWebSecurityConfigurer(String audience, String issuer, AuthenticationProvider authenticationProvider) { + private JwtWebSecurityConfigurer(String audience, String[] issuers, AuthenticationProvider authenticationProvider) { this.audience = audience; - this.issuer = issuer; + this.issuers = issuers; this.provider = authenticationProvider; } @@ -32,8 +32,7 @@ private JwtWebSecurityConfigurer(String audience, String issuer, AuthenticationP */ @SuppressWarnings({"WeakerAccess", "SameParameterValue"}) public static JwtWebSecurityConfigurer forRS256(String audience, String issuer) { - final JwkProvider jwkProvider = new JwkProviderBuilder(issuer).build(); - return new JwtWebSecurityConfigurer(audience, issuer, new JwtAuthenticationProvider(jwkProvider, issuer, audience)); + return forRS256(audience, new String[]{issuer}); } /** @@ -47,7 +46,35 @@ public static JwtWebSecurityConfigurer forRS256(String audience, String issuer) */ @SuppressWarnings({"WeakerAccess", "SameParameterValue"}) public static JwtWebSecurityConfigurer forRS256(String audience, String issuer, AuthenticationProvider provider) { - return new JwtWebSecurityConfigurer(audience, issuer, provider); + return forRS256(audience, new String[]{issuer}, provider); + } + + /** + * Configures application authorization for JWT signed with RS256. + * Will try to validate the token using the public key downloaded from "$issuer/.well-known/jwks.json" + * and matched by the value of {@code kid} of the JWT header + * @param audience identifier of the API and must match the {@code aud} value in the token + * @param issuers array of allowed issuers of the token for this API and one of the entries must match the {@code iss} value in the token + * @return JwtWebSecurityConfigurer for further configuration + */ + @SuppressWarnings({"WeakerAccess", "SameParameterValue"}) + public static JwtWebSecurityConfigurer forRS256(String audience, String[] issuers) { + final JwkProvider jwkProvider = new JwkProviderBuilder(issuers[0]).build(); // we use the first issuer for getting the jwkProvider + return new JwtWebSecurityConfigurer(audience, issuers, new JwtAuthenticationProvider(jwkProvider, issuers, audience)); + } + + /** + * Configures application authorization for JWT signed with RS256 + * Will try to validate the token using the public key downloaded from "$issuer/.well-known/jwks.json" + * and matched by the value of {@code kid} of the JWT header + * @param audience identifier of the API and must match the {@code aud} value in the token + * @param issuers array of allowed issuers of the token for this API and one of the entries must match the {@code iss} value in the token + * @param provider of Spring Authentication objects that can validate a {@link com.auth0.spring.security.api.authentication.PreAuthenticatedAuthenticationJsonWebToken} + * @return JwtWebSecurityConfigurer for further configuration + */ + @SuppressWarnings({"WeakerAccess", "SameParameterValue"}) + public static JwtWebSecurityConfigurer forRS256(String audience, String[] issuers, AuthenticationProvider provider) { + return new JwtWebSecurityConfigurer(audience, issuers, provider); } /** @@ -59,8 +86,7 @@ public static JwtWebSecurityConfigurer forRS256(String audience, String issuer, */ @SuppressWarnings({"WeakerAccess", "SameParameterValue"}) public static JwtWebSecurityConfigurer forHS256WithBase64Secret(String audience, String issuer, String secret) { - final byte[] secretBytes = new Base64(true).decode(secret); - return new JwtWebSecurityConfigurer(audience, issuer, new JwtAuthenticationProvider(secretBytes, issuer, audience)); + return forHS256WithBase64Secret(audience, new String[]{issuer}, secret); } /** @@ -72,7 +98,7 @@ public static JwtWebSecurityConfigurer forHS256WithBase64Secret(String audience, */ @SuppressWarnings({"WeakerAccess", "SameParameterValue"}) public static JwtWebSecurityConfigurer forHS256(String audience, String issuer, byte[] secret) { - return new JwtWebSecurityConfigurer(audience, issuer, new JwtAuthenticationProvider(secret, issuer, audience)); + return forHS256(audience, new String[]{issuer}, secret); } /** @@ -84,7 +110,44 @@ public static JwtWebSecurityConfigurer forHS256(String audience, String issuer, */ @SuppressWarnings({"WeakerAccess", "SameParameterValue"}) public static JwtWebSecurityConfigurer forHS256(String audience, String issuer, AuthenticationProvider provider) { - return new JwtWebSecurityConfigurer(audience, issuer, provider); + return forHS256(audience, new String[]{issuer}, provider); + } + + /** + * Configures application authorization for JWT signed with HS256 + * @param audience identifier of the API and must match the {@code aud} value in the token + * @param issuers array of allowed issuers of the token for this API and one of the entries must match the {@code iss} value in the token + * @param secret used to sign and verify tokens encoded in Base64 + * @return JwtWebSecurityConfigurer for further configuration + */ + @SuppressWarnings({"WeakerAccess", "SameParameterValue"}) + public static JwtWebSecurityConfigurer forHS256WithBase64Secret(String audience, String[] issuers, String secret) { + final byte[] secretBytes = new Base64(true).decode(secret); + return new JwtWebSecurityConfigurer(audience, issuers, new JwtAuthenticationProvider(secretBytes, issuers, audience)); + } + + /** + * Configures application authorization for JWT signed with HS256 + * @param audience identifier of the API and must match the {@code aud} value in the token + * @param issuers array of allowed issuers of the token for this API and one of the entries must match the {@code iss} value in the token + * @param secret used to sign and verify tokens + * @return JwtWebSecurityConfigurer for further configuration + */ + @SuppressWarnings({"WeakerAccess", "SameParameterValue"}) + public static JwtWebSecurityConfigurer forHS256(String audience, String[] issuers, byte[] secret) { + return new JwtWebSecurityConfigurer(audience, issuers, new JwtAuthenticationProvider(secret, issuers, audience)); + } + + /** + * Configures application authorization for JWT signed with HS256 + * @param audience identifier of the API and must match the {@code aud} value in the token + * @param issuers list of allowed issuers of the token for this API and one of the entries must match the {@code iss} value in the token + * @param provider of Spring Authentication objects that can validate a {@link com.auth0.spring.security.api.authentication.PreAuthenticatedAuthenticationJsonWebToken} + * @return JwtWebSecurityConfigurer for further configuration + */ + @SuppressWarnings({"WeakerAccess", "SameParameterValue"}) + public static JwtWebSecurityConfigurer forHS256(String audience, String[] issuers, AuthenticationProvider provider) { + return new JwtWebSecurityConfigurer(audience, issuers, provider); } /** diff --git a/lib/src/test/java/com/auth0/spring/security/api/JwtAuthenticationProviderTest.java b/lib/src/test/java/com/auth0/spring/security/api/JwtAuthenticationProviderTest.java index 957de09..426f6b3 100644 --- a/lib/src/test/java/com/auth0/spring/security/api/JwtAuthenticationProviderTest.java +++ b/lib/src/test/java/com/auth0/spring/security/api/JwtAuthenticationProviderTest.java @@ -52,6 +52,14 @@ public void shouldCreateUsingJWKProvider() throws Exception { assertThat(provider, is(notNullValue())); } + @Test + public void shouldCreateUsingJWKProviderAndIssuerIsNull() throws Exception { + JwkProvider jwkProvider = mock(JwkProvider.class); + String issuer = null; + JwtAuthenticationProvider provider = new JwtAuthenticationProvider(jwkProvider, issuer, "test-audience"); + + assertThat(provider, is(notNullValue())); + } @Test public void shouldSupportJwkAuthentication() throws Exception { JwtAuthenticationProvider provider = new JwtAuthenticationProvider("secret".getBytes(), "test-issuer", "test-audience"); @@ -127,6 +135,21 @@ public void shouldFailToAuthenticateUsingSecretIfIssuerClaimDoesNotMatch() throw provider.authenticate(authentication); } + @Test + public void shouldFailToAuthenticateUsingSecretIfIssuerClaimDoesNotMatchIssuersArray() throws Exception { + JwtAuthenticationProvider provider = new JwtAuthenticationProvider("secret".getBytes(), new String[]{"test-issuer1", "test-issuer2"}, "test-audience"); + String token = JWT.create() + .withAudience("test-audience") + .withIssuer("some-issuer") + .sign(Algorithm.HMAC256("secret")); + Authentication authentication = PreAuthenticatedAuthenticationJsonWebToken.usingToken(token); + + exception.expect(BadCredentialsException.class); + exception.expectMessage("Not a valid token"); + exception.expectCause(Matchers.instanceOf(InvalidClaimException.class)); + provider.authenticate(authentication); + } + @Test public void shouldFailToAuthenticateUsingSecretIfAudienceClaimDoesNotMatch() throws Exception { JwtAuthenticationProvider provider = new JwtAuthenticationProvider("secret".getBytes(), "test-issuer", "test-audience"); @@ -318,6 +341,30 @@ public void shouldFailToAuthenticateUsingJWKIfIssuerClaimDoesNotMatch() throws E provider.authenticate(authentication); } + @Test + public void shouldFailToAuthenticateUsingJWKIfIssuerClaimDoesNotMatchAllowedIssuers() throws Exception { + Jwk jwk = mock(Jwk.class); + JwkProvider jwkProvider = mock(JwkProvider.class); + + KeyPair keyPair = RSAKeyPair(); + when(jwkProvider.get(eq("key-id"))).thenReturn(jwk); + when(jwk.getPublicKey()).thenReturn(keyPair.getPublic()); + JwtAuthenticationProvider provider = new JwtAuthenticationProvider(jwkProvider, new String[]{"test-issuer1", "test-issuer2"}, "test-audience"); + Map keyIdHeader = Collections.singletonMap("kid", (Object) "key-id"); + String token = JWT.create() + .withAudience("test-audience") + .withIssuer("some-issuer") + .withHeader(keyIdHeader) + .sign(Algorithm.RSA256(null, (RSAPrivateKey) keyPair.getPrivate())); + + Authentication authentication = PreAuthenticatedAuthenticationJsonWebToken.usingToken(token); + + exception.expect(BadCredentialsException.class); + exception.expectMessage("Not a valid token"); + exception.expectCause(Matchers.instanceOf(InvalidClaimException.class)); + provider.authenticate(authentication); + } + @Test public void shouldFailToAuthenticateUsingJWKIfAudienceClaimDoesNotMatch() throws Exception { Jwk jwk = mock(Jwk.class); @@ -489,6 +536,30 @@ public void shouldAuthenticateUsingJWK() throws Exception { assertThat(result, is(not(equalTo(authentication)))); } + @Test + public void shouldAuthenticateUsingJWKAndSeveralAllowedIssuers() throws Exception { + Jwk jwk = mock(Jwk.class); + JwkProvider jwkProvider = mock(JwkProvider.class); + + KeyPair keyPair = RSAKeyPair(); + when(jwkProvider.get(eq("key-id"))).thenReturn(jwk); + when(jwk.getPublicKey()).thenReturn(keyPair.getPublic()); + JwtAuthenticationProvider provider = new JwtAuthenticationProvider(jwkProvider, new String[]{"test-issuer1", "test-issuer2"}, "test-audience"); + Map keyIdHeader = Collections.singletonMap("kid", (Object) "key-id"); + String token = JWT.create() + .withAudience("test-audience") + .withIssuer("test-issuer2") + .withHeader(keyIdHeader) + .sign(Algorithm.RSA256(null, (RSAPrivateKey) keyPair.getPrivate())); + + Authentication authentication = PreAuthenticatedAuthenticationJsonWebToken.usingToken(token); + + Authentication result = provider.authenticate(authentication); + + assertThat(result, is(notNullValue())); + assertThat(result, is(not(equalTo(authentication)))); + } + @Test public void shouldAuthenticateUsingJWKWithExpiredTokenAndLeeway() throws Exception { Calendar calendar = Calendar.getInstance(); diff --git a/lib/src/test/java/com/auth0/spring/security/api/JwtWebSecurityConfigurerTest.java b/lib/src/test/java/com/auth0/spring/security/api/JwtWebSecurityConfigurerTest.java index 7338a44..b26ae5f 100644 --- a/lib/src/test/java/com/auth0/spring/security/api/JwtWebSecurityConfigurerTest.java +++ b/lib/src/test/java/com/auth0/spring/security/api/JwtWebSecurityConfigurerTest.java @@ -15,7 +15,8 @@ public void shouldCreateRS256Configurer() throws Exception { assertThat(configurer, is(notNullValue())); assertThat(configurer.audience, is("audience")); - assertThat(configurer.issuer, is("issuer")); + assertThat(configurer.issuers, arrayWithSize(1)); + assertThat(configurer.issuers, arrayContaining("issuer")); assertThat(configurer.provider, is(notNullValue())); assertThat(configurer.provider, is(instanceOf(JwtAuthenticationProvider.class))); } @@ -27,7 +28,8 @@ public void shouldCreateRS256ConfigurerWithCustomAuthenticationProvider() throws assertThat(configurer, is(notNullValue())); assertThat(configurer.audience, is("audience")); - assertThat(configurer.issuer, is("issuer")); + assertThat(configurer.issuers, arrayWithSize(1)); + assertThat(configurer.issuers, arrayContaining("issuer")); assertThat(configurer.provider, is(notNullValue())); assertThat(configurer.provider, is(provider)); } @@ -38,7 +40,8 @@ public void shouldCreateHS256ConfigurerWithBase64EncodedSecret() throws Exceptio assertThat(configurer, is(notNullValue())); assertThat(configurer.audience, is("audience")); - assertThat(configurer.issuer, is("issuer")); + assertThat(configurer.issuers, arrayWithSize(1)); + assertThat(configurer.issuers, arrayContaining("issuer")); assertThat(configurer.provider, is(notNullValue())); assertThat(configurer.provider, is(instanceOf(JwtAuthenticationProvider.class))); } @@ -49,7 +52,20 @@ public void shouldCreateHS256Configurer() throws Exception { assertThat(configurer, is(notNullValue())); assertThat(configurer.audience, is("audience")); - assertThat(configurer.issuer, is("issuer")); + assertThat(configurer.issuers, arrayWithSize(1)); + assertThat(configurer.issuers, arrayContaining("issuer")); + assertThat(configurer.provider, is(notNullValue())); + assertThat(configurer.provider, is(instanceOf(JwtAuthenticationProvider.class))); + } + + @Test + public void shouldCreateHS256ConfigurerWithSeveralIssuers() throws Exception { + JwtWebSecurityConfigurer configurer = JwtWebSecurityConfigurer.forHS256("audience", new String[]{"issuer1", "issuer2"}, "secret".getBytes()); + + assertThat(configurer, is(notNullValue())); + assertThat(configurer.audience, is("audience")); + assertThat(configurer.issuers, arrayWithSize(2)); + assertThat(configurer.issuers, arrayContaining("issuer1", "issuer2")); assertThat(configurer.provider, is(notNullValue())); assertThat(configurer.provider, is(instanceOf(JwtAuthenticationProvider.class))); } @@ -61,7 +77,8 @@ public void shouldCreateHS256ConfigurerWithCustomAuthenticationProvider() throws assertThat(configurer, is(notNullValue())); assertThat(configurer.audience, is("audience")); - assertThat(configurer.issuer, is("issuer")); + assertThat(configurer.issuers, arrayWithSize(1)); + assertThat(configurer.issuers, arrayContaining("issuer")); assertThat(configurer.provider, is(notNullValue())); assertThat(configurer.provider, is(provider)); }