Skip to content

Commit

Permalink
Deal with list of issuers in JwtAuthenticationProvider (#30)
Browse files Browse the repository at this point in the history
* Deal with list of issuers in JwtAuthenticationProvider

* Added hint for several issuers feature in README.md

Co-authored-by: Sebastian Heupts <[email protected]>
Co-authored-by: Jim Anderson <[email protected]>
  • Loading branch information
3 people committed Jun 18, 2020
1 parent 94c10c1 commit 4bc371a
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 25 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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});
}

/**
Expand All @@ -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);
}

/**
Expand All @@ -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);
}

/**
Expand All @@ -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);
}

/**
Expand All @@ -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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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.<Throwable>instanceOf(InvalidClaimException.class));
provider.authenticate(authentication);
}

@Test
public void shouldFailToAuthenticateUsingSecretIfAudienceClaimDoesNotMatch() throws Exception {
JwtAuthenticationProvider provider = new JwtAuthenticationProvider("secret".getBytes(), "test-issuer", "test-audience");
Expand Down Expand Up @@ -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<String, Object> 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.<Throwable>instanceOf(InvalidClaimException.class));
provider.authenticate(authentication);
}

@Test
public void shouldFailToAuthenticateUsingJWKIfAudienceClaimDoesNotMatch() throws Exception {
Jwk jwk = mock(Jwk.class);
Expand Down Expand Up @@ -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<String, Object> 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
}
Expand All @@ -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));
}
Expand All @@ -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)));
}
Expand All @@ -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)));
}
Expand All @@ -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));
}
Expand Down

0 comments on commit 4bc371a

Please sign in to comment.