Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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