From 6c431e1fe6a27628c9817c2bb40ff03c8589e9ef Mon Sep 17 00:00:00 2001 From: Stephen Crawford <65832608+stephen-crawford@users.noreply.github.com> Date: Tue, 20 Aug 2024 13:58:37 -0400 Subject: [PATCH] [Backport 2.x] Manually backport #4632 (Allow multiple signing keys to be provided) (#4666) Signed-off-by: Stephen Crawford --- .../security/http/JwtAuthenticationTests.java | 65 +++- .../JwtAuthenticationWithUrlParamTests.java | 5 +- .../test/framework/JwtConfigBuilder.java | 11 +- .../test/framework/TestSecurityConfig.java | 2 +- .../auth/http/jwt/HTTPJwtAuthenticator.java | 84 ++--- .../opensearch/security/util/KeyUtils.java | 3 +- .../http/jwt/HTTPJwtAuthenticatorTest.java | 300 ++++++++++++++++++ 7 files changed, 410 insertions(+), 60 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationTests.java b/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationTests.java index 0ff9137dbb..31419dfce5 100644 --- a/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationTests.java +++ b/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationTests.java @@ -90,15 +90,25 @@ public class JwtAuthenticationTests { public static final String QA_SONG_INDEX_NAME = String.format("song_lyrics_%s", QA_DEPARTMENT); - private static final KeyPair KEY_PAIR = Keys.keyPairFor(SignatureAlgorithm.RS256); - private static final String PUBLIC_KEY = new String(Base64.getEncoder().encode(KEY_PAIR.getPublic().getEncoded()), US_ASCII); + private static final KeyPair KEY_PAIR1 = Keys.keyPairFor(SignatureAlgorithm.RS256); + private static final String PUBLIC_KEY1 = new String(Base64.getEncoder().encode(KEY_PAIR1.getPublic().getEncoded()), US_ASCII); + + private static final KeyPair KEY_PAIR2 = Keys.keyPairFor(SignatureAlgorithm.RS256); + private static final String PUBLIC_KEY2 = new String(Base64.getEncoder().encode(KEY_PAIR2.getPublic().getEncoded()), US_ASCII); static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); private static final String JWT_AUTH_HEADER = "jwt-auth"; - private static final JwtAuthorizationHeaderFactory tokenFactory = new JwtAuthorizationHeaderFactory( - KEY_PAIR.getPrivate(), + private static final JwtAuthorizationHeaderFactory tokenFactory1 = new JwtAuthorizationHeaderFactory( + KEY_PAIR1.getPrivate(), + CLAIM_USERNAME, + CLAIM_ROLES, + JWT_AUTH_HEADER + ); + + private static final JwtAuthorizationHeaderFactory tokenFactory2 = new JwtAuthorizationHeaderFactory( + KEY_PAIR2.getPrivate(), CLAIM_USERNAME, CLAIM_ROLES, JWT_AUTH_HEADER @@ -108,7 +118,10 @@ public class JwtAuthenticationTests { "jwt", BASIC_AUTH_DOMAIN_ORDER - 1 ).jwtHttpAuthenticator( - new JwtConfigBuilder().jwtHeader(JWT_AUTH_HEADER).signingKey(PUBLIC_KEY).subjectKey(CLAIM_USERNAME).rolesKey(CLAIM_ROLES) + new JwtConfigBuilder().jwtHeader(JWT_AUTH_HEADER) + .signingKey(List.of(PUBLIC_KEY1, PUBLIC_KEY2)) + .subjectKey(CLAIM_USERNAME) + .rolesKey(CLAIM_ROLES) ).backend("noop"); public static final String SONG_ID_1 = "song-id-01"; @@ -143,7 +156,7 @@ public static void createTestData() { @Test public void shouldAuthenticateWithJwtToken_positive() { - try (TestRestClient client = cluster.getRestClient(tokenFactory.generateValidToken(USER_SUPERHERO))) { + try (TestRestClient client = cluster.getRestClient(tokenFactory1.generateValidToken(USER_SUPERHERO))) { HttpResponse response = client.getAuthInfo(); @@ -155,7 +168,7 @@ public void shouldAuthenticateWithJwtToken_positive() { @Test public void shouldAuthenticateWithJwtToken_positiveWithAnotherUsername() { - try (TestRestClient client = cluster.getRestClient(tokenFactory.generateValidToken(USERNAME_ROOT))) { + try (TestRestClient client = cluster.getRestClient(tokenFactory1.generateValidToken(USERNAME_ROOT))) { HttpResponse response = client.getAuthInfo(); @@ -167,7 +180,7 @@ public void shouldAuthenticateWithJwtToken_positiveWithAnotherUsername() { @Test public void shouldAuthenticateWithJwtToken_failureLackingUserName() { - try (TestRestClient client = cluster.getRestClient(tokenFactory.generateTokenWithoutPreferredUsername(USER_SUPERHERO))) { + try (TestRestClient client = cluster.getRestClient(tokenFactory1.generateTokenWithoutPreferredUsername(USER_SUPERHERO))) { HttpResponse response = client.getAuthInfo(); @@ -178,7 +191,7 @@ public void shouldAuthenticateWithJwtToken_failureLackingUserName() { @Test public void shouldAuthenticateWithJwtToken_failureExpiredToken() { - try (TestRestClient client = cluster.getRestClient(tokenFactory.generateExpiredToken(USER_SUPERHERO))) { + try (TestRestClient client = cluster.getRestClient(tokenFactory1.generateExpiredToken(USER_SUPERHERO))) { HttpResponse response = client.getAuthInfo(); @@ -202,7 +215,7 @@ public void shouldAuthenticateWithJwtToken_failureIncorrectFormatOfToken() { @Test public void shouldAuthenticateWithJwtToken_failureIncorrectSignature() { KeyPair incorrectKeyPair = Keys.keyPairFor(SignatureAlgorithm.RS256); - Header header = tokenFactory.generateTokenSignedWithKey(incorrectKeyPair.getPrivate(), USER_SUPERHERO); + Header header = tokenFactory1.generateTokenSignedWithKey(incorrectKeyPair.getPrivate(), USER_SUPERHERO); try (TestRestClient client = cluster.getRestClient(header)) { HttpResponse response = client.getAuthInfo(); @@ -214,7 +227,7 @@ public void shouldAuthenticateWithJwtToken_failureIncorrectSignature() { @Test public void shouldReadRolesFromToken_positiveFirstRoleSet() { - Header header = tokenFactory.generateValidToken(USER_SUPERHERO, ROLE_ADMIN, ROLE_DEVELOPER, ROLE_QA); + Header header = tokenFactory1.generateValidToken(USER_SUPERHERO, ROLE_ADMIN, ROLE_DEVELOPER, ROLE_QA); try (TestRestClient client = cluster.getRestClient(header)) { HttpResponse response = client.getAuthInfo(); @@ -228,7 +241,7 @@ public void shouldReadRolesFromToken_positiveFirstRoleSet() { @Test public void shouldReadRolesFromToken_positiveSecondRoleSet() { - Header header = tokenFactory.generateValidToken(USER_SUPERHERO, ROLE_CTO, ROLE_CEO, ROLE_VP); + Header header = tokenFactory1.generateValidToken(USER_SUPERHERO, ROLE_CTO, ROLE_CEO, ROLE_VP); try (TestRestClient client = cluster.getRestClient(header)) { HttpResponse response = client.getAuthInfo(); @@ -244,7 +257,7 @@ public void shouldReadRolesFromToken_positiveSecondRoleSet() { public void shouldExposeTokenClaimsAsUserAttributes_positive() throws IOException { String[] roles = { ROLE_VP }; Map additionalClaims = Map.of(CLAIM_DEPARTMENT, QA_DEPARTMENT); - Header header = tokenFactory.generateValidTokenWithCustomClaims(USER_SUPERHERO, roles, additionalClaims); + Header header = tokenFactory1.generateValidTokenWithCustomClaims(USER_SUPERHERO, roles, additionalClaims); try (RestHighLevelClient client = cluster.getRestHighLevelClient(List.of(header))) { SearchRequest searchRequest = queryStringQueryRequest(QA_SONG_INDEX_NAME, QUERY_TITLE_MAGNUM_OPUS); @@ -261,11 +274,35 @@ public void shouldExposeTokenClaimsAsUserAttributes_positive() throws IOExceptio public void shouldExposeTokenClaimsAsUserAttributes_negative() throws IOException { String[] roles = { ROLE_VP }; Map additionalClaims = Map.of(CLAIM_DEPARTMENT, "department-without-access-to-qa-song-index"); - Header header = tokenFactory.generateValidTokenWithCustomClaims(USER_SUPERHERO, roles, additionalClaims); + Header header = tokenFactory1.generateValidTokenWithCustomClaims(USER_SUPERHERO, roles, additionalClaims); try (RestHighLevelClient client = cluster.getRestHighLevelClient(List.of(header))) { SearchRequest searchRequest = queryStringQueryRequest(QA_SONG_INDEX_NAME, QUERY_TITLE_MAGNUM_OPUS); assertThatThrownBy(() -> client.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); } } + + @Test + public void secondKeypairShouldAuthenticateWithJwtToken_positive() { + try (TestRestClient client = cluster.getRestClient(tokenFactory2.generateValidToken(USER_SUPERHERO))) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + String username = response.getTextFromJsonBody(POINTER_USERNAME); + assertThat(username, equalTo(USER_SUPERHERO)); + } + } + + @Test + public void secondKeypairShouldAuthenticateWithJwtToken_positiveWithAnotherUsername() { + try (TestRestClient client = cluster.getRestClient(tokenFactory2.generateValidToken(USERNAME_ROOT))) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + String username = response.getTextFromJsonBody(POINTER_USERNAME); + assertThat(username, equalTo(USERNAME_ROOT)); + } + } } diff --git a/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationWithUrlParamTests.java b/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationWithUrlParamTests.java index 0d63d2d21e..d2335603bb 100644 --- a/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationWithUrlParamTests.java +++ b/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationWithUrlParamTests.java @@ -72,7 +72,10 @@ public class JwtAuthenticationWithUrlParamTests { "jwt", BASIC_AUTH_DOMAIN_ORDER - 1 ).jwtHttpAuthenticator( - new JwtConfigBuilder().jwtUrlParameter(TOKEN_URL_PARAM).signingKey(PUBLIC_KEY).subjectKey(CLAIM_USERNAME).rolesKey(CLAIM_ROLES) + new JwtConfigBuilder().jwtUrlParameter(TOKEN_URL_PARAM) + .signingKey(List.of(PUBLIC_KEY)) + .subjectKey(CLAIM_USERNAME) + .rolesKey(CLAIM_ROLES) ).backend("noop"); @Rule diff --git a/src/integrationTest/java/org/opensearch/test/framework/JwtConfigBuilder.java b/src/integrationTest/java/org/opensearch/test/framework/JwtConfigBuilder.java index 88297bacd2..f5984bad8f 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/JwtConfigBuilder.java +++ b/src/integrationTest/java/org/opensearch/test/framework/JwtConfigBuilder.java @@ -9,6 +9,7 @@ */ package org.opensearch.test.framework; +import java.util.List; import java.util.Map; import java.util.Objects; @@ -19,7 +20,7 @@ public class JwtConfigBuilder { private String jwtHeader; private String jwtUrlParameter; - private String signingKey; + private List signingKeys; private String subjectKey; private String rolesKey; @@ -33,8 +34,8 @@ public JwtConfigBuilder jwtUrlParameter(String jwtUrlParameter) { return this; } - public JwtConfigBuilder signingKey(String signingKey) { - this.signingKey = signingKey; + public JwtConfigBuilder signingKey(List signingKeys) { + this.signingKeys = signingKeys; return this; } @@ -50,10 +51,10 @@ public JwtConfigBuilder rolesKey(String rolesKey) { public Map build() { Builder builder = new Builder<>(); - if (Objects.isNull(signingKey)) { + if (Objects.isNull(signingKeys)) { throw new IllegalStateException("Signing key is required."); } - builder.put("signing_key", signingKey); + builder.put("signing_key", signingKeys); if (isNoneBlank(jwtHeader)) { builder.put("jwt_header", jwtHeader); } diff --git a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java index 81604d1376..a1ea3720ba 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java +++ b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java @@ -815,7 +815,7 @@ public static class AuthcDomain implements ToXContentObject { ).httpAuthenticator("basic").backend("internal"); public final static AuthcDomain JWT_AUTH_DOMAIN = new TestSecurityConfig.AuthcDomain("jwt", 1).jwtHttpAuthenticator( - new JwtConfigBuilder().jwtHeader(AUTHORIZATION).signingKey(PUBLIC_KEY) + new JwtConfigBuilder().jwtHeader(AUTHORIZATION).signingKey(List.of(PUBLIC_KEY)) ).backend("noop"); private final String id; diff --git a/src/main/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticator.java b/src/main/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticator.java index d897c408b9..106dec7ee0 100644 --- a/src/main/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticator.java +++ b/src/main/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticator.java @@ -19,6 +19,7 @@ import java.security.PublicKey; import java.security.spec.InvalidKeySpecException; import java.security.spec.X509EncodedKeySpec; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -59,7 +60,7 @@ public class HTTPJwtAuthenticator implements HTTPAuthenticator { private static final Pattern BASIC = Pattern.compile("^\\s*Basic\\s.*", Pattern.CASE_INSENSITIVE); private static final String BEARER = "bearer "; - private final JwtParser jwtParser; + private final List jwtParsers = new ArrayList<>(); private final String jwtHeaderName; private final boolean isDefaultAuthHeader; private final String jwtUrlParameter; @@ -71,7 +72,7 @@ public class HTTPJwtAuthenticator implements HTTPAuthenticator { public HTTPJwtAuthenticator(final Settings settings, final Path configPath) { super(); - String signingKey = settings.get("signing_key"); + List signingKeys = settings.getAsList("signing_key"); jwtUrlParameter = settings.get("jwt_url_parameter"); jwtHeaderName = settings.get("jwt_header", AUTHORIZATION); isDefaultAuthHeader = AUTHORIZATION.equalsIgnoreCase(jwtHeaderName); @@ -87,19 +88,23 @@ public HTTPJwtAuthenticator(final Settings settings, final Path configPath) { ); } - final JwtParserBuilder jwtParserBuilder = KeyUtils.createJwtParserBuilderFromSigningKey(signingKey, log); - if (jwtParserBuilder == null) { - jwtParser = null; - } else { - if (requireIssuer != null) { - jwtParserBuilder.requireIssuer(requireIssuer); - } - - final SecurityManager sm = System.getSecurityManager(); - if (sm != null) { - sm.checkPermission(new SpecialPermission()); + for (String key : signingKeys) { + JwtParser jwtParser; + final JwtParserBuilder jwtParserBuilder = KeyUtils.createJwtParserBuilderFromSigningKey(key, log); + if (jwtParserBuilder == null) { + jwtParser = null; + } else { + if (requireIssuer != null) { + jwtParserBuilder.requireIssuer(requireIssuer); + } + + final SecurityManager sm = System.getSecurityManager(); + if (sm != null) { + sm.checkPermission(new SpecialPermission()); + } + jwtParser = AccessController.doPrivileged((PrivilegedAction) jwtParserBuilder::build); } - jwtParser = AccessController.doPrivileged((PrivilegedAction) jwtParserBuilder::build); + jwtParsers.add(jwtParser); } } @@ -124,7 +129,7 @@ public AuthCredentials run() { } private AuthCredentials extractCredentials0(final SecurityRequest request) { - if (jwtParser == null) { + if (jwtParsers.isEmpty() || jwtParsers.get(0) == null) { log.error("Missing Signing Key. JWT authentication will not work"); return null; } @@ -161,39 +166,42 @@ private AuthCredentials extractCredentials0(final SecurityRequest request) { } } - try { - final Claims claims = jwtParser.parseClaimsJws(jwtToken).getBody(); + for (JwtParser jwtParser : jwtParsers) { + try { - if (!requiredAudience.isEmpty()) { - assertValidAudienceClaim(claims); - } + final Claims claims = jwtParser.parseClaimsJws(jwtToken).getBody(); - final String subject = extractSubject(claims, request); + if (!requiredAudience.isEmpty()) { + assertValidAudienceClaim(claims); + } + final String subject = extractSubject(claims, request); - if (subject == null) { - log.error("No subject found in JWT token"); - return null; - } + if (subject == null) { + log.error("No subject found in JWT token"); + return null; + } - final String[] roles = extractRoles(claims, request); + final String[] roles = extractRoles(claims, request); - final AuthCredentials ac = new AuthCredentials(subject, roles).markComplete(); + final AuthCredentials ac = new AuthCredentials(subject, roles).markComplete(); - for (Entry claim : claims.entrySet()) { - ac.addAttribute("attr.jwt." + claim.getKey(), String.valueOf(claim.getValue())); - } + for (Entry claim : claims.entrySet()) { + ac.addAttribute("attr.jwt." + claim.getKey(), String.valueOf(claim.getValue())); + } - return ac; + return ac; - } catch (WeakKeyException e) { - log.error("Cannot authenticate user with JWT because of ", e); - return null; - } catch (Exception e) { - if (log.isDebugEnabled()) { - log.debug("Invalid or expired JWT token.", e); + } catch (WeakKeyException e) { + log.error("Cannot authenticate user with JWT because of ", e); + return null; + } catch (Exception e) { + if (log.isDebugEnabled()) { + log.debug("Invalid or expired JWT token.", e); + } } - return null; } + log.error("Failed to parse JWT token using any of the available parsers"); + return null; } private void assertValidAudienceClaim(Claims claims) throws BadJWTException { diff --git a/src/main/java/org/opensearch/security/util/KeyUtils.java b/src/main/java/org/opensearch/security/util/KeyUtils.java index 4f03c08cd5..1e69d7f697 100644 --- a/src/main/java/org/opensearch/security/util/KeyUtils.java +++ b/src/main/java/org/opensearch/security/util/KeyUtils.java @@ -53,7 +53,8 @@ public JwtParserBuilder run() { PublicKey key = null; final String minimalKeyFormat = signingKey.replace("-----BEGIN PUBLIC KEY-----\n", "") - .replace("-----END PUBLIC KEY-----", ""); + .replace("-----END PUBLIC KEY-----", "") + .trim(); final byte[] decoded = Base64.getDecoder().decode(minimalKeyFormat); diff --git a/src/test/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticatorTest.java b/src/test/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticatorTest.java index aee9747c4d..18cb5b2889 100644 --- a/src/test/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticatorTest.java +++ b/src/test/java/com/amazon/dlic/auth/http/jwt/HTTPJwtAuthenticatorTest.java @@ -14,6 +14,7 @@ import java.nio.charset.StandardCharsets; import java.security.KeyPair; import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.SecureRandom; @@ -543,4 +544,303 @@ private AuthCredentials extractCredentialsFromJwtHeader(final Settings.Builder s return jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap<>()).asSecurityRequest(), null); } + @Test + public void testMultipleSigningKeysParseSuccessfully() throws NoSuchAlgorithmException { + + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(2048); + KeyPair pair1 = keyGen.generateKeyPair(); + PrivateKey priv1 = pair1.getPrivate(); + PublicKey pub1 = pair1.getPublic(); + + KeyPair pair2 = keyGen.generateKeyPair(); + PrivateKey priv2 = pair2.getPrivate(); + PublicKey pub2 = pair2.getPublic(); + + String jwsToken1 = Jwts.builder().setSubject("Leonard McCoy").signWith(priv1, SignatureAlgorithm.RS256).compact(); + String jwsToken2 = Jwts.builder().setSubject("Stephen Crawford").signWith(priv2, SignatureAlgorithm.RS256).compact(); + + Settings settings = Settings.builder() + .put( + "signing_key", + "-----BEGIN PUBLIC KEY-----\n" + + BaseEncoding.base64().encode(pub1.getEncoded()) + + "-----END PUBLIC KEY-----,-----BEGIN PUBLIC KEY-----\n" + + BaseEncoding.base64().encode(pub2.getEncoded()) + + "-----END PUBLIC KEY-----" + ) + .build(); + + HTTPJwtAuthenticator jwtAuth = new HTTPJwtAuthenticator(settings, null); + Map headers1 = new HashMap(); + headers1.put("Authorization", "Bearer " + jwsToken1); + + AuthCredentials creds1 = jwtAuth.extractCredentials( + new FakeRestRequest(headers1, new HashMap()).asSecurityRequest(), + null + ); + + Assert.assertNotNull(creds1); + assertThat(creds1.getUsername(), is("Leonard McCoy")); + assertThat(creds1.getBackendRoles().size(), is(0)); + + Map headers2 = new HashMap(); + headers2.put("Authorization", "Bearer " + jwsToken2); + AuthCredentials creds2 = jwtAuth.extractCredentials( + new FakeRestRequest(headers2, new HashMap()).asSecurityRequest(), + null + ); + + Assert.assertNotNull(creds2); + assertThat(creds2.getUsername(), is("Stephen Crawford")); + assertThat(creds2.getBackendRoles().size(), is(0)); + } + + @Test + public void testMultipleSigningKeysParseWithSpacesSuccessfully() throws NoSuchAlgorithmException { + + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(2048); + KeyPair pair1 = keyGen.generateKeyPair(); + PrivateKey priv1 = pair1.getPrivate(); + PublicKey pub1 = pair1.getPublic(); + + KeyPair pair2 = keyGen.generateKeyPair(); + PrivateKey priv2 = pair2.getPrivate(); + PublicKey pub2 = pair2.getPublic(); + + String jwsToken1 = Jwts.builder().setSubject("Leonard McCoy").signWith(priv1, SignatureAlgorithm.RS256).compact(); + String jwsToken2 = Jwts.builder().setSubject("Stephen Crawford").signWith(priv2, SignatureAlgorithm.RS256).compact(); + + Settings settings = Settings.builder() + .put( + "signing_key", + "-----BEGIN PUBLIC KEY-----\n" + + BaseEncoding.base64().encode(pub1.getEncoded()) + + "-----END PUBLIC KEY-----, -----BEGIN PUBLIC KEY-----\n" + + BaseEncoding.base64().encode(pub2.getEncoded()) + + "-----END PUBLIC KEY-----" + ) + .build(); + + HTTPJwtAuthenticator jwtAuth = new HTTPJwtAuthenticator(settings, null); + Map headers1 = new HashMap(); + headers1.put("Authorization", "Bearer " + jwsToken1); + + AuthCredentials creds1 = jwtAuth.extractCredentials( + new FakeRestRequest(headers1, new HashMap()).asSecurityRequest(), + null + ); + + Assert.assertNotNull(creds1); + assertThat(creds1.getUsername(), is("Leonard McCoy")); + assertThat(creds1.getBackendRoles().size(), is(0)); + + Map headers2 = new HashMap(); + headers2.put("Authorization", "Bearer " + jwsToken2); + AuthCredentials creds2 = jwtAuth.extractCredentials( + new FakeRestRequest(headers2, new HashMap()).asSecurityRequest(), + null + ); + + Assert.assertNotNull(creds2); + assertThat(creds2.getUsername(), is("Stephen Crawford")); + assertThat(creds2.getBackendRoles().size(), is(0)); + } + + @Test + public void testMultipleSigningKeysMixedAlgsParseSuccessfully() throws NoSuchAlgorithmException { + + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(2048); + KeyPair pair1 = keyGen.generateKeyPair(); + PrivateKey priv1 = pair1.getPrivate(); + PublicKey pub1 = pair1.getPublic(); + + KeyPairGenerator keyGen2 = KeyPairGenerator.getInstance("EC"); + keyGen2.initialize(521); + KeyPair pair = keyGen2.generateKeyPair(); + PrivateKey priv2 = pair.getPrivate(); + PublicKey pub2 = pair.getPublic(); + + String jwsToken1 = Jwts.builder().setSubject("Leonard McCoy").signWith(priv1, SignatureAlgorithm.RS256).compact(); + + String jwsToken2 = Jwts.builder().setSubject("Stephen Crawford").signWith(priv2, SignatureAlgorithm.ES512).compact(); + + Settings settings = Settings.builder() + .put( + "signing_key", + "-----BEGIN PUBLIC KEY-----\n" + + BaseEncoding.base64().encode(pub1.getEncoded()) + + "-----END PUBLIC KEY-----," + + BaseEncoding.base64().encode(pub2.getEncoded()) + ) + .build(); + + HTTPJwtAuthenticator jwtAuth = new HTTPJwtAuthenticator(settings, null); + Map headers1 = new HashMap(); + headers1.put("Authorization", "Bearer " + jwsToken1); + + AuthCredentials creds1 = jwtAuth.extractCredentials( + new FakeRestRequest(headers1, new HashMap()).asSecurityRequest(), + null + ); + + Assert.assertNotNull(creds1); + assertThat(creds1.getUsername(), is("Leonard McCoy")); + assertThat(creds1.getBackendRoles().size(), is(0)); + + Map headers2 = new HashMap(); + headers2.put("Authorization", "Bearer " + jwsToken2); + AuthCredentials creds2 = jwtAuth.extractCredentials( + new FakeRestRequest(headers2, new HashMap()).asSecurityRequest(), + null + ); + + Assert.assertNotNull(creds2); + assertThat(creds2.getUsername(), is("Stephen Crawford")); + assertThat(creds2.getBackendRoles().size(), is(0)); + } + + @Test + public void testManyMultipleSigningKeysMixedAlgsParseSuccessfully() throws NoSuchAlgorithmException { + + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(2048); + KeyPair pair1 = keyGen.generateKeyPair(); + PrivateKey priv1 = pair1.getPrivate(); + PublicKey pub1 = pair1.getPublic(); + + KeyPairGenerator keyGen2 = KeyPairGenerator.getInstance("EC"); + keyGen2.initialize(521); + KeyPair pair = keyGen2.generateKeyPair(); + PrivateKey priv2 = pair.getPrivate(); + PublicKey pub2 = pair.getPublic(); + + KeyPairGenerator keyGen3 = KeyPairGenerator.getInstance("RSA"); + keyGen3.initialize(2048); + KeyPair pair3 = keyGen3.generateKeyPair(); + PrivateKey priv3 = pair3.getPrivate(); + PublicKey pub3 = pair3.getPublic(); + + KeyPairGenerator keyGen4 = KeyPairGenerator.getInstance("EC"); + keyGen4.initialize(521); + KeyPair pair4 = keyGen4.generateKeyPair(); + PrivateKey priv4 = pair4.getPrivate(); + PublicKey pub4 = pair4.getPublic(); + + String jwsToken1 = Jwts.builder().setSubject("Stephen Crawford").signWith(priv1, SignatureAlgorithm.RS256).compact(); + String jwsToken2 = Jwts.builder().setSubject("Craig Perkins").signWith(priv2, SignatureAlgorithm.ES512).compact(); + String jwsToken3 = Jwts.builder().setSubject("Darshit Chanpura").signWith(priv3, SignatureAlgorithm.RS256).compact(); + String jwsToken4 = Jwts.builder().setSubject("Derek Ho").signWith(priv4, SignatureAlgorithm.ES512).compact(); + + Settings settings = Settings.builder() + .put( + "signing_key", + "-----BEGIN PUBLIC KEY-----\n" + + BaseEncoding.base64().encode(pub1.getEncoded()) + + "-----END PUBLIC KEY-----," + + BaseEncoding.base64().encode(pub2.getEncoded()) + + "," + + "-----BEGIN PUBLIC KEY-----\n" + + BaseEncoding.base64().encode(pub3.getEncoded()) + + "-----END PUBLIC KEY-----," + + BaseEncoding.base64().encode(pub4.getEncoded()) + ) + .build(); + + HTTPJwtAuthenticator jwtAuth = new HTTPJwtAuthenticator(settings, null); + Map headers1 = new HashMap(); + headers1.put("Authorization", "Bearer " + jwsToken1); + + AuthCredentials creds1 = jwtAuth.extractCredentials( + new FakeRestRequest(headers1, new HashMap()).asSecurityRequest(), + null + ); + + Assert.assertNotNull(creds1); + assertThat(creds1.getUsername(), is("Stephen Crawford")); + assertThat(creds1.getBackendRoles().size(), is(0)); + + Map headers2 = new HashMap(); + headers2.put("Authorization", "Bearer " + jwsToken2); + AuthCredentials creds2 = jwtAuth.extractCredentials( + new FakeRestRequest(headers2, new HashMap()).asSecurityRequest(), + null + ); + + Assert.assertNotNull(creds2); + assertThat(creds2.getUsername(), is("Craig Perkins")); + assertThat(creds2.getBackendRoles().size(), is(0)); + + Map headers3 = new HashMap(); + headers3.put("Authorization", "Bearer " + jwsToken3); + + AuthCredentials creds3 = jwtAuth.extractCredentials( + new FakeRestRequest(headers3, new HashMap()).asSecurityRequest(), + null + ); + + Assert.assertNotNull(creds3); + assertThat(creds3.getUsername(), is("Darshit Chanpura")); + assertThat(creds3.getBackendRoles().size(), is(0)); + + Map headers4 = new HashMap(); + headers4.put("Authorization", "Bearer " + jwsToken4); + AuthCredentials creds4 = jwtAuth.extractCredentials( + new FakeRestRequest(headers4, new HashMap()).asSecurityRequest(), + null + ); + + Assert.assertNotNull(creds4); + assertThat(creds4.getUsername(), is("Derek Ho")); + assertThat(creds4.getBackendRoles().size(), is(0)); + } + + @Test + public void testMultipleSigningKeysFailToParseReturnsNull() throws NoSuchAlgorithmException { + + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(2048); + KeyPair pair1 = keyGen.generateKeyPair(); + PrivateKey priv1 = pair1.getPrivate(); + PublicKey pub1 = pair1.getPublic(); + + KeyPair pair2 = keyGen.generateKeyPair(); + PrivateKey priv2 = pair2.getPrivate(); + PublicKey pub2 = pair2.getPublic(); + + String invalidJwsToken = "123invalidtoken.."; + + Settings settings = Settings.builder() + .put( + "signing_key", + "-----BEGIN PUBLIC KEY-----\n" + + BaseEncoding.base64().encode(pub1.getEncoded()) + + "-----END PUBLIC KEY-----, -----BEGIN PUBLIC KEY-----\n" + + BaseEncoding.base64().encode(pub2.getEncoded()) + + "-----END PUBLIC KEY-----" + ) + .build(); + + HTTPJwtAuthenticator jwtAuth = new HTTPJwtAuthenticator(settings, null); + Map headers1 = new HashMap(); + headers1.put("Authorization", "Bearer " + invalidJwsToken); + + AuthCredentials creds1 = jwtAuth.extractCredentials( + new FakeRestRequest(headers1, new HashMap()).asSecurityRequest(), + null + ); + + Assert.assertNull(creds1); + + Map headers2 = new HashMap(); + headers2.put("Authorization", "Bearer " + invalidJwsToken); + AuthCredentials creds2 = jwtAuth.extractCredentials( + new FakeRestRequest(headers2, new HashMap()).asSecurityRequest(), + null + ); + + Assert.assertNull(creds2); + } }