Skip to content

Commit 4bc371a

Browse files
coperatorSebastian Heuptsjimmyjames
authored
Deal with list of issuers in JwtAuthenticationProvider (#30)
* Deal with list of issuers in JwtAuthenticationProvider * Added hint for several issuers feature in README.md Co-authored-by: Sebastian Heupts <sebastian.heupts@iese.fraunhofer.de> Co-authored-by: Jim Anderson <jim.anderson@auth0.com>
1 parent 94c10c1 commit 4bc371a

5 files changed

Lines changed: 186 additions & 25 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
6464

6565
> If you need further customization (like a leeway for JWT verification) use the `JwtWebSecurityConfigurer` signatures which accept a `JwtAuthenticationProvider`.
6666
67+
> If you need to configure several allowed issuers use the `JwtWebSecurityConfigurer` signatures which accept a `String[] issuers`.
68+
6769

6870
Then using Spring Security `HttpSecurity` you can specify which paths requires authentication:
6971

lib/src/main/java/com/auth0/spring/security/api/JwtAuthenticationProvider.java

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,23 +21,31 @@ public class JwtAuthenticationProvider implements AuthenticationProvider {
2121
private static Logger logger = LoggerFactory.getLogger(JwtAuthenticationProvider.class);
2222

2323
private final byte[] secret;
24-
private final String issuer;
24+
private final String[] issuers;
2525
private final String audience;
2626
private final JwkProvider jwkProvider;
2727

2828
private long leeway = 0;
2929

3030
public JwtAuthenticationProvider(byte[] secret, String issuer, String audience) {
31+
this(secret, new String[]{issuer}, audience);
32+
}
33+
34+
public JwtAuthenticationProvider(JwkProvider jwkProvider, String issuer, String audience) {
35+
this(jwkProvider, new String[]{issuer}, audience);
36+
}
37+
38+
public JwtAuthenticationProvider(byte[] secret, String[] issuers, String audience) {
3139
this.secret = secret;
32-
this.issuer = issuer;
40+
this.issuers = issuers;
3341
this.audience = audience;
3442
this.jwkProvider = null;
3543
}
3644

37-
public JwtAuthenticationProvider(JwkProvider jwkProvider, String issuer, String audience) {
45+
public JwtAuthenticationProvider(JwkProvider jwkProvider, String[] issuers, String audience) {
3846
this.jwkProvider = jwkProvider;
3947
this.secret = null;
40-
this.issuer = issuer;
48+
this.issuers = issuers;
4149
this.audience = audience;
4250
}
4351

@@ -76,7 +84,7 @@ public JwtAuthenticationProvider withJwtVerifierLeeway(long leeway) {
7684

7785
private JWTVerifier jwtVerifier(JwtAuthentication authentication) throws AuthenticationException {
7886
if (secret != null) {
79-
return providerForHS256(secret, issuer, audience, leeway);
87+
return providerForHS256(secret, issuers, audience, leeway);
8088
}
8189
final String kid = authentication.getKeyId();
8290
if (kid == null) {
@@ -87,7 +95,7 @@ private JWTVerifier jwtVerifier(JwtAuthentication authentication) throws Authent
8795
}
8896
try {
8997
final Jwk jwk = jwkProvider.get(kid);
90-
return providerForRS256((RSAPublicKey) jwk.getPublicKey(), issuer, audience, leeway);
98+
return providerForRS256((RSAPublicKey) jwk.getPublicKey(), issuers, audience, leeway);
9199
} catch (SigningKeyNotFoundException e) {
92100
throw new AuthenticationServiceException("Could not retrieve jwks from issuer", e);
93101
} catch (InvalidPublicKeyException e) {
@@ -97,17 +105,17 @@ private JWTVerifier jwtVerifier(JwtAuthentication authentication) throws Authent
97105
}
98106
}
99107

100-
private static JWTVerifier providerForRS256(RSAPublicKey publicKey, String issuer, String audience, long leeway) {
108+
private static JWTVerifier providerForRS256(RSAPublicKey publicKey, String[] issuers, String audience, long leeway) {
101109
return JWT.require(Algorithm.RSA256(publicKey, null))
102-
.withIssuer(issuer)
110+
.withIssuer(issuers)
103111
.withAudience(audience)
104112
.acceptLeeway(leeway)
105113
.build();
106114
}
107115

108-
private static JWTVerifier providerForHS256(byte[] secret, String issuer, String audience, long leeway) {
116+
private static JWTVerifier providerForHS256(byte[] secret, String[] issuers, String audience, long leeway) {
109117
return JWT.require(Algorithm.HMAC256(secret))
110-
.withIssuer(issuer)
118+
.withIssuer(issuers)
111119
.withAudience(audience)
112120
.acceptLeeway(leeway)
113121
.build();

lib/src/main/java/com/auth0/spring/security/api/JwtWebSecurityConfigurer.java

Lines changed: 73 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@
1313
public class JwtWebSecurityConfigurer {
1414

1515
final String audience;
16-
final String issuer;
16+
final String[] issuers;
1717
final AuthenticationProvider provider;
1818

19-
private JwtWebSecurityConfigurer(String audience, String issuer, AuthenticationProvider authenticationProvider) {
19+
private JwtWebSecurityConfigurer(String audience, String[] issuers, AuthenticationProvider authenticationProvider) {
2020
this.audience = audience;
21-
this.issuer = issuer;
21+
this.issuers = issuers;
2222
this.provider = authenticationProvider;
2323
}
2424

@@ -32,8 +32,7 @@ private JwtWebSecurityConfigurer(String audience, String issuer, AuthenticationP
3232
*/
3333
@SuppressWarnings({"WeakerAccess", "SameParameterValue"})
3434
public static JwtWebSecurityConfigurer forRS256(String audience, String issuer) {
35-
final JwkProvider jwkProvider = new JwkProviderBuilder(issuer).build();
36-
return new JwtWebSecurityConfigurer(audience, issuer, new JwtAuthenticationProvider(jwkProvider, issuer, audience));
35+
return forRS256(audience, new String[]{issuer});
3736
}
3837

3938
/**
@@ -47,7 +46,35 @@ public static JwtWebSecurityConfigurer forRS256(String audience, String issuer)
4746
*/
4847
@SuppressWarnings({"WeakerAccess", "SameParameterValue"})
4948
public static JwtWebSecurityConfigurer forRS256(String audience, String issuer, AuthenticationProvider provider) {
50-
return new JwtWebSecurityConfigurer(audience, issuer, provider);
49+
return forRS256(audience, new String[]{issuer}, provider);
50+
}
51+
52+
/**
53+
* Configures application authorization for JWT signed with RS256.
54+
* Will try to validate the token using the public key downloaded from "$issuer/.well-known/jwks.json"
55+
* and matched by the value of {@code kid} of the JWT header
56+
* @param audience identifier of the API and must match the {@code aud} value in the token
57+
* @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
58+
* @return JwtWebSecurityConfigurer for further configuration
59+
*/
60+
@SuppressWarnings({"WeakerAccess", "SameParameterValue"})
61+
public static JwtWebSecurityConfigurer forRS256(String audience, String[] issuers) {
62+
final JwkProvider jwkProvider = new JwkProviderBuilder(issuers[0]).build(); // we use the first issuer for getting the jwkProvider
63+
return new JwtWebSecurityConfigurer(audience, issuers, new JwtAuthenticationProvider(jwkProvider, issuers, audience));
64+
}
65+
66+
/**
67+
* Configures application authorization for JWT signed with RS256
68+
* Will try to validate the token using the public key downloaded from "$issuer/.well-known/jwks.json"
69+
* and matched by the value of {@code kid} of the JWT header
70+
* @param audience identifier of the API and must match the {@code aud} value in the token
71+
* @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
72+
* @param provider of Spring Authentication objects that can validate a {@link com.auth0.spring.security.api.authentication.PreAuthenticatedAuthenticationJsonWebToken}
73+
* @return JwtWebSecurityConfigurer for further configuration
74+
*/
75+
@SuppressWarnings({"WeakerAccess", "SameParameterValue"})
76+
public static JwtWebSecurityConfigurer forRS256(String audience, String[] issuers, AuthenticationProvider provider) {
77+
return new JwtWebSecurityConfigurer(audience, issuers, provider);
5178
}
5279

5380
/**
@@ -59,8 +86,7 @@ public static JwtWebSecurityConfigurer forRS256(String audience, String issuer,
5986
*/
6087
@SuppressWarnings({"WeakerAccess", "SameParameterValue"})
6188
public static JwtWebSecurityConfigurer forHS256WithBase64Secret(String audience, String issuer, String secret) {
62-
final byte[] secretBytes = new Base64(true).decode(secret);
63-
return new JwtWebSecurityConfigurer(audience, issuer, new JwtAuthenticationProvider(secretBytes, issuer, audience));
89+
return forHS256WithBase64Secret(audience, new String[]{issuer}, secret);
6490
}
6591

6692
/**
@@ -72,7 +98,7 @@ public static JwtWebSecurityConfigurer forHS256WithBase64Secret(String audience,
7298
*/
7399
@SuppressWarnings({"WeakerAccess", "SameParameterValue"})
74100
public static JwtWebSecurityConfigurer forHS256(String audience, String issuer, byte[] secret) {
75-
return new JwtWebSecurityConfigurer(audience, issuer, new JwtAuthenticationProvider(secret, issuer, audience));
101+
return forHS256(audience, new String[]{issuer}, secret);
76102
}
77103

78104
/**
@@ -84,7 +110,44 @@ public static JwtWebSecurityConfigurer forHS256(String audience, String issuer,
84110
*/
85111
@SuppressWarnings({"WeakerAccess", "SameParameterValue"})
86112
public static JwtWebSecurityConfigurer forHS256(String audience, String issuer, AuthenticationProvider provider) {
87-
return new JwtWebSecurityConfigurer(audience, issuer, provider);
113+
return forHS256(audience, new String[]{issuer}, provider);
114+
}
115+
116+
/**
117+
* Configures application authorization for JWT signed with HS256
118+
* @param audience identifier of the API and must match the {@code aud} value in the token
119+
* @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
120+
* @param secret used to sign and verify tokens encoded in Base64
121+
* @return JwtWebSecurityConfigurer for further configuration
122+
*/
123+
@SuppressWarnings({"WeakerAccess", "SameParameterValue"})
124+
public static JwtWebSecurityConfigurer forHS256WithBase64Secret(String audience, String[] issuers, String secret) {
125+
final byte[] secretBytes = new Base64(true).decode(secret);
126+
return new JwtWebSecurityConfigurer(audience, issuers, new JwtAuthenticationProvider(secretBytes, issuers, audience));
127+
}
128+
129+
/**
130+
* Configures application authorization for JWT signed with HS256
131+
* @param audience identifier of the API and must match the {@code aud} value in the token
132+
* @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
133+
* @param secret used to sign and verify tokens
134+
* @return JwtWebSecurityConfigurer for further configuration
135+
*/
136+
@SuppressWarnings({"WeakerAccess", "SameParameterValue"})
137+
public static JwtWebSecurityConfigurer forHS256(String audience, String[] issuers, byte[] secret) {
138+
return new JwtWebSecurityConfigurer(audience, issuers, new JwtAuthenticationProvider(secret, issuers, audience));
139+
}
140+
141+
/**
142+
* Configures application authorization for JWT signed with HS256
143+
* @param audience identifier of the API and must match the {@code aud} value in the token
144+
* @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
145+
* @param provider of Spring Authentication objects that can validate a {@link com.auth0.spring.security.api.authentication.PreAuthenticatedAuthenticationJsonWebToken}
146+
* @return JwtWebSecurityConfigurer for further configuration
147+
*/
148+
@SuppressWarnings({"WeakerAccess", "SameParameterValue"})
149+
public static JwtWebSecurityConfigurer forHS256(String audience, String[] issuers, AuthenticationProvider provider) {
150+
return new JwtWebSecurityConfigurer(audience, issuers, provider);
88151
}
89152

90153
/**

lib/src/test/java/com/auth0/spring/security/api/JwtAuthenticationProviderTest.java

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@ public void shouldCreateUsingJWKProvider() throws Exception {
5252
assertThat(provider, is(notNullValue()));
5353
}
5454

55+
@Test
56+
public void shouldCreateUsingJWKProviderAndIssuerIsNull() throws Exception {
57+
JwkProvider jwkProvider = mock(JwkProvider.class);
58+
String issuer = null;
59+
JwtAuthenticationProvider provider = new JwtAuthenticationProvider(jwkProvider, issuer, "test-audience");
60+
61+
assertThat(provider, is(notNullValue()));
62+
}
5563
@Test
5664
public void shouldSupportJwkAuthentication() throws Exception {
5765
JwtAuthenticationProvider provider = new JwtAuthenticationProvider("secret".getBytes(), "test-issuer", "test-audience");
@@ -127,6 +135,21 @@ public void shouldFailToAuthenticateUsingSecretIfIssuerClaimDoesNotMatch() throw
127135
provider.authenticate(authentication);
128136
}
129137

138+
@Test
139+
public void shouldFailToAuthenticateUsingSecretIfIssuerClaimDoesNotMatchIssuersArray() throws Exception {
140+
JwtAuthenticationProvider provider = new JwtAuthenticationProvider("secret".getBytes(), new String[]{"test-issuer1", "test-issuer2"}, "test-audience");
141+
String token = JWT.create()
142+
.withAudience("test-audience")
143+
.withIssuer("some-issuer")
144+
.sign(Algorithm.HMAC256("secret"));
145+
Authentication authentication = PreAuthenticatedAuthenticationJsonWebToken.usingToken(token);
146+
147+
exception.expect(BadCredentialsException.class);
148+
exception.expectMessage("Not a valid token");
149+
exception.expectCause(Matchers.<Throwable>instanceOf(InvalidClaimException.class));
150+
provider.authenticate(authentication);
151+
}
152+
130153
@Test
131154
public void shouldFailToAuthenticateUsingSecretIfAudienceClaimDoesNotMatch() throws Exception {
132155
JwtAuthenticationProvider provider = new JwtAuthenticationProvider("secret".getBytes(), "test-issuer", "test-audience");
@@ -318,6 +341,30 @@ public void shouldFailToAuthenticateUsingJWKIfIssuerClaimDoesNotMatch() throws E
318341
provider.authenticate(authentication);
319342
}
320343

344+
@Test
345+
public void shouldFailToAuthenticateUsingJWKIfIssuerClaimDoesNotMatchAllowedIssuers() throws Exception {
346+
Jwk jwk = mock(Jwk.class);
347+
JwkProvider jwkProvider = mock(JwkProvider.class);
348+
349+
KeyPair keyPair = RSAKeyPair();
350+
when(jwkProvider.get(eq("key-id"))).thenReturn(jwk);
351+
when(jwk.getPublicKey()).thenReturn(keyPair.getPublic());
352+
JwtAuthenticationProvider provider = new JwtAuthenticationProvider(jwkProvider, new String[]{"test-issuer1", "test-issuer2"}, "test-audience");
353+
Map<String, Object> keyIdHeader = Collections.singletonMap("kid", (Object) "key-id");
354+
String token = JWT.create()
355+
.withAudience("test-audience")
356+
.withIssuer("some-issuer")
357+
.withHeader(keyIdHeader)
358+
.sign(Algorithm.RSA256(null, (RSAPrivateKey) keyPair.getPrivate()));
359+
360+
Authentication authentication = PreAuthenticatedAuthenticationJsonWebToken.usingToken(token);
361+
362+
exception.expect(BadCredentialsException.class);
363+
exception.expectMessage("Not a valid token");
364+
exception.expectCause(Matchers.<Throwable>instanceOf(InvalidClaimException.class));
365+
provider.authenticate(authentication);
366+
}
367+
321368
@Test
322369
public void shouldFailToAuthenticateUsingJWKIfAudienceClaimDoesNotMatch() throws Exception {
323370
Jwk jwk = mock(Jwk.class);
@@ -489,6 +536,30 @@ public void shouldAuthenticateUsingJWK() throws Exception {
489536
assertThat(result, is(not(equalTo(authentication))));
490537
}
491538

539+
@Test
540+
public void shouldAuthenticateUsingJWKAndSeveralAllowedIssuers() throws Exception {
541+
Jwk jwk = mock(Jwk.class);
542+
JwkProvider jwkProvider = mock(JwkProvider.class);
543+
544+
KeyPair keyPair = RSAKeyPair();
545+
when(jwkProvider.get(eq("key-id"))).thenReturn(jwk);
546+
when(jwk.getPublicKey()).thenReturn(keyPair.getPublic());
547+
JwtAuthenticationProvider provider = new JwtAuthenticationProvider(jwkProvider, new String[]{"test-issuer1", "test-issuer2"}, "test-audience");
548+
Map<String, Object> keyIdHeader = Collections.singletonMap("kid", (Object) "key-id");
549+
String token = JWT.create()
550+
.withAudience("test-audience")
551+
.withIssuer("test-issuer2")
552+
.withHeader(keyIdHeader)
553+
.sign(Algorithm.RSA256(null, (RSAPrivateKey) keyPair.getPrivate()));
554+
555+
Authentication authentication = PreAuthenticatedAuthenticationJsonWebToken.usingToken(token);
556+
557+
Authentication result = provider.authenticate(authentication);
558+
559+
assertThat(result, is(notNullValue()));
560+
assertThat(result, is(not(equalTo(authentication))));
561+
}
562+
492563
@Test
493564
public void shouldAuthenticateUsingJWKWithExpiredTokenAndLeeway() throws Exception {
494565
Calendar calendar = Calendar.getInstance();

lib/src/test/java/com/auth0/spring/security/api/JwtWebSecurityConfigurerTest.java

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ public void shouldCreateRS256Configurer() throws Exception {
1515

1616
assertThat(configurer, is(notNullValue()));
1717
assertThat(configurer.audience, is("audience"));
18-
assertThat(configurer.issuer, is("issuer"));
18+
assertThat(configurer.issuers, arrayWithSize(1));
19+
assertThat(configurer.issuers, arrayContaining("issuer"));
1920
assertThat(configurer.provider, is(notNullValue()));
2021
assertThat(configurer.provider, is(instanceOf(JwtAuthenticationProvider.class)));
2122
}
@@ -27,7 +28,8 @@ public void shouldCreateRS256ConfigurerWithCustomAuthenticationProvider() throws
2728

2829
assertThat(configurer, is(notNullValue()));
2930
assertThat(configurer.audience, is("audience"));
30-
assertThat(configurer.issuer, is("issuer"));
31+
assertThat(configurer.issuers, arrayWithSize(1));
32+
assertThat(configurer.issuers, arrayContaining("issuer"));
3133
assertThat(configurer.provider, is(notNullValue()));
3234
assertThat(configurer.provider, is(provider));
3335
}
@@ -38,7 +40,8 @@ public void shouldCreateHS256ConfigurerWithBase64EncodedSecret() throws Exceptio
3840

3941
assertThat(configurer, is(notNullValue()));
4042
assertThat(configurer.audience, is("audience"));
41-
assertThat(configurer.issuer, is("issuer"));
43+
assertThat(configurer.issuers, arrayWithSize(1));
44+
assertThat(configurer.issuers, arrayContaining("issuer"));
4245
assertThat(configurer.provider, is(notNullValue()));
4346
assertThat(configurer.provider, is(instanceOf(JwtAuthenticationProvider.class)));
4447
}
@@ -49,7 +52,20 @@ public void shouldCreateHS256Configurer() throws Exception {
4952

5053
assertThat(configurer, is(notNullValue()));
5154
assertThat(configurer.audience, is("audience"));
52-
assertThat(configurer.issuer, is("issuer"));
55+
assertThat(configurer.issuers, arrayWithSize(1));
56+
assertThat(configurer.issuers, arrayContaining("issuer"));
57+
assertThat(configurer.provider, is(notNullValue()));
58+
assertThat(configurer.provider, is(instanceOf(JwtAuthenticationProvider.class)));
59+
}
60+
61+
@Test
62+
public void shouldCreateHS256ConfigurerWithSeveralIssuers() throws Exception {
63+
JwtWebSecurityConfigurer configurer = JwtWebSecurityConfigurer.forHS256("audience", new String[]{"issuer1", "issuer2"}, "secret".getBytes());
64+
65+
assertThat(configurer, is(notNullValue()));
66+
assertThat(configurer.audience, is("audience"));
67+
assertThat(configurer.issuers, arrayWithSize(2));
68+
assertThat(configurer.issuers, arrayContaining("issuer1", "issuer2"));
5369
assertThat(configurer.provider, is(notNullValue()));
5470
assertThat(configurer.provider, is(instanceOf(JwtAuthenticationProvider.class)));
5571
}
@@ -61,7 +77,8 @@ public void shouldCreateHS256ConfigurerWithCustomAuthenticationProvider() throws
6177

6278
assertThat(configurer, is(notNullValue()));
6379
assertThat(configurer.audience, is("audience"));
64-
assertThat(configurer.issuer, is("issuer"));
80+
assertThat(configurer.issuers, arrayWithSize(1));
81+
assertThat(configurer.issuers, arrayContaining("issuer"));
6582
assertThat(configurer.provider, is(notNullValue()));
6683
assertThat(configurer.provider, is(provider));
6784
}

0 commit comments

Comments
 (0)