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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ 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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -47,31 +46,74 @@
@ThreadLeakScope(ThreadLeakScope.Scope.NONE)
public class JwtAuthenticationNestedClaimsTests {

public static final String CLAIM_USERNAME = "preferred-username";
public static final List<String> CLAIM_ROLES = List.of("attributes", "roles");
public static final List<String> USERNAME_CLAIM = List.of("preferred-username");
public static final List<String> NESTED_ROLES = List.of("attributes", "roles");
public static final List<String> NESTED_SUBJECT = List.of("attributes_sub", "sub");
public static final List<String> NESTED_SUBJECT_ATTRIBUTES_ONLY = List.of("attributes", "sub");
public static final List<String> ROLES_CLAIM = List.of("all_access", "securitymanager");

public static final String USER_SUPERHERO = "superhero";
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 String JWT_AUTH_HEADER = "jwt-auth";

// Token factory for regular subject + nested roles
private static final JwtAuthorizationHeaderFactory tokenFactory1 = new JwtAuthorizationHeaderFactory(
KEY_PAIR1.getPrivate(),
CLAIM_USERNAME,
CLAIM_ROLES,
USERNAME_CLAIM,
NESTED_ROLES,
JWT_AUTH_HEADER
);

// 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(CLAIM_USERNAME).rolesKey(CLAIM_ROLES)
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
Expand All @@ -82,8 +124,7 @@ public class JwtAuthenticationNestedClaimsTests {
public void shouldAuthenticateWithNestedRolesClaim() {
// Create nested claims structure
Map<String, Object> attributes = new HashMap<>();
List<String> rolesClaim = Arrays.asList("all_access", "securitymanager");
attributes.put("roles", rolesClaim);
attributes.put("roles", ROLES_CLAIM);

Map<String, Object> nestedClaims = new HashMap<>();
nestedClaims.put("attributes", attributes);
Expand Down Expand Up @@ -124,4 +165,203 @@ public void shouldHandleMissingNestedRolesClaim() {
assertThat(roles, hasSize(0));
}
}

@Test
public void shouldAuthenticateWithNestedSubjectAndNestedRoles() {
// Create nested subject structure - the key should match NESTED_SUBJECT path
Map<String, Object> attributesSub = new HashMap<>();
attributesSub.put("sub", USER_SUPERHERO);

// Create nested roles structure
Map<String, Object> attributes = new HashMap<>();
attributes.put("roles", ROLES_CLAIM);

// Combine both in the claims
Map<String, Object> 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<String> 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<String, Object> attributesSub = new HashMap<>();
attributesSub.put("sub", USER_SUPERHERO);

Map<String, Object> 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<String> 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<String, Object> attributes = new HashMap<>();
attributes.put("roles", ROLES_CLAIM);

Map<String, Object> 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<String, Object> attributesSub = new HashMap<>();
attributesSub.put("wrong_key", USER_SUPERHERO); // Wrong key, should be "sub"

Map<String, Object> attributes = new HashMap<>();
attributes.put("roles", ROLES_CLAIM);

Map<String, Object> 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<String, Object> attributesSub = new HashMap<>();
attributesSub.put("sub", USER_SUPERHERO);

Map<String, Object> 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<String> roles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES);
assertThat(roles, hasSize(0));
}
}

@Test
public void shouldHandleWrongNestedRolesStructure() {
// Create nested subject structure with wrong roles structure
Map<String, Object> attributesSub = new HashMap<>();
attributesSub.put("sub", USER_SUPERHERO);

Map<String, Object> attributes = new HashMap<>();
attributes.put("wrong_roles_key", ROLES_CLAIM); // Wrong key, should be "roles"

Map<String, Object> 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<String> roles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES);
assertThat(roles, hasSize(0));
}
}

@Test
public void shouldFailAuthenticationWithCompletelyWrongTokenStructure() {
// Create completely wrong token structure
Map<String, Object> 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<String, Object> attributes = new HashMap<>();
attributes.put("sub", USER_SUPERHERO);
attributes.put("roles", ROLES_CLAIM);

Map<String, Object> 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<String> roles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES);
assertThat(roles, hasSize(2));
assertThat(roles, containsInAnyOrder("all_access", "securitymanager"));
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
@ThreadLeakScope(ThreadLeakScope.Scope.NONE)
public class JwtAuthenticationTests {

public static final String CLAIM_USERNAME = "preferred-username";
public static final List<String> CLAIM_USERNAME = List.of("preferred-username");
public static final List<String> CLAIM_ROLES = List.of("backend-user-roles");

public static final String USER_SUPERHERO = "superhero";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
@ThreadLeakScope(ThreadLeakScope.Scope.NONE)
public class JwtAuthenticationWithUrlParamTests {

public static final String CLAIM_USERNAME = "preferred-username";
public static final List<String> CLAIM_USERNAME = List.of("preferred-username");
public static final List<String> CLAIM_ROLES = List.of("backend-user-roles");
public static final String POINTER_USERNAME = "/user_name";

Expand Down
Loading
Loading