From fda720dd85c7a894d7bfe32700c5b6dc47bdc9e6 Mon Sep 17 00:00:00 2001 From: Rishav Kumar Date: Fri, 11 Jul 2025 11:38:31 +0530 Subject: [PATCH 01/20] nested subject Signed-off-by: Rishav Kumar --- .../test/framework/JwtConfigBuilder.java | 9 +++- .../jwt/AbstractHTTPJwtAuthenticator.java | 30 +++++++++-- .../auth/http/jwt/HTTPJwtAuthenticator.java | 48 ++++++++++++------ ...wtKeyByOpenIdConnectAuthenticatorTest.java | 50 +++++++++++++++++++ .../auth/http/jwt/keybyoidc/TestJwts.java | 30 ++++++++++- 5 files changed, 142 insertions(+), 25 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/test/framework/JwtConfigBuilder.java b/src/integrationTest/java/org/opensearch/test/framework/JwtConfigBuilder.java index 871419d2bf..0dedebd1dd 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/JwtConfigBuilder.java +++ b/src/integrationTest/java/org/opensearch/test/framework/JwtConfigBuilder.java @@ -21,7 +21,7 @@ public class JwtConfigBuilder { private String jwtHeader; private String jwtUrlParameter; private List signingKeys; - private String subjectKey; + private List subjectKey; private List rolesKey; public JwtConfigBuilder jwtHeader(String jwtHeader) { @@ -40,6 +40,11 @@ public JwtConfigBuilder signingKey(List signingKeys) { } public JwtConfigBuilder subjectKey(String subjectKey) { + this.subjectKey = List.of(subjectKey); + return this; + } + + public JwtConfigBuilder subjectKey(List subjectKey) { this.subjectKey = subjectKey; return this; } @@ -66,7 +71,7 @@ public Map build() { if (isNoneBlank(jwtUrlParameter)) { builder.put("jwt_url_parameter", jwtUrlParameter); } - if (isNoneBlank(subjectKey)) { + if (subjectKey != null && !subjectKey.isEmpty()) { builder.put("subject_key", subjectKey); } if (rolesKey != null && !rolesKey.isEmpty()) { diff --git a/src/main/java/org/opensearch/security/auth/http/jwt/AbstractHTTPJwtAuthenticator.java b/src/main/java/org/opensearch/security/auth/http/jwt/AbstractHTTPJwtAuthenticator.java index f79f61d3e8..55088a1111 100644 --- a/src/main/java/org/opensearch/security/auth/http/jwt/AbstractHTTPJwtAuthenticator.java +++ b/src/main/java/org/opensearch/security/auth/http/jwt/AbstractHTTPJwtAuthenticator.java @@ -60,7 +60,7 @@ public abstract class AbstractHTTPJwtAuthenticator implements HTTPAuthenticator private final String jwtHeaderName; private final boolean isDefaultAuthHeader; private final String jwtUrlParameter; - private final String subjectKey; + private final List subjectKey; private final List rolesKey; private final List requiredAudience; private final String requiredIssuer; @@ -73,7 +73,7 @@ public AbstractHTTPJwtAuthenticator(Settings settings, Path configPath) { jwtHeaderName = settings.get("jwt_header", AUTHORIZATION); isDefaultAuthHeader = AUTHORIZATION.equalsIgnoreCase(jwtHeaderName); rolesKey = settings.getAsList("roles_key"); - subjectKey = settings.get("subject_key"); + subjectKey = settings.getAsList("subject_key"); clockSkewToleranceSeconds = settings.getAsInt("jwt_clock_skew_tolerance_seconds", DEFAULT_CLOCK_SKEW_TOLERANCE_SECONDS); requiredAudience = settings.getAsList("required_audience"); requiredIssuer = settings.get("required_issuer"); @@ -183,12 +183,30 @@ protected String getJwtTokenString(SecurityRequest request) { return jwtToken; } + @SuppressWarnings("unchecked") @VisibleForTesting public String extractSubject(JWTClaimsSet claims) { String subject = claims.getSubject(); + log.warn("subject is '{}' ", subject); + log.warn("subjectKey is '{}' ", subjectKey); + log.warn("claims is '{}' ", claims); + + if (subjectKey != null && !subjectKey.isEmpty()) { + Object subjectObject = null; + Map claimsMap = claims.getClaims(); + // This loop is necessary for nested claim traversal + for (int i = 0; i < subjectKey.size(); i++) { + if (i == subjectKey.size() - 1) { + subjectObject = claimsMap.get(subjectKey.get(i)); + } else if (claimsMap.get(subjectKey.get(i)) instanceof Map) { + claimsMap = (Map) claimsMap.get(subjectKey.get(i)); + } else { + log.warn("Failed to get subject from JWT claims with subject_key '{}'.", subjectKey); + return null; + } + } - if (subjectKey != null) { - Object subjectObject = claims.getClaim(subjectKey); + log.warn("subjectObject is '{}' ", subjectObject); if (subjectObject == null) { log.warn("Failed to get subject from JWT claims, check if subject_key '{}' is correct.", subjectKey); @@ -219,6 +237,7 @@ public String[] extractRoles(JWTClaimsSet claims) { return new String[0]; } + log.warn("rolesKey is '{}' ", rolesKey); Object rolesObject = null; Map claimsMap = claims.getClaims(); for (int i = 0; i < rolesKey.size(); i++) { @@ -242,7 +261,8 @@ public String[] extractRoles(JWTClaimsSet claims) { ); return new String[0]; } - + + log.warn("rolesObject is '{}' ", rolesObject); String[] roles = String.valueOf(rolesObject).split(","); // We expect a String or Collection. If we find something else, convert to diff --git a/src/main/java/org/opensearch/security/auth/http/jwt/HTTPJwtAuthenticator.java b/src/main/java/org/opensearch/security/auth/http/jwt/HTTPJwtAuthenticator.java index 51ada0f87f..96c52520f7 100644 --- a/src/main/java/org/opensearch/security/auth/http/jwt/HTTPJwtAuthenticator.java +++ b/src/main/java/org/opensearch/security/auth/http/jwt/HTTPJwtAuthenticator.java @@ -65,7 +65,7 @@ public class HTTPJwtAuthenticator implements HTTPAuthenticator { private final boolean isDefaultAuthHeader; private final String jwtUrlParameter; private final List rolesKey; - private final String subjectKey; + private final List subjectKey; private final List requiredAudience; private final String requireIssuer; @@ -79,7 +79,7 @@ public HTTPJwtAuthenticator(final Settings settings, final Path configPath) { jwtHeaderName = settings.get("jwt_header", AUTHORIZATION); isDefaultAuthHeader = AUTHORIZATION.equalsIgnoreCase(jwtHeaderName); rolesKey = settings.getAsList("roles_key"); - subjectKey = settings.get("subject_key"); + subjectKey = settings.getAsList("subject_key"); requiredAudience = settings.getAsList("required_audience"); requireIssuer = settings.get("required_issuer"); @@ -178,7 +178,7 @@ private AuthCredentials extractCredentials0(final SecurityRequest request) { assertValidAudienceClaim(claims); } - final String subject = extractSubject(claims, request); + final String subject = extractSubject(claims); if (subject == null) { log.error("No subject found in JWT token"); @@ -253,25 +253,41 @@ public String getType() { return "jwt"; } - protected String extractSubject(final Claims claims, final SecurityRequest request) { + protected String extractSubject(final Claims claims) { String subject = claims.getSubject(); - if (subjectKey != null) { - // try to get roles from claims, first as Object to avoid having to catch the ExpectedTypeException - Object subjectObject = claims.get(subjectKey, Object.class); - if (subjectObject == null) { - log.warn("Failed to get subject from JWT claims, check if subject_key '{}' is correct.", subjectKey); - return null; + if (subjectKey != null && !subjectKey.isEmpty()) { + // ── 1. Traverse the nested structure ─────────────────────────────────────── + Object node = claims; // start at root + for (String key : subjectKey) { + if (!(node instanceof Map map)) { // unexpected shape + log.warn( + "While following subject_key path {}, expected a JSON object before '{}', but found '{}' ({}).", + subjectKey, + key, + node, + node.getClass() + ); + return subject; // Return default subject on error + } + node = map.get(key); + if (node == null) { // key missing + log.warn("Failed to find '{}' in JWT claims while following subject_key path {}.", key, subjectKey); + return subject; // Return default subject on error + } } - // We expect a String. If we find something else, convert to String but issue a warning - if (!(subjectObject instanceof String)) { + // ── 2. Interpret the leaf value ──────────────────────────────────────────── + if (node instanceof String str) { + return str.trim(); + } else { // something odd log.warn( - "Expected type String for roles in the JWT for subject_key {}, but value was '{}' ({}). Will convert this value to String.", + "Expected a String at the end of subject_key path {}, but found '{}' ({}). Converting to String.", subjectKey, - subjectObject, - subjectObject.getClass() + node, + node.getClass() ); + return String.valueOf(node).trim(); } - subject = String.valueOf(subjectObject); + } return subject; } diff --git a/src/test/java/org/opensearch/security/auth/http/jwt/keybyoidc/HTTPJwtKeyByOpenIdConnectAuthenticatorTest.java b/src/test/java/org/opensearch/security/auth/http/jwt/keybyoidc/HTTPJwtKeyByOpenIdConnectAuthenticatorTest.java index debe581fa6..a6534d4e57 100644 --- a/src/test/java/org/opensearch/security/auth/http/jwt/keybyoidc/HTTPJwtKeyByOpenIdConnectAuthenticatorTest.java +++ b/src/test/java/org/opensearch/security/auth/http/jwt/keybyoidc/HTTPJwtKeyByOpenIdConnectAuthenticatorTest.java @@ -285,6 +285,56 @@ public void testRolesInNestedClaim() { assertThat(creds.getBackendRoles(), Matchers.is(TestJwts.TEST_ROLES)); } + @Test + public void testSubjectInNestedClaim() { + Settings settings = Settings.builder() + .put("openid_connect_url", mockIdpServer.getDiscoverUri()) + .putList("subject_key", TestJwts.NESTED_MCCOY_SUBJECT) + .put("roles_key", TestJwts.ROLES_CLAIM) + .put("required_issuer", TestJwts.TEST_ISSUER) + .put("required_audience", TestJwts.TEST_AUDIENCE) + .build(); + + HTTPJwtKeyByOpenIdConnectAuthenticator jwtAuth = new HTTPJwtKeyByOpenIdConnectAuthenticator(settings, null); + + AuthCredentials creds = jwtAuth.extractCredentials( + new FakeRestRequest( + ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_NESTED_SUBJECT_OCT_1), + new HashMap() + ).asSecurityRequest(), + null + ); + + Assert.assertNotNull(creds); + assertThat(creds.getUsername(), Matchers.is(TestJwts.MCCOY_SUBJECT)); + assertThat(creds.getBackendRoles(), Matchers.is(TestJwts.TEST_ROLES)); + } + + @Test + public void testSubjectAndRolesInNestedClaim() { + Settings settings = Settings.builder() + .put("openid_connect_url", mockIdpServer.getDiscoverUri()) + .putList("subject_key", TestJwts.NESTED_MCCOY_SUBJECT) + .putList("roles_key", TestJwts.NESTED_ROLES_CLAIM) + .put("required_issuer", TestJwts.TEST_ISSUER) + .put("required_audience", TestJwts.TEST_AUDIENCE) + .build(); + + HTTPJwtKeyByOpenIdConnectAuthenticator jwtAuth = new HTTPJwtKeyByOpenIdConnectAuthenticator(settings, null); + + AuthCredentials creds = jwtAuth.extractCredentials( + new FakeRestRequest( + ImmutableMap.of("Authorization", TestJwts.MC_COY_SIGNED_NESTED_ROLES_AND_SUBJECT_OCT_1), + new HashMap() + ).asSecurityRequest(), + null + ); + + Assert.assertNotNull(creds); + assertThat(creds.getUsername(), Matchers.is(TestJwts.MCCOY_SUBJECT)); + assertThat(creds.getBackendRoles(), Matchers.is(TestJwts.TEST_ROLES)); + } + @Test public void testExp() { Settings settings = Settings.builder().put("openid_connect_url", mockIdpServer.getDiscoverUri()).build(); diff --git a/src/test/java/org/opensearch/security/auth/http/jwt/keybyoidc/TestJwts.java b/src/test/java/org/opensearch/security/auth/http/jwt/keybyoidc/TestJwts.java index c120bf7e45..05433c2a05 100644 --- a/src/test/java/org/opensearch/security/auth/http/jwt/keybyoidc/TestJwts.java +++ b/src/test/java/org/opensearch/security/auth/http/jwt/keybyoidc/TestJwts.java @@ -33,12 +33,14 @@ class TestJwts { static final String ROLES_CLAIM = "roles"; static final List NESTED_ROLES_CLAIM = List.of("attributes", "roles"); + static final List NESTED_ROLES_AND_SUBJECT_CLAIM = List.of("attributes", "sub"); static final Set TEST_ROLES = ImmutableSet.of("role1", "role2"); static final String TEST_ROLES_STRING = String.join(",", TEST_ROLES); static final String TEST_AUDIENCE = "TestAudience"; static final String MCCOY_SUBJECT = "Leonard McCoy"; + static final List NESTED_MCCOY_SUBJECT = List.of("attributes_sub", "sub"); static final String TEST_ISSUER = "TestIssuer"; @@ -46,6 +48,16 @@ class TestJwts { static final JWTClaimsSet MC_COY_2 = create(MCCOY_SUBJECT, TEST_AUDIENCE, TEST_ISSUER, ROLES_CLAIM, TEST_ROLES_STRING); + static final JWTClaimsSet MC_COY_NESTED_SUBJECT = create( + null, + TEST_AUDIENCE, + TEST_ISSUER, + NESTED_MCCOY_SUBJECT, + MCCOY_SUBJECT, + ROLES_CLAIM, + TEST_ROLES_STRING + ); + static final JWTClaimsSet MC_COY_NESTED_ROLES = create( MCCOY_SUBJECT, TEST_AUDIENCE, @@ -54,6 +66,16 @@ class TestJwts { TEST_ROLES_STRING ); + static final JWTClaimsSet MC_COY_NESTED_ROLES_AND_SUBJECT = create( + null, + TEST_AUDIENCE, + TEST_ISSUER, + NESTED_ROLES_CLAIM, + TEST_ROLES_STRING, + NESTED_ROLES_AND_SUBJECT_CLAIM, + MCCOY_SUBJECT + ); + static final JWTClaimsSet MC_COY_NO_AUDIENCE = create(MCCOY_SUBJECT, null, TEST_ISSUER, ROLES_CLAIM, TEST_ROLES_STRING); static final JWTClaimsSet MC_COY_NO_ISSUER = create(MCCOY_SUBJECT, TEST_AUDIENCE, null, ROLES_CLAIM, TEST_ROLES_STRING); @@ -71,8 +93,9 @@ class TestJwts { static final String MC_COY_SIGNED_OCT_1 = createSigned(MC_COY, TestJwk.OCT_1); static final String MC_COY_SIGNED_OCT_2 = createSigned(MC_COY_2, TestJwk.OCT_2); - + static final String MC_COY_SIGNED_NESTED_SUBJECT_OCT_1 = createSigned(MC_COY_NESTED_SUBJECT, TestJwk.OCT_1); static final String MC_COY_SIGNED_NESTED_ROLES_OCT_1 = createSigned(MC_COY_NESTED_ROLES, TestJwk.OCT_1); + static final String MC_COY_SIGNED_NESTED_ROLES_AND_SUBJECT_OCT_1 = createSigned(MC_COY_NESTED_ROLES_AND_SUBJECT, TestJwk.OCT_1); static final String MC_COY_SIGNED_NO_AUDIENCE_OCT_1 = createSigned(MC_COY_NO_AUDIENCE, TestJwk.OCT_1); static final String MC_COY_SIGNED_NO_ISSUER_OCT_1 = createSigned(MC_COY_NO_ISSUER, TestJwk.OCT_1); @@ -96,8 +119,11 @@ static class PeculiarEscaping { static JWTClaimsSet create(String subject, String audience, String issuer, Object... moreClaims) { JWTClaimsSet.Builder claimsBuilder = new JWTClaimsSet.Builder(); + // Handle only simple subject case + if (subject != null) { + claimsBuilder.subject(String.valueOf(subject)); + } - claimsBuilder.subject(subject); if (audience != null) { claimsBuilder.audience(audience); } From 68f3bd3c31f1f9775698943b9b355dc850faf9e3 Mon Sep 17 00:00:00 2001 From: Rishav Kumar Date: Mon, 14 Jul 2025 13:25:54 +0530 Subject: [PATCH 02/20] unit-test-debug Signed-off-by: Rishav Kumar --- .../keybyoidc/HTTPJwtKeyByOpenIdConnectAuthenticatorTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/opensearch/security/auth/http/jwt/keybyoidc/HTTPJwtKeyByOpenIdConnectAuthenticatorTest.java b/src/test/java/org/opensearch/security/auth/http/jwt/keybyoidc/HTTPJwtKeyByOpenIdConnectAuthenticatorTest.java index a6534d4e57..e2f43433bf 100644 --- a/src/test/java/org/opensearch/security/auth/http/jwt/keybyoidc/HTTPJwtKeyByOpenIdConnectAuthenticatorTest.java +++ b/src/test/java/org/opensearch/security/auth/http/jwt/keybyoidc/HTTPJwtKeyByOpenIdConnectAuthenticatorTest.java @@ -314,7 +314,7 @@ public void testSubjectInNestedClaim() { public void testSubjectAndRolesInNestedClaim() { Settings settings = Settings.builder() .put("openid_connect_url", mockIdpServer.getDiscoverUri()) - .putList("subject_key", TestJwts.NESTED_MCCOY_SUBJECT) + .putList("subject_key", TestJwts.NESTED_ROLES_AND_SUBJECT_CLAIM) .putList("roles_key", TestJwts.NESTED_ROLES_CLAIM) .put("required_issuer", TestJwts.TEST_ISSUER) .put("required_audience", TestJwts.TEST_AUDIENCE) From b2532eba8ceaed576550ab3ef1b56dbebbe3f579 Mon Sep 17 00:00:00 2001 From: Rishav Kumar Date: Mon, 14 Jul 2025 15:43:22 +0530 Subject: [PATCH 03/20] added combined nested claims Signed-off-by: Rishav Kumar --- .../auth/http/jwt/keybyoidc/TestJwts.java | 86 ++++++++++--------- 1 file changed, 47 insertions(+), 39 deletions(-) diff --git a/src/test/java/org/opensearch/security/auth/http/jwt/keybyoidc/TestJwts.java b/src/test/java/org/opensearch/security/auth/http/jwt/keybyoidc/TestJwts.java index 05433c2a05..e1e36b6f5e 100644 --- a/src/test/java/org/opensearch/security/auth/http/jwt/keybyoidc/TestJwts.java +++ b/src/test/java/org/opensearch/security/auth/http/jwt/keybyoidc/TestJwts.java @@ -119,51 +119,59 @@ static class PeculiarEscaping { static JWTClaimsSet create(String subject, String audience, String issuer, Object... moreClaims) { JWTClaimsSet.Builder claimsBuilder = new JWTClaimsSet.Builder(); - // Handle only simple subject case - if (subject != null) { - claimsBuilder.subject(String.valueOf(subject)); - } - - if (audience != null) { - claimsBuilder.audience(audience); - } - if (issuer != null) { - claimsBuilder.issuer(issuer); - } + + // Handle only simple subject case + if (subject != null) { + claimsBuilder.subject(String.valueOf(subject)); + } + if (audience != null) { + claimsBuilder.audience(audience); + } + if (issuer != null) { + claimsBuilder.issuer(issuer); + } - if (moreClaims != null) { - for (int i = 0; i < moreClaims.length; i += 2) { - Object claimPath = moreClaims[i]; - Object claimValue = moreClaims[i + 1]; - - if (claimPath instanceof List pathParts) { - // Handle nested path specified as List - if (!pathParts.isEmpty()) { - Map nestedMap = new HashMap<>(); - Map currentMap = nestedMap; - - // Build nested structure for all but last element - for (int j = 0; j < pathParts.size() - 1; j++) { - Map nextMap = new HashMap<>(); - currentMap.put(String.valueOf(pathParts.get(j)), nextMap); - currentMap = nextMap; + Map topLevelClaims = new HashMap<>(); + + if (moreClaims != null) { + for (int i = 0; i < moreClaims.length; i += 2) { + Object claimPath = moreClaims[i]; + Object claimValue = moreClaims[i + 1]; + + if (claimPath instanceof List pathParts) { + if (!pathParts.isEmpty()) { + // Get or create the top-level map + String topLevelKey = String.valueOf(pathParts.get(0)); + Map currentMap = (Map) topLevelClaims + .computeIfAbsent(topLevelKey, k -> new HashMap()); + + // Navigate to the correct nested level + for (int j = 1; j < pathParts.size() - 1; j++) { + String key = String.valueOf(pathParts.get(j)); + currentMap = (Map) currentMap + .computeIfAbsent(key, k -> new HashMap()); + } + + // Set the final value + String lastKey = String.valueOf(pathParts.get(pathParts.size() - 1)); + if (claimValue instanceof String && lastKey.equals("roles")) { + // Handle roles as array + currentMap.put(lastKey, Arrays.asList(((String) claimValue).split(","))); + } else { + currentMap.put(lastKey, claimValue); + } } - - // Set the final value at the deepest level - currentMap.put(String.valueOf(pathParts.get(pathParts.size() - 1)), claimValue); - - // Add the top-level claim - claimsBuilder.claim(String.valueOf(pathParts.get(0)), nestedMap.get(pathParts.get(0))); + } else { + // Handle simple claim + topLevelClaims.put(String.valueOf(claimPath), claimValue); } - } else { - // Handle simple claim - claimsBuilder.claim(String.valueOf(claimPath), claimValue); } } - } - // JwtToken result = new JwtToken(claimsBuilder); - return claimsBuilder.build(); + // Add all claims to the builder + topLevelClaims.forEach(claimsBuilder::claim); + + return claimsBuilder.build(); } static String createSigned(JWTClaimsSet jwtClaimsSet, JWK jwk) { From 75a85436ebd2192a6683ab76195872c1176746f8 Mon Sep 17 00:00:00 2001 From: Rishav Kumar Date: Mon, 14 Jul 2025 15:53:36 +0530 Subject: [PATCH 04/20] debug-unit-test Signed-off-by: Rishav Kumar --- .../opensearch/security/auth/http/jwt/keybyoidc/TestJwts.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/java/org/opensearch/security/auth/http/jwt/keybyoidc/TestJwts.java b/src/test/java/org/opensearch/security/auth/http/jwt/keybyoidc/TestJwts.java index e1e36b6f5e..59c21192df 100644 --- a/src/test/java/org/opensearch/security/auth/http/jwt/keybyoidc/TestJwts.java +++ b/src/test/java/org/opensearch/security/auth/http/jwt/keybyoidc/TestJwts.java @@ -15,6 +15,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.Arrays; import com.google.common.collect.ImmutableSet; From 985deaa1f16c7019302ed47ec548aa8bc5da82a9 Mon Sep 17 00:00:00 2001 From: Rishav Kumar Date: Mon, 14 Jul 2025 16:01:11 +0530 Subject: [PATCH 05/20] debug-temp Signed-off-by: Rishav Kumar --- .../auth/http/jwt/keybyoidc/TestJwts.java | 100 ++++++++++-------- 1 file changed, 56 insertions(+), 44 deletions(-) diff --git a/src/test/java/org/opensearch/security/auth/http/jwt/keybyoidc/TestJwts.java b/src/test/java/org/opensearch/security/auth/http/jwt/keybyoidc/TestJwts.java index 59c21192df..00630e04c5 100644 --- a/src/test/java/org/opensearch/security/auth/http/jwt/keybyoidc/TestJwts.java +++ b/src/test/java/org/opensearch/security/auth/http/jwt/keybyoidc/TestJwts.java @@ -118,61 +118,73 @@ static class PeculiarEscaping { static final String MC_COY_SIGNED_RSA_1 = createSignedWithPeculiarEscaping(MC_COY, TestJwk.RSA_1); } + @SuppressWarnings("unchecked") static JWTClaimsSet create(String subject, String audience, String issuer, Object... moreClaims) { JWTClaimsSet.Builder claimsBuilder = new JWTClaimsSet.Builder(); - - // Handle only simple subject case - if (subject != null) { - claimsBuilder.subject(String.valueOf(subject)); - } - if (audience != null) { - claimsBuilder.audience(audience); - } - if (issuer != null) { - claimsBuilder.issuer(issuer); - } + + if (subject != null) { + claimsBuilder.subject(String.valueOf(subject)); + } + if (audience != null) { + claimsBuilder.audience(audience); + } + if (issuer != null) { + claimsBuilder.issuer(issuer); + } - Map topLevelClaims = new HashMap<>(); - - if (moreClaims != null) { - for (int i = 0; i < moreClaims.length; i += 2) { - Object claimPath = moreClaims[i]; - Object claimValue = moreClaims[i + 1]; - - if (claimPath instanceof List pathParts) { - if (!pathParts.isEmpty()) { - // Get or create the top-level map - String topLevelKey = String.valueOf(pathParts.get(0)); - Map currentMap = (Map) topLevelClaims - .computeIfAbsent(topLevelKey, k -> new HashMap()); - - // Navigate to the correct nested level - for (int j = 1; j < pathParts.size() - 1; j++) { - String key = String.valueOf(pathParts.get(j)); - currentMap = (Map) currentMap - .computeIfAbsent(key, k -> new HashMap()); - } + Map topLevelClaims = new HashMap<>(); + + if (moreClaims != null) { + for (int i = 0; i < moreClaims.length; i += 2) { + Object claimPath = moreClaims[i]; + Object claimValue = moreClaims[i + 1]; + + if (claimPath instanceof List pathParts) { + if (!pathParts.isEmpty()) { + String topLevelKey = String.valueOf(pathParts.get(0)); + @SuppressWarnings("unchecked") + Map currentMap = topLevelClaims.containsKey(topLevelKey) + ? (Map) topLevelClaims.get(topLevelKey) + : new HashMap<>(); + + if (!topLevelClaims.containsKey(topLevelKey)) { + topLevelClaims.put(topLevelKey, currentMap); + } - // Set the final value - String lastKey = String.valueOf(pathParts.get(pathParts.size() - 1)); - if (claimValue instanceof String && lastKey.equals("roles")) { - // Handle roles as array - currentMap.put(lastKey, Arrays.asList(((String) claimValue).split(","))); - } else { - currentMap.put(lastKey, claimValue); + // Navigate to the correct nested level + for (int j = 1; j < pathParts.size() - 1; j++) { + String key = String.valueOf(pathParts.get(j)); + @SuppressWarnings("unchecked") + Map nextMap = currentMap.containsKey(key) + ? (Map) currentMap.get(key) + : new HashMap<>(); + + if (!currentMap.containsKey(key)) { + currentMap.put(key, nextMap); } + currentMap = nextMap; + } + + // Set the final value + String lastKey = String.valueOf(pathParts.get(pathParts.size() - 1)); + if (claimValue instanceof String && lastKey.equals("roles")) { + // Handle roles as array + currentMap.put(lastKey, Arrays.asList(((String) claimValue).split(","))); + } else { + currentMap.put(lastKey, claimValue); } - } else { - // Handle simple claim - topLevelClaims.put(String.valueOf(claimPath), claimValue); } + } else { + // Handle simple claim + topLevelClaims.put(String.valueOf(claimPath), claimValue); } } + } - // Add all claims to the builder - topLevelClaims.forEach(claimsBuilder::claim); + // Add all claims to the builder + topLevelClaims.forEach(claimsBuilder::claim); - return claimsBuilder.build(); + return claimsBuilder.build(); } static String createSigned(JWTClaimsSet jwtClaimsSet, JWK jwk) { From 7b36b937847aeb42dcbe367edc749d56b36c0bd8 Mon Sep 17 00:00:00 2001 From: Rishav Kumar Date: Mon, 14 Jul 2025 16:24:42 +0530 Subject: [PATCH 06/20] debug-temp Signed-off-by: Rishav Kumar --- .../opensearch/security/auth/http/jwt/keybyoidc/TestJwts.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/org/opensearch/security/auth/http/jwt/keybyoidc/TestJwts.java b/src/test/java/org/opensearch/security/auth/http/jwt/keybyoidc/TestJwts.java index 00630e04c5..0620681650 100644 --- a/src/test/java/org/opensearch/security/auth/http/jwt/keybyoidc/TestJwts.java +++ b/src/test/java/org/opensearch/security/auth/http/jwt/keybyoidc/TestJwts.java @@ -154,7 +154,6 @@ static JWTClaimsSet create(String subject, String audience, String issuer, Objec // Navigate to the correct nested level for (int j = 1; j < pathParts.size() - 1; j++) { String key = String.valueOf(pathParts.get(j)); - @SuppressWarnings("unchecked") Map nextMap = currentMap.containsKey(key) ? (Map) currentMap.get(key) : new HashMap<>(); From d68b18771fbdc1fc7caf8e84871d7ca440e6e0d6 Mon Sep 17 00:00:00 2001 From: Rishav Kumar Date: Mon, 14 Jul 2025 16:50:11 +0530 Subject: [PATCH 07/20] refactoring existing integs Signed-off-by: Rishav Kumar --- .../org/opensearch/security/http/JwtAuthenticationTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationTests.java b/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationTests.java index 6173dd7c55..3f2f8f7db5 100644 --- a/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationTests.java +++ b/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationTests.java @@ -68,7 +68,7 @@ @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class JwtAuthenticationTests { - public static final String CLAIM_USERNAME = "preferred-username"; + public static final List CLAIM_USERNAME = List.of("preferred-username"); public static final List CLAIM_ROLES = List.of("backend-user-roles"); public static final String USER_SUPERHERO = "superhero"; From 8be7b643c2134dd9aae5b9ab4fa513790affa846 Mon Sep 17 00:00:00 2001 From: Rishav Kumar Date: Mon, 14 Jul 2025 17:09:56 +0530 Subject: [PATCH 08/20] adding-integ Signed-off-by: Rishav Kumar --- .../JwtAuthenticationWithUrlParamTests.java | 2 +- .../http/JwtAuthorizationHeaderFactory.java | 29 +++++++++++++++++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationWithUrlParamTests.java b/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationWithUrlParamTests.java index 788abb1432..d30643f758 100644 --- a/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationWithUrlParamTests.java +++ b/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationWithUrlParamTests.java @@ -50,7 +50,7 @@ @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class JwtAuthenticationWithUrlParamTests { - public static final String CLAIM_USERNAME = "preferred-username"; + public static final List CLAIM_USERNAME = List.of("preferred-username"); public static final List CLAIM_ROLES = List.of("backend-user-roles"); public static final String POINTER_USERNAME = "/user_name"; diff --git a/src/integrationTest/java/org/opensearch/security/http/JwtAuthorizationHeaderFactory.java b/src/integrationTest/java/org/opensearch/security/http/JwtAuthorizationHeaderFactory.java index d189d3062d..9c6ed1f401 100644 --- a/src/integrationTest/java/org/opensearch/security/http/JwtAuthorizationHeaderFactory.java +++ b/src/integrationTest/java/org/opensearch/security/http/JwtAuthorizationHeaderFactory.java @@ -30,13 +30,13 @@ class JwtAuthorizationHeaderFactory { public static final String ISSUER = "test-code"; private final PrivateKey privateKey; - private final String usernameClaimName; + private final List usernameClaimName; private final List rolesClaimName; private final String headerName; - public JwtAuthorizationHeaderFactory(PrivateKey privateKey, String usernameClaimName, List rolesClaimName, String headerName) { + public JwtAuthorizationHeaderFactory(PrivateKey privateKey, List usernameClaimName, List rolesClaimName, String headerName) { this.privateKey = requireNonNull(privateKey, "Private key is required"); this.usernameClaimName = requireNonNull(usernameClaimName, "Username claim name is required"); this.rolesClaimName = requireNonNull(rolesClaimName, "Roles claim name is required."); @@ -60,9 +60,32 @@ Header generateValidToken(String username, String... roles) { private Map customClaimsMap(String username, String[] roles) { ImmutableMap.Builder builder = new ImmutableMap.Builder(); + // Handle username claim if (StringUtils.isNoneEmpty(username)) { - builder.put(usernameClaimName, username); + if (usernameClaimName instanceof List && !((List) usernameClaimName).isEmpty()) { + // Handle nested username claim + List usernamePath = (List) usernameClaimName; + Map nestedUserMap = new HashMap<>(); + Map currentUserMap = nestedUserMap; + + // Build the nested structure for username + for (int i = 0; i < usernamePath.size() - 1; i++) { + Map nextMap = new HashMap<>(); + currentUserMap.put(usernamePath.get(i), nextMap); + currentUserMap = nextMap; + } + + // Add the username at the deepest level + currentUserMap.put(usernamePath.get(usernamePath.size() - 1), username); + + // Add the entire nested username structure to the builder + builder.putAll(nestedUserMap); + } else { + // Simple case - no nesting for username + builder.put(usernameClaimName.toString(), username); + } } + if (roles != null && roles.length > 0) { if (rolesClaimName.size() == 1) { // Simple case - no nesting From e255b505df3bac4d4cc626154a781acf50da8b9c Mon Sep 17 00:00:00 2001 From: Rishav Kumar Date: Mon, 14 Jul 2025 17:13:52 +0530 Subject: [PATCH 09/20] integ-refactor Signed-off-by: Rishav Kumar --- .../security/http/JwtAuthenticationNestedClaimsTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationNestedClaimsTests.java b/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationNestedClaimsTests.java index 47f4a9c980..3c2f769481 100644 --- a/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationNestedClaimsTests.java +++ b/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationNestedClaimsTests.java @@ -47,7 +47,7 @@ @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class JwtAuthenticationNestedClaimsTests { - public static final String CLAIM_USERNAME = "preferred-username"; + public static final List CLAIM_USERNAME = List.of("preferred-username"); public static final List CLAIM_ROLES = List.of("attributes", "roles"); public static final String USER_SUPERHERO = "superhero"; From 362bf90e9e699e7c9753969d7fb18c7a92ddb205 Mon Sep 17 00:00:00 2001 From: Rishav Kumar Date: Wed, 23 Jul 2025 15:10:25 +0530 Subject: [PATCH 10/20] Refactor JwtAuthorizationHeaderFactory to handle List usernameClaimName. Signed-off-by: Rishav Kumar --- .../security/http/JwtAuthorizationHeaderFactory.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/http/JwtAuthorizationHeaderFactory.java b/src/integrationTest/java/org/opensearch/security/http/JwtAuthorizationHeaderFactory.java index 9c6ed1f401..c61318a3e9 100644 --- a/src/integrationTest/java/org/opensearch/security/http/JwtAuthorizationHeaderFactory.java +++ b/src/integrationTest/java/org/opensearch/security/http/JwtAuthorizationHeaderFactory.java @@ -151,7 +151,7 @@ public Header generateExpiredToken(String username) { requireNonNull(username, "Username is required"); Date now = new Date(1000); String token = Jwts.builder() - .setClaims(Map.of(usernameClaimName, username)) + .setClaims(customClaimsMap(username, null)) .setIssuer(ISSUER) .setSubject(subject(username)) .setAudience(AUDIENCE) @@ -167,7 +167,7 @@ public Header generateTokenSignedWithKey(PrivateKey key, String username) { requireNonNull(username, "Username is required"); Date now = new Date(); String token = Jwts.builder() - .setClaims(Map.of(usernameClaimName, username)) + .setClaims(customClaimsMap(username, null)) .setIssuer(ISSUER) .setSubject(subject(username)) .setAudience(AUDIENCE) From d0cfa791950dd3d9ce4e9f5b87ef4481fa4baa69 Mon Sep 17 00:00:00 2001 From: Rishav Kumar Date: Wed, 23 Jul 2025 16:10:46 +0530 Subject: [PATCH 11/20] added extract subject refoator Signed-off-by: Rishav Kumar --- .../security/http/JwtAuthorizationHeaderFactory.java | 7 ++++++- .../auth/http/jwt/AbstractHTTPJwtAuthenticator.java | 2 +- .../security/auth/http/jwt/HTTPJwtAuthenticator.java | 4 ++-- .../security/auth/http/jwt/keybyoidc/TestJwts.java | 10 +++++----- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/http/JwtAuthorizationHeaderFactory.java b/src/integrationTest/java/org/opensearch/security/http/JwtAuthorizationHeaderFactory.java index c61318a3e9..caabf713d3 100644 --- a/src/integrationTest/java/org/opensearch/security/http/JwtAuthorizationHeaderFactory.java +++ b/src/integrationTest/java/org/opensearch/security/http/JwtAuthorizationHeaderFactory.java @@ -36,7 +36,12 @@ class JwtAuthorizationHeaderFactory { private final String headerName; - public JwtAuthorizationHeaderFactory(PrivateKey privateKey, List usernameClaimName, List rolesClaimName, String headerName) { + public JwtAuthorizationHeaderFactory( + PrivateKey privateKey, + List usernameClaimName, + List rolesClaimName, + String headerName + ) { this.privateKey = requireNonNull(privateKey, "Private key is required"); this.usernameClaimName = requireNonNull(usernameClaimName, "Username claim name is required"); this.rolesClaimName = requireNonNull(rolesClaimName, "Roles claim name is required."); diff --git a/src/main/java/org/opensearch/security/auth/http/jwt/AbstractHTTPJwtAuthenticator.java b/src/main/java/org/opensearch/security/auth/http/jwt/AbstractHTTPJwtAuthenticator.java index 55088a1111..0565c005ee 100644 --- a/src/main/java/org/opensearch/security/auth/http/jwt/AbstractHTTPJwtAuthenticator.java +++ b/src/main/java/org/opensearch/security/auth/http/jwt/AbstractHTTPJwtAuthenticator.java @@ -261,7 +261,7 @@ public String[] extractRoles(JWTClaimsSet claims) { ); return new String[0]; } - + log.warn("rolesObject is '{}' ", rolesObject); String[] roles = String.valueOf(rolesObject).split(","); diff --git a/src/main/java/org/opensearch/security/auth/http/jwt/HTTPJwtAuthenticator.java b/src/main/java/org/opensearch/security/auth/http/jwt/HTTPJwtAuthenticator.java index 96c52520f7..652ad0aca7 100644 --- a/src/main/java/org/opensearch/security/auth/http/jwt/HTTPJwtAuthenticator.java +++ b/src/main/java/org/opensearch/security/auth/http/jwt/HTTPJwtAuthenticator.java @@ -267,12 +267,12 @@ protected String extractSubject(final Claims claims) { node, node.getClass() ); - return subject; // Return default subject on error + return null; // Subject cannot be extracted from the configured path } node = map.get(key); if (node == null) { // key missing log.warn("Failed to find '{}' in JWT claims while following subject_key path {}.", key, subjectKey); - return subject; // Return default subject on error + return null; // Subject cannot be extracted from the configured path } } // ── 2. Interpret the leaf value ──────────────────────────────────────────── diff --git a/src/test/java/org/opensearch/security/auth/http/jwt/keybyoidc/TestJwts.java b/src/test/java/org/opensearch/security/auth/http/jwt/keybyoidc/TestJwts.java index 0620681650..9971ca4bc5 100644 --- a/src/test/java/org/opensearch/security/auth/http/jwt/keybyoidc/TestJwts.java +++ b/src/test/java/org/opensearch/security/auth/http/jwt/keybyoidc/TestJwts.java @@ -11,11 +11,11 @@ package org.opensearch.security.auth.http.jwt.keybyoidc; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.Arrays; import com.google.common.collect.ImmutableSet; @@ -121,7 +121,7 @@ static class PeculiarEscaping { @SuppressWarnings("unchecked") static JWTClaimsSet create(String subject, String audience, String issuer, Object... moreClaims) { JWTClaimsSet.Builder claimsBuilder = new JWTClaimsSet.Builder(); - + if (subject != null) { claimsBuilder.subject(String.valueOf(subject)); } @@ -143,10 +143,10 @@ static JWTClaimsSet create(String subject, String audience, String issuer, Objec if (!pathParts.isEmpty()) { String topLevelKey = String.valueOf(pathParts.get(0)); @SuppressWarnings("unchecked") - Map currentMap = topLevelClaims.containsKey(topLevelKey) + Map currentMap = topLevelClaims.containsKey(topLevelKey) ? (Map) topLevelClaims.get(topLevelKey) : new HashMap<>(); - + if (!topLevelClaims.containsKey(topLevelKey)) { topLevelClaims.put(topLevelKey, currentMap); } @@ -157,7 +157,7 @@ static JWTClaimsSet create(String subject, String audience, String issuer, Objec Map nextMap = currentMap.containsKey(key) ? (Map) currentMap.get(key) : new HashMap<>(); - + if (!currentMap.containsKey(key)) { currentMap.put(key, nextMap); } From dc9d7d68b2345bcdb1ce1b2ad34810a01281647f Mon Sep 17 00:00:00 2001 From: Rishav Kumar Date: Wed, 23 Jul 2025 22:02:14 +0530 Subject: [PATCH 12/20] adding new integ Signed-off-by: Rishav Kumar --- .../http/JwtAuthenticationNestedClaimsTests.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationNestedClaimsTests.java b/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationNestedClaimsTests.java index 3c2f769481..9084fbe604 100644 --- a/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationNestedClaimsTests.java +++ b/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationNestedClaimsTests.java @@ -48,7 +48,8 @@ public class JwtAuthenticationNestedClaimsTests { public static final List CLAIM_USERNAME = List.of("preferred-username"); - public static final List CLAIM_ROLES = List.of("attributes", "roles"); + public static final List NESTED_ROLES = List.of("attributes", "roles"); + public static final List NESTED_SUBJECT = List.of("attributes_sub", "sub"); public static final String USER_SUPERHERO = "superhero"; private static final KeyPair KEY_PAIR1 = Keys.keyPairFor(SignatureAlgorithm.RS256); @@ -58,14 +59,18 @@ public class JwtAuthenticationNestedClaimsTests { private static final JwtAuthorizationHeaderFactory tokenFactory1 = new JwtAuthorizationHeaderFactory( KEY_PAIR1.getPrivate(), CLAIM_USERNAME, - CLAIM_ROLES, + NESTED_ROLES, JWT_AUTH_HEADER ); public static final TestSecurityConfig.AuthcDomain JWT_AUTH_DOMAIN = new TestSecurityConfig.AuthcDomain( "jwt", BASIC_AUTH_DOMAIN_ORDER - 1 ).jwtHttpAuthenticator( - new JwtConfigBuilder().jwtHeader(JWT_AUTH_HEADER).signingKey(List.of(PUBLIC_KEY1)).subjectKey(CLAIM_USERNAME).rolesKey(CLAIM_ROLES) + new JwtConfigBuilder().jwtHeader(JWT_AUTH_HEADER).signingKey(List.of(PUBLIC_KEY1)).subjectKey(CLAIM_USERNAME).rolesKey(NESTED_ROLES).build( + new HashMap<>( + Map.of( + "noop", List.of("noop") + ).backend("noop"); @ClassRule From 48fb47b53be2f5399c99323813ffceabc5cee03a Mon Sep 17 00:00:00 2001 From: Rishav Kumar Date: Wed, 23 Jul 2025 22:06:16 +0530 Subject: [PATCH 13/20] adding new integ Signed-off-by: Rishav Kumar --- .../security/http/JwtAuthenticationNestedClaimsTests.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationNestedClaimsTests.java b/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationNestedClaimsTests.java index 9084fbe604..bb73b95d16 100644 --- a/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationNestedClaimsTests.java +++ b/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationNestedClaimsTests.java @@ -66,11 +66,7 @@ public class JwtAuthenticationNestedClaimsTests { "jwt", BASIC_AUTH_DOMAIN_ORDER - 1 ).jwtHttpAuthenticator( - new JwtConfigBuilder().jwtHeader(JWT_AUTH_HEADER).signingKey(List.of(PUBLIC_KEY1)).subjectKey(CLAIM_USERNAME).rolesKey(NESTED_ROLES).build( - new HashMap<>( - Map.of( - "noop", List.of("noop") - + new JwtConfigBuilder().jwtHeader(JWT_AUTH_HEADER).signingKey(List.of(PUBLIC_KEY1)).subjectKey(CLAIM_USERNAME).rolesKey(NESTED_ROLES) ).backend("noop"); @ClassRule From 5d03fc00093865f468d78c768d403ff73b193861 Mon Sep 17 00:00:00 2001 From: Rishav Kumar Date: Wed, 23 Jul 2025 23:52:43 +0530 Subject: [PATCH 14/20] added new integ Signed-off-by: Rishav Kumar --- .../JwtAuthenticationNestedClaimsTests.java | 50 +++++++++++++++++-- .../http/JwtAuthorizationHeaderFactory.java | 2 +- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationNestedClaimsTests.java b/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationNestedClaimsTests.java index bb73b95d16..fa9386f58c 100644 --- a/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationNestedClaimsTests.java +++ b/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationNestedClaimsTests.java @@ -47,9 +47,10 @@ @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class JwtAuthenticationNestedClaimsTests { - public static final List CLAIM_USERNAME = List.of("preferred-username"); + public static final List USERNAME_CLAIM = List.of("preferred-username"); public static final List NESTED_ROLES = List.of("attributes", "roles"); public static final List NESTED_SUBJECT = List.of("attributes_sub", "sub"); + public static final List ROLES_CLAIM = List.of("all_access", "securitymanager"); public static final String USER_SUPERHERO = "superhero"; private static final KeyPair KEY_PAIR1 = Keys.keyPairFor(SignatureAlgorithm.RS256); @@ -58,7 +59,7 @@ public class JwtAuthenticationNestedClaimsTests { private static final JwtAuthorizationHeaderFactory tokenFactory1 = new JwtAuthorizationHeaderFactory( KEY_PAIR1.getPrivate(), - CLAIM_USERNAME, + USERNAME_CLAIM, NESTED_ROLES, JWT_AUTH_HEADER ); @@ -66,8 +67,15 @@ public class JwtAuthenticationNestedClaimsTests { "jwt", BASIC_AUTH_DOMAIN_ORDER - 1 ).jwtHttpAuthenticator( - new JwtConfigBuilder().jwtHeader(JWT_AUTH_HEADER).signingKey(List.of(PUBLIC_KEY1)).subjectKey(CLAIM_USERNAME).rolesKey(NESTED_ROLES) + new JwtConfigBuilder().jwtHeader(JWT_AUTH_HEADER).signingKey(List.of(PUBLIC_KEY1)).subjectKey(USERNAME_CLAIM).rolesKey(NESTED_ROLES) ).backend("noop"); + + private static final JwtAuthorizationHeaderFactory tokenFactoryNestedSubjectAndRole = new JwtAuthorizationHeaderFactory( + KEY_PAIR1.getPrivate(), + NESTED_SUBJECT, + NESTED_ROLES, + JWT_AUTH_HEADER + ); @ClassRule public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) @@ -83,8 +91,7 @@ public class JwtAuthenticationNestedClaimsTests { public void shouldAuthenticateWithNestedRolesClaim() { // Create nested claims structure Map attributes = new HashMap<>(); - List rolesClaim = Arrays.asList("all_access", "securitymanager"); - attributes.put("roles", rolesClaim); + attributes.put("roles", ROLES_CLAIM); Map nestedClaims = new HashMap<>(); nestedClaims.put("attributes", attributes); @@ -125,4 +132,37 @@ public void shouldHandleMissingNestedRolesClaim() { assertThat(roles, hasSize(0)); } } + + @Test + public void shouldAuthenticateWithNestedSubjectAndNestedRoles() { + // Create nested subject structure + Map attributesSub = new HashMap<>(); + attributesSub.put("username", USER_SUPERHERO); + + // Create nested roles structure + Map attributes = new HashMap<>(); + attributes.put("roles", ROLES_CLAIM); + + // Combine both in the claims + Map nestedClaims = new HashMap<>(); + nestedClaims.put("attributes_sub", attributesSub); + nestedClaims.put("attributes", attributes); + + // Use the token factory with nested subject configuration + Header header = tokenFactoryNestedSubjectAndRole.generateValidTokenWithCustomClaims(null, null, nestedClaims); + + try (TestRestClient client = cluster.getRestClient(header)) { + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + String username = response.getTextFromJsonBody(POINTER_USERNAME); + assertThat(username, equalTo(USER_SUPERHERO)); + + // But roles should still be extracted correctly + List roles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); + assertThat(roles, hasSize(2)); + assertThat(roles, containsInAnyOrder("all_access", "securitymanager")); + } + } + } diff --git a/src/integrationTest/java/org/opensearch/security/http/JwtAuthorizationHeaderFactory.java b/src/integrationTest/java/org/opensearch/security/http/JwtAuthorizationHeaderFactory.java index caabf713d3..77da262a95 100644 --- a/src/integrationTest/java/org/opensearch/security/http/JwtAuthorizationHeaderFactory.java +++ b/src/integrationTest/java/org/opensearch/security/http/JwtAuthorizationHeaderFactory.java @@ -118,7 +118,7 @@ private Map customClaimsMap(String username, String[] roles) { } Header generateValidTokenWithCustomClaims(String username, String[] roles, Map additionalClaims) { - requireNonNull(username, "Username is required"); + // requireNonNull(username, "Username is required"); not required as username can be null requireNonNull(additionalClaims, "Custom claims are required"); Map claims = new HashMap<>(customClaimsMap(username, roles)); claims.putAll(additionalClaims); From 0b8222f27fa2a677410f102af8f7308ce1474163 Mon Sep 17 00:00:00 2001 From: Rishav Kumar Date: Sun, 27 Jul 2025 16:09:17 +0530 Subject: [PATCH 15/20] integ complete Signed-off-by: Rishav Kumar --- .../JwtAuthenticationNestedClaimsTests.java | 216 +++++++++++++++++- 1 file changed, 206 insertions(+), 10 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationNestedClaimsTests.java b/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationNestedClaimsTests.java index fa9386f58c..935fc5bcd0 100644 --- a/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationNestedClaimsTests.java +++ b/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationNestedClaimsTests.java @@ -10,7 +10,6 @@ package org.opensearch.security.http; import java.security.KeyPair; -import java.util.Arrays; import java.util.Base64; import java.util.HashMap; import java.util.List; @@ -50,6 +49,7 @@ public class JwtAuthenticationNestedClaimsTests { public static final List USERNAME_CLAIM = List.of("preferred-username"); public static final List NESTED_ROLES = List.of("attributes", "roles"); public static final List NESTED_SUBJECT = List.of("attributes_sub", "sub"); + public static final List NESTED_SUBJECT_ATTRIBUTES_ONLY = List.of("attributes", "sub"); public static final List ROLES_CLAIM = List.of("all_access", "securitymanager"); public static final String USER_SUPERHERO = "superhero"; @@ -57,30 +57,60 @@ public class JwtAuthenticationNestedClaimsTests { private static final String PUBLIC_KEY1 = new String(Base64.getEncoder().encode(KEY_PAIR1.getPublic().getEncoded()), US_ASCII); private static final String JWT_AUTH_HEADER = "jwt-auth"; + // Token factory for regular subject + nested roles private static final JwtAuthorizationHeaderFactory tokenFactory1 = new JwtAuthorizationHeaderFactory( KEY_PAIR1.getPrivate(), USERNAME_CLAIM, NESTED_ROLES, JWT_AUTH_HEADER ); - public static final TestSecurityConfig.AuthcDomain JWT_AUTH_DOMAIN = new TestSecurityConfig.AuthcDomain( - "jwt", - BASIC_AUTH_DOMAIN_ORDER - 1 - ).jwtHttpAuthenticator( - new JwtConfigBuilder().jwtHeader(JWT_AUTH_HEADER).signingKey(List.of(PUBLIC_KEY1)).subjectKey(USERNAME_CLAIM).rolesKey(NESTED_ROLES) - ).backend("noop"); + // Token factory for nested subject + nested roles private static final JwtAuthorizationHeaderFactory tokenFactoryNestedSubjectAndRole = new JwtAuthorizationHeaderFactory( KEY_PAIR1.getPrivate(), NESTED_SUBJECT, NESTED_ROLES, JWT_AUTH_HEADER ); + + // Token factory for both subject and roles nested under same "attributes" only + private static final JwtAuthorizationHeaderFactory tokenFactoryAttributesOnly = new JwtAuthorizationHeaderFactory( + KEY_PAIR1.getPrivate(), + NESTED_SUBJECT_ATTRIBUTES_ONLY, + NESTED_ROLES, + JWT_AUTH_HEADER + ); + + // JWT domain for regular subject + nested roles + public static final TestSecurityConfig.AuthcDomain JWT_AUTH_DOMAIN = new TestSecurityConfig.AuthcDomain( + "jwt", + BASIC_AUTH_DOMAIN_ORDER - 1 + ).jwtHttpAuthenticator( + new JwtConfigBuilder().jwtHeader(JWT_AUTH_HEADER).signingKey(List.of(PUBLIC_KEY1)).subjectKey(USERNAME_CLAIM).rolesKey(NESTED_ROLES) + ).backend("noop"); + + // JWT domain for nested subject + nested roles + public static final TestSecurityConfig.AuthcDomain JWT_AUTH_DOMAIN_NESTED_SUBJECT = new TestSecurityConfig.AuthcDomain( + "jwt-nested", + BASIC_AUTH_DOMAIN_ORDER - 2 + ).jwtHttpAuthenticator( + new JwtConfigBuilder().jwtHeader(JWT_AUTH_HEADER).signingKey(List.of(PUBLIC_KEY1)).subjectKey(NESTED_SUBJECT).rolesKey(NESTED_ROLES) + ).backend("noop"); + + // JWT domain for both subject and roles using "attributes" only + public static final TestSecurityConfig.AuthcDomain JWT_AUTH_DOMAIN_ATTRIBUTES_ONLY = new TestSecurityConfig.AuthcDomain( + "jwt-attributes-only", + BASIC_AUTH_DOMAIN_ORDER - 3 + ).jwtHttpAuthenticator( + new JwtConfigBuilder().jwtHeader(JWT_AUTH_HEADER).signingKey(List.of(PUBLIC_KEY1)).subjectKey(NESTED_SUBJECT_ATTRIBUTES_ONLY).rolesKey(NESTED_ROLES) + ).backend("noop"); @ClassRule public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) .anonymousAuth(false) .authc(JWT_AUTH_DOMAIN) + .authc(JWT_AUTH_DOMAIN_NESTED_SUBJECT) + .authc(JWT_AUTH_DOMAIN_ATTRIBUTES_ONLY) .build(); @Rule @@ -135,9 +165,9 @@ public void shouldHandleMissingNestedRolesClaim() { @Test public void shouldAuthenticateWithNestedSubjectAndNestedRoles() { - // Create nested subject structure + // Create nested subject structure - the key should match NESTED_SUBJECT path Map attributesSub = new HashMap<>(); - attributesSub.put("username", USER_SUPERHERO); + attributesSub.put("sub", USER_SUPERHERO); // Create nested roles structure Map attributes = new HashMap<>(); @@ -158,7 +188,173 @@ public void shouldAuthenticateWithNestedSubjectAndNestedRoles() { String username = response.getTextFromJsonBody(POINTER_USERNAME); assertThat(username, equalTo(USER_SUPERHERO)); - // But roles should still be extracted correctly + List roles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); + assertThat(roles, hasSize(2)); + assertThat(roles, containsInAnyOrder("all_access", "securitymanager")); + } + } + + @Test + public void shouldAuthenticateWithNestedSubjectAndSimpleRoles() { + // Create nested subject structure + Map attributesSub = new HashMap<>(); + attributesSub.put("sub", USER_SUPERHERO); + + Map nestedClaims = new HashMap<>(); + nestedClaims.put("attributes_sub", attributesSub); + + Header header = tokenFactoryNestedSubjectAndRole.generateValidTokenWithCustomClaims(null, null, nestedClaims); + + try (TestRestClient client = cluster.getRestClient(header)) { + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + String username = response.getTextFromJsonBody(POINTER_USERNAME); + assertThat(username, equalTo(USER_SUPERHERO)); + + // Should have no roles since they're not in the expected nested location + List roles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); + assertThat(roles, hasSize(0)); + } + } + + // Negative test cases + + @Test + public void shouldFailAuthenticationWithMissingNestedSubject() { + // Create nested roles structure but missing nested subject + Map attributes = new HashMap<>(); + attributes.put("roles", ROLES_CLAIM); + + Map nestedClaims = new HashMap<>(); + nestedClaims.put("attributes", attributes); + // Missing attributes_sub structure + + Header header = tokenFactoryNestedSubjectAndRole.generateValidTokenWithCustomClaims(null, null, nestedClaims); + + try (TestRestClient client = cluster.getRestClient(header)) { + HttpResponse response = client.getAuthInfo(); + + // Should fail authentication due to missing subject + response.assertStatusCode(401); + } + } + + @Test + public void shouldFailAuthenticationWithWrongNestedSubjectStructure() { + // Create wrong nested subject structure + Map attributesSub = new HashMap<>(); + attributesSub.put("wrong_key", USER_SUPERHERO); // Wrong key, should be "sub" + + Map attributes = new HashMap<>(); + attributes.put("roles", ROLES_CLAIM); + + Map nestedClaims = new HashMap<>(); + nestedClaims.put("attributes_sub", attributesSub); + nestedClaims.put("attributes", attributes); + + Header header = tokenFactoryNestedSubjectAndRole.generateValidTokenWithCustomClaims(null, null, nestedClaims); + + try (TestRestClient client = cluster.getRestClient(header)) { + HttpResponse response = client.getAuthInfo(); + + // Should fail authentication due to wrong subject structure + response.assertStatusCode(401); + } + } + + @Test + public void shouldAuthenticateWithMissingRolesButValidSubject() { + // Create nested subject structure but missing roles + Map attributesSub = new HashMap<>(); + attributesSub.put("sub", USER_SUPERHERO); + + Map nestedClaims = new HashMap<>(); + nestedClaims.put("attributes_sub", attributesSub); + // Missing roles structure + + Header header = tokenFactoryNestedSubjectAndRole.generateValidTokenWithCustomClaims(null, null, nestedClaims); + + try (TestRestClient client = cluster.getRestClient(header)) { + HttpResponse response = client.getAuthInfo(); + + // Should authenticate but with no roles + response.assertStatusCode(200); + String username = response.getTextFromJsonBody(POINTER_USERNAME); + assertThat(username, equalTo(USER_SUPERHERO)); + + List roles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); + assertThat(roles, hasSize(0)); + } + } + + @Test + public void shouldHandleWrongNestedRolesStructure() { + // Create nested subject structure with wrong roles structure + Map attributesSub = new HashMap<>(); + attributesSub.put("sub", USER_SUPERHERO); + + Map attributes = new HashMap<>(); + attributes.put("wrong_roles_key", ROLES_CLAIM); // Wrong key, should be "roles" + + Map nestedClaims = new HashMap<>(); + nestedClaims.put("attributes_sub", attributesSub); + nestedClaims.put("attributes", attributes); + + Header header = tokenFactoryNestedSubjectAndRole.generateValidTokenWithCustomClaims(null, null, nestedClaims); + + try (TestRestClient client = cluster.getRestClient(header)) { + HttpResponse response = client.getAuthInfo(); + + // Should authenticate but with no roles due to wrong roles structure + response.assertStatusCode(200); + String username = response.getTextFromJsonBody(POINTER_USERNAME); + assertThat(username, equalTo(USER_SUPERHERO)); + + List roles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); + assertThat(roles, hasSize(0)); + } + } + + @Test + public void shouldFailAuthenticationWithCompletelyWrongTokenStructure() { + // Create completely wrong token structure + Map wrongClaims = new HashMap<>(); + wrongClaims.put("completely", "wrong"); + wrongClaims.put("structure", "invalid"); + + Header header = tokenFactoryNestedSubjectAndRole.generateValidTokenWithCustomClaims(null, null, wrongClaims); + + try (TestRestClient client = cluster.getRestClient(header)) { + HttpResponse response = client.getAuthInfo(); + + // Should fail authentication due to completely wrong structure + response.assertStatusCode(401); + } + } + + @Test + public void shouldAuthenticateWithBothSubjectAndRolesInAttributesOnly() { + // Create nested structure where both subject and roles are under "attributes" + // Subject path: attributes -> sub + // Roles path: attributes -> roles + Map attributes = new HashMap<>(); + attributes.put("sub", USER_SUPERHERO); + attributes.put("roles", ROLES_CLAIM); + + Map nestedClaims = new HashMap<>(); + nestedClaims.put("attributes", attributes); + + // Use the token factory configured for attributes-only paths + Header header = tokenFactoryAttributesOnly.generateValidTokenWithCustomClaims(null, null, nestedClaims); + + try (TestRestClient client = cluster.getRestClient(header)) { + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + String username = response.getTextFromJsonBody(POINTER_USERNAME); + assertThat(username, equalTo(USER_SUPERHERO)); + List roles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); assertThat(roles, hasSize(2)); assertThat(roles, containsInAnyOrder("all_access", "securitymanager")); From 279a2f306bdfb76dc2efb92c88d8d49bccc28032 Mon Sep 17 00:00:00 2001 From: Rishav Kumar Date: Sun, 27 Jul 2025 16:11:34 +0530 Subject: [PATCH 16/20] spotlessApply modified Signed-off-by: Rishav Kumar --- .../JwtAuthenticationNestedClaimsTests.java | 115 +++++++++--------- 1 file changed, 59 insertions(+), 56 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationNestedClaimsTests.java b/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationNestedClaimsTests.java index 935fc5bcd0..b51d21d586 100644 --- a/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationNestedClaimsTests.java +++ b/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationNestedClaimsTests.java @@ -57,14 +57,14 @@ public class JwtAuthenticationNestedClaimsTests { private static final String PUBLIC_KEY1 = new String(Base64.getEncoder().encode(KEY_PAIR1.getPublic().getEncoded()), US_ASCII); private static final String JWT_AUTH_HEADER = "jwt-auth"; - // Token factory for regular subject + nested roles + // Token factory for regular subject + nested roles private static final JwtAuthorizationHeaderFactory tokenFactory1 = new JwtAuthorizationHeaderFactory( KEY_PAIR1.getPrivate(), USERNAME_CLAIM, NESTED_ROLES, JWT_AUTH_HEADER ); - + // Token factory for nested subject + nested roles private static final JwtAuthorizationHeaderFactory tokenFactoryNestedSubjectAndRole = new JwtAuthorizationHeaderFactory( KEY_PAIR1.getPrivate(), @@ -72,7 +72,7 @@ public class JwtAuthenticationNestedClaimsTests { NESTED_ROLES, JWT_AUTH_HEADER ); - + // Token factory for both subject and roles nested under same "attributes" only private static final JwtAuthorizationHeaderFactory tokenFactoryAttributesOnly = new JwtAuthorizationHeaderFactory( KEY_PAIR1.getPrivate(), @@ -81,14 +81,14 @@ public class JwtAuthenticationNestedClaimsTests { JWT_AUTH_HEADER ); - // JWT domain for regular subject + nested roles + // JWT domain for regular subject + nested roles public static final TestSecurityConfig.AuthcDomain JWT_AUTH_DOMAIN = new TestSecurityConfig.AuthcDomain( "jwt", BASIC_AUTH_DOMAIN_ORDER - 1 ).jwtHttpAuthenticator( new JwtConfigBuilder().jwtHeader(JWT_AUTH_HEADER).signingKey(List.of(PUBLIC_KEY1)).subjectKey(USERNAME_CLAIM).rolesKey(NESTED_ROLES) ).backend("noop"); - + // JWT domain for nested subject + nested roles public static final TestSecurityConfig.AuthcDomain JWT_AUTH_DOMAIN_NESTED_SUBJECT = new TestSecurityConfig.AuthcDomain( "jwt-nested", @@ -96,13 +96,16 @@ public class JwtAuthenticationNestedClaimsTests { ).jwtHttpAuthenticator( new JwtConfigBuilder().jwtHeader(JWT_AUTH_HEADER).signingKey(List.of(PUBLIC_KEY1)).subjectKey(NESTED_SUBJECT).rolesKey(NESTED_ROLES) ).backend("noop"); - + // JWT domain for both subject and roles using "attributes" only public static final TestSecurityConfig.AuthcDomain JWT_AUTH_DOMAIN_ATTRIBUTES_ONLY = new TestSecurityConfig.AuthcDomain( "jwt-attributes-only", BASIC_AUTH_DOMAIN_ORDER - 3 ).jwtHttpAuthenticator( - new JwtConfigBuilder().jwtHeader(JWT_AUTH_HEADER).signingKey(List.of(PUBLIC_KEY1)).subjectKey(NESTED_SUBJECT_ATTRIBUTES_ONLY).rolesKey(NESTED_ROLES) + new JwtConfigBuilder().jwtHeader(JWT_AUTH_HEADER) + .signingKey(List.of(PUBLIC_KEY1)) + .subjectKey(NESTED_SUBJECT_ATTRIBUTES_ONLY) + .rolesKey(NESTED_ROLES) ).backend("noop"); @ClassRule @@ -162,177 +165,177 @@ public void shouldHandleMissingNestedRolesClaim() { assertThat(roles, hasSize(0)); } } - + @Test public void shouldAuthenticateWithNestedSubjectAndNestedRoles() { // Create nested subject structure - the key should match NESTED_SUBJECT path Map attributesSub = new HashMap<>(); attributesSub.put("sub", USER_SUPERHERO); - + // Create nested roles structure Map attributes = new HashMap<>(); attributes.put("roles", ROLES_CLAIM); - + // Combine both in the claims Map nestedClaims = new HashMap<>(); nestedClaims.put("attributes_sub", attributesSub); nestedClaims.put("attributes", attributes); - + // Use the token factory with nested subject configuration Header header = tokenFactoryNestedSubjectAndRole.generateValidTokenWithCustomClaims(null, null, nestedClaims); - + try (TestRestClient client = cluster.getRestClient(header)) { HttpResponse response = client.getAuthInfo(); - + response.assertStatusCode(200); String username = response.getTextFromJsonBody(POINTER_USERNAME); assertThat(username, equalTo(USER_SUPERHERO)); - + List roles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); assertThat(roles, hasSize(2)); assertThat(roles, containsInAnyOrder("all_access", "securitymanager")); } } - + @Test public void shouldAuthenticateWithNestedSubjectAndSimpleRoles() { // Create nested subject structure Map attributesSub = new HashMap<>(); attributesSub.put("sub", USER_SUPERHERO); - + Map nestedClaims = new HashMap<>(); nestedClaims.put("attributes_sub", attributesSub); - + Header header = tokenFactoryNestedSubjectAndRole.generateValidTokenWithCustomClaims(null, null, nestedClaims); - + try (TestRestClient client = cluster.getRestClient(header)) { HttpResponse response = client.getAuthInfo(); - + response.assertStatusCode(200); String username = response.getTextFromJsonBody(POINTER_USERNAME); assertThat(username, equalTo(USER_SUPERHERO)); - + // Should have no roles since they're not in the expected nested location List roles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); assertThat(roles, hasSize(0)); } } - + // Negative test cases - + @Test public void shouldFailAuthenticationWithMissingNestedSubject() { // Create nested roles structure but missing nested subject Map attributes = new HashMap<>(); attributes.put("roles", ROLES_CLAIM); - + Map nestedClaims = new HashMap<>(); nestedClaims.put("attributes", attributes); // Missing attributes_sub structure - + Header header = tokenFactoryNestedSubjectAndRole.generateValidTokenWithCustomClaims(null, null, nestedClaims); - + try (TestRestClient client = cluster.getRestClient(header)) { HttpResponse response = client.getAuthInfo(); - + // Should fail authentication due to missing subject response.assertStatusCode(401); } } - + @Test public void shouldFailAuthenticationWithWrongNestedSubjectStructure() { // Create wrong nested subject structure Map attributesSub = new HashMap<>(); attributesSub.put("wrong_key", USER_SUPERHERO); // Wrong key, should be "sub" - + Map attributes = new HashMap<>(); attributes.put("roles", ROLES_CLAIM); - + Map nestedClaims = new HashMap<>(); nestedClaims.put("attributes_sub", attributesSub); nestedClaims.put("attributes", attributes); - + Header header = tokenFactoryNestedSubjectAndRole.generateValidTokenWithCustomClaims(null, null, nestedClaims); - + try (TestRestClient client = cluster.getRestClient(header)) { HttpResponse response = client.getAuthInfo(); - + // Should fail authentication due to wrong subject structure response.assertStatusCode(401); } } - + @Test public void shouldAuthenticateWithMissingRolesButValidSubject() { // Create nested subject structure but missing roles Map attributesSub = new HashMap<>(); attributesSub.put("sub", USER_SUPERHERO); - + Map nestedClaims = new HashMap<>(); nestedClaims.put("attributes_sub", attributesSub); // Missing roles structure - + Header header = tokenFactoryNestedSubjectAndRole.generateValidTokenWithCustomClaims(null, null, nestedClaims); - + try (TestRestClient client = cluster.getRestClient(header)) { HttpResponse response = client.getAuthInfo(); - + // Should authenticate but with no roles response.assertStatusCode(200); String username = response.getTextFromJsonBody(POINTER_USERNAME); assertThat(username, equalTo(USER_SUPERHERO)); - + List roles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); assertThat(roles, hasSize(0)); } } - + @Test public void shouldHandleWrongNestedRolesStructure() { // Create nested subject structure with wrong roles structure Map attributesSub = new HashMap<>(); attributesSub.put("sub", USER_SUPERHERO); - + Map attributes = new HashMap<>(); attributes.put("wrong_roles_key", ROLES_CLAIM); // Wrong key, should be "roles" - + Map nestedClaims = new HashMap<>(); nestedClaims.put("attributes_sub", attributesSub); nestedClaims.put("attributes", attributes); - + Header header = tokenFactoryNestedSubjectAndRole.generateValidTokenWithCustomClaims(null, null, nestedClaims); - + try (TestRestClient client = cluster.getRestClient(header)) { HttpResponse response = client.getAuthInfo(); - + // Should authenticate but with no roles due to wrong roles structure response.assertStatusCode(200); String username = response.getTextFromJsonBody(POINTER_USERNAME); assertThat(username, equalTo(USER_SUPERHERO)); - + List roles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); assertThat(roles, hasSize(0)); } } - + @Test public void shouldFailAuthenticationWithCompletelyWrongTokenStructure() { // Create completely wrong token structure Map wrongClaims = new HashMap<>(); wrongClaims.put("completely", "wrong"); wrongClaims.put("structure", "invalid"); - + Header header = tokenFactoryNestedSubjectAndRole.generateValidTokenWithCustomClaims(null, null, wrongClaims); - + try (TestRestClient client = cluster.getRestClient(header)) { HttpResponse response = client.getAuthInfo(); - + // Should fail authentication due to completely wrong structure response.assertStatusCode(401); } } - + @Test public void shouldAuthenticateWithBothSubjectAndRolesInAttributesOnly() { // Create nested structure where both subject and roles are under "attributes" @@ -341,24 +344,24 @@ public void shouldAuthenticateWithBothSubjectAndRolesInAttributesOnly() { Map attributes = new HashMap<>(); attributes.put("sub", USER_SUPERHERO); attributes.put("roles", ROLES_CLAIM); - + Map nestedClaims = new HashMap<>(); nestedClaims.put("attributes", attributes); - + // Use the token factory configured for attributes-only paths Header header = tokenFactoryAttributesOnly.generateValidTokenWithCustomClaims(null, null, nestedClaims); - + try (TestRestClient client = cluster.getRestClient(header)) { HttpResponse response = client.getAuthInfo(); - + response.assertStatusCode(200); String username = response.getTextFromJsonBody(POINTER_USERNAME); assertThat(username, equalTo(USER_SUPERHERO)); - + List roles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); assertThat(roles, hasSize(2)); assertThat(roles, containsInAnyOrder("all_access", "securitymanager")); } } - + } From cca53f4c5b4662bc08fcb751199a84989a2ba8b1 Mon Sep 17 00:00:00 2001 From: Rishav Kumar Date: Sun, 27 Jul 2025 16:14:04 +0530 Subject: [PATCH 17/20] added changelog Signed-off-by: Rishav Kumar --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b12f9c7a32..e3e91ded03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), * Optimized wildcard matching runtime performance ([#5470](https://github.com/opensearch-project/security/pull/5470)) * Optimized performance for construction of internal action privileges data structure ([#5470](https://github.com/opensearch-project/security/pull/5470)) * Restricting query optimization via star tree index for users with queries on indices with DLS/FLS/FieldMasked restrictions ([#5492](https://github.com/opensearch-project/security/pull/5492)) - +* Handle subject in nested claim for JWT auth backends ([#5467](https://github.com/opensearch-project/security/pull/5467)) + ### Bug Fixes * Fix compilation issue after change to Subject interface in core and bump to 3.2.0 ([#5423](https://github.com/opensearch-project/security/pull/5423)) From c1c49837af224d6ed8e39606eb7aed131411a0f2 Mon Sep 17 00:00:00 2001 From: Rishav Kumar Date: Sun, 27 Jul 2025 16:21:58 +0530 Subject: [PATCH 18/20] removed comments Signed-off-by: Rishav Kumar --- CHANGELOG.md | 2 +- .../security/http/JwtAuthorizationHeaderFactory.java | 1 - .../auth/http/jwt/AbstractHTTPJwtAuthenticator.java | 6 ------ 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3e91ded03..d54603bd2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), * Optimized performance for construction of internal action privileges data structure ([#5470](https://github.com/opensearch-project/security/pull/5470)) * Restricting query optimization via star tree index for users with queries on indices with DLS/FLS/FieldMasked restrictions ([#5492](https://github.com/opensearch-project/security/pull/5492)) * Handle subject in nested claim for JWT auth backends ([#5467](https://github.com/opensearch-project/security/pull/5467)) - + ### Bug Fixes * Fix compilation issue after change to Subject interface in core and bump to 3.2.0 ([#5423](https://github.com/opensearch-project/security/pull/5423)) diff --git a/src/integrationTest/java/org/opensearch/security/http/JwtAuthorizationHeaderFactory.java b/src/integrationTest/java/org/opensearch/security/http/JwtAuthorizationHeaderFactory.java index 77da262a95..4c6a35a6bf 100644 --- a/src/integrationTest/java/org/opensearch/security/http/JwtAuthorizationHeaderFactory.java +++ b/src/integrationTest/java/org/opensearch/security/http/JwtAuthorizationHeaderFactory.java @@ -118,7 +118,6 @@ private Map customClaimsMap(String username, String[] roles) { } Header generateValidTokenWithCustomClaims(String username, String[] roles, Map additionalClaims) { - // requireNonNull(username, "Username is required"); not required as username can be null requireNonNull(additionalClaims, "Custom claims are required"); Map claims = new HashMap<>(customClaimsMap(username, roles)); claims.putAll(additionalClaims); diff --git a/src/main/java/org/opensearch/security/auth/http/jwt/AbstractHTTPJwtAuthenticator.java b/src/main/java/org/opensearch/security/auth/http/jwt/AbstractHTTPJwtAuthenticator.java index 0565c005ee..e338379d44 100644 --- a/src/main/java/org/opensearch/security/auth/http/jwt/AbstractHTTPJwtAuthenticator.java +++ b/src/main/java/org/opensearch/security/auth/http/jwt/AbstractHTTPJwtAuthenticator.java @@ -187,9 +187,6 @@ protected String getJwtTokenString(SecurityRequest request) { @VisibleForTesting public String extractSubject(JWTClaimsSet claims) { String subject = claims.getSubject(); - log.warn("subject is '{}' ", subject); - log.warn("subjectKey is '{}' ", subjectKey); - log.warn("claims is '{}' ", claims); if (subjectKey != null && !subjectKey.isEmpty()) { Object subjectObject = null; @@ -206,7 +203,6 @@ public String extractSubject(JWTClaimsSet claims) { } } - log.warn("subjectObject is '{}' ", subjectObject); if (subjectObject == null) { log.warn("Failed to get subject from JWT claims, check if subject_key '{}' is correct.", subjectKey); @@ -237,7 +233,6 @@ public String[] extractRoles(JWTClaimsSet claims) { return new String[0]; } - log.warn("rolesKey is '{}' ", rolesKey); Object rolesObject = null; Map claimsMap = claims.getClaims(); for (int i = 0; i < rolesKey.size(); i++) { @@ -262,7 +257,6 @@ public String[] extractRoles(JWTClaimsSet claims) { return new String[0]; } - log.warn("rolesObject is '{}' ", rolesObject); String[] roles = String.valueOf(rolesObject).split(","); // We expect a String or Collection. If we find something else, convert to From 999b14d9dde940409cb190ee312631dff85ca0f9 Mon Sep 17 00:00:00 2001 From: Rishav Kumar Date: Sun, 27 Jul 2025 16:37:10 +0530 Subject: [PATCH 19/20] spotless Signed-off-by: Rishav Kumar --- .../security/auth/http/jwt/AbstractHTTPJwtAuthenticator.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/org/opensearch/security/auth/http/jwt/AbstractHTTPJwtAuthenticator.java b/src/main/java/org/opensearch/security/auth/http/jwt/AbstractHTTPJwtAuthenticator.java index e338379d44..c38afebdf5 100644 --- a/src/main/java/org/opensearch/security/auth/http/jwt/AbstractHTTPJwtAuthenticator.java +++ b/src/main/java/org/opensearch/security/auth/http/jwt/AbstractHTTPJwtAuthenticator.java @@ -203,7 +203,6 @@ public String extractSubject(JWTClaimsSet claims) { } } - if (subjectObject == null) { log.warn("Failed to get subject from JWT claims, check if subject_key '{}' is correct.", subjectKey); return null; From 3feeafea82d174ee169384f48be199f217a9ff68 Mon Sep 17 00:00:00 2001 From: Rishav Kumar Date: Tue, 29 Jul 2025 11:54:13 +0530 Subject: [PATCH 20/20] refactored auth header factory Signed-off-by: Rishav Kumar --- .../http/JwtAuthorizationHeaderFactory.java | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/integrationTest/java/org/opensearch/security/http/JwtAuthorizationHeaderFactory.java b/src/integrationTest/java/org/opensearch/security/http/JwtAuthorizationHeaderFactory.java index 4c6a35a6bf..38cd1992c3 100644 --- a/src/integrationTest/java/org/opensearch/security/http/JwtAuthorizationHeaderFactory.java +++ b/src/integrationTest/java/org/opensearch/security/http/JwtAuthorizationHeaderFactory.java @@ -67,27 +67,26 @@ private Map customClaimsMap(String username, String[] roles) { ImmutableMap.Builder builder = new ImmutableMap.Builder(); // Handle username claim if (StringUtils.isNoneEmpty(username)) { - if (usernameClaimName instanceof List && !((List) usernameClaimName).isEmpty()) { - // Handle nested username claim - List usernamePath = (List) usernameClaimName; - Map nestedUserMap = new HashMap<>(); - Map currentUserMap = nestedUserMap; - - // Build the nested structure for username - for (int i = 0; i < usernamePath.size() - 1; i++) { + if (usernameClaimName.size() == 1) { + // Simple case - no nesting + builder.put(usernameClaimName.get(0), username); + } else { + // Handle nested claims + Map nestedMap = new HashMap<>(); + Map currentMap = nestedMap; + + // Build the nested structure + for (int i = 0; i < usernameClaimName.size() - 1; i++) { Map nextMap = new HashMap<>(); - currentUserMap.put(usernamePath.get(i), nextMap); - currentUserMap = nextMap; + currentMap.put(usernameClaimName.get(i), nextMap); + currentMap = nextMap; } // Add the username at the deepest level - currentUserMap.put(usernamePath.get(usernamePath.size() - 1), username); + currentMap.put(usernameClaimName.get(usernameClaimName.size() - 1), username); - // Add the entire nested username structure to the builder - builder.putAll(nestedUserMap); - } else { - // Simple case - no nesting for username - builder.put(usernameClaimName.toString(), username); + // Add the entire nested structure to the builder + builder.putAll(nestedMap); } }