|  | 
|  | 1 | +/* | 
|  | 2 | +* Copyright OpenSearch Contributors | 
|  | 3 | +* SPDX-License-Identifier: Apache-2.0 | 
|  | 4 | +* | 
|  | 5 | +* The OpenSearch Contributors require contributions made to | 
|  | 6 | +* this file be licensed under the Apache-2.0 license or a | 
|  | 7 | +* compatible open source license. | 
|  | 8 | +* | 
|  | 9 | +*/ | 
|  | 10 | +package org.opensearch.security.http; | 
|  | 11 | + | 
|  | 12 | +import java.security.KeyPair; | 
|  | 13 | +import java.util.Arrays; | 
|  | 14 | +import java.util.Base64; | 
|  | 15 | +import java.util.HashMap; | 
|  | 16 | +import java.util.List; | 
|  | 17 | +import java.util.Map; | 
|  | 18 | + | 
|  | 19 | +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; | 
|  | 20 | +import org.apache.hc.core5.http.Header; | 
|  | 21 | +import org.junit.ClassRule; | 
|  | 22 | +import org.junit.Rule; | 
|  | 23 | +import org.junit.Test; | 
|  | 24 | +import org.junit.runner.RunWith; | 
|  | 25 | + | 
|  | 26 | +import org.opensearch.test.framework.JwtConfigBuilder; | 
|  | 27 | +import org.opensearch.test.framework.TestSecurityConfig; | 
|  | 28 | +import org.opensearch.test.framework.cluster.ClusterManager; | 
|  | 29 | +import org.opensearch.test.framework.cluster.LocalCluster; | 
|  | 30 | +import org.opensearch.test.framework.cluster.TestRestClient; | 
|  | 31 | +import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; | 
|  | 32 | +import org.opensearch.test.framework.log.LogsRule; | 
|  | 33 | + | 
|  | 34 | +import io.jsonwebtoken.SignatureAlgorithm; | 
|  | 35 | +import io.jsonwebtoken.security.Keys; | 
|  | 36 | + | 
|  | 37 | +import static java.nio.charset.StandardCharsets.US_ASCII; | 
|  | 38 | +import static org.hamcrest.MatcherAssert.assertThat; | 
|  | 39 | +import static org.hamcrest.Matchers.containsInAnyOrder; | 
|  | 40 | +import static org.hamcrest.Matchers.equalTo; | 
|  | 41 | +import static org.hamcrest.Matchers.hasSize; | 
|  | 42 | +import static org.opensearch.security.http.JwtAuthenticationTests.POINTER_BACKEND_ROLES; | 
|  | 43 | +import static org.opensearch.security.http.JwtAuthenticationTests.POINTER_USERNAME; | 
|  | 44 | +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.BASIC_AUTH_DOMAIN_ORDER; | 
|  | 45 | + | 
|  | 46 | +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) | 
|  | 47 | +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) | 
|  | 48 | +public class JwtAuthenticationNestedClaimsTests { | 
|  | 49 | + | 
|  | 50 | +    public static final String CLAIM_USERNAME = "preferred-username"; | 
|  | 51 | +    public static final List<String> CLAIM_ROLES = List.of("attributes", "roles"); | 
|  | 52 | + | 
|  | 53 | +    public static final String USER_SUPERHERO = "superhero"; | 
|  | 54 | +    private static final KeyPair KEY_PAIR1 = Keys.keyPairFor(SignatureAlgorithm.RS256); | 
|  | 55 | +    private static final String PUBLIC_KEY1 = new String(Base64.getEncoder().encode(KEY_PAIR1.getPublic().getEncoded()), US_ASCII); | 
|  | 56 | +    private static final String JWT_AUTH_HEADER = "jwt-auth"; | 
|  | 57 | + | 
|  | 58 | +    private static final JwtAuthorizationHeaderFactory tokenFactory1 = new JwtAuthorizationHeaderFactory( | 
|  | 59 | +        KEY_PAIR1.getPrivate(), | 
|  | 60 | +        CLAIM_USERNAME, | 
|  | 61 | +        CLAIM_ROLES, | 
|  | 62 | +        JWT_AUTH_HEADER | 
|  | 63 | +    ); | 
|  | 64 | +    public static final TestSecurityConfig.AuthcDomain JWT_AUTH_DOMAIN = new TestSecurityConfig.AuthcDomain( | 
|  | 65 | +        "jwt", | 
|  | 66 | +        BASIC_AUTH_DOMAIN_ORDER - 1 | 
|  | 67 | +    ).jwtHttpAuthenticator( | 
|  | 68 | +        new JwtConfigBuilder().jwtHeader(JWT_AUTH_HEADER).signingKey(List.of(PUBLIC_KEY1)).subjectKey(CLAIM_USERNAME).rolesKey(CLAIM_ROLES) | 
|  | 69 | +    ).backend("noop"); | 
|  | 70 | + | 
|  | 71 | +    @ClassRule | 
|  | 72 | +    public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) | 
|  | 73 | +        .anonymousAuth(false) | 
|  | 74 | +        .authc(JWT_AUTH_DOMAIN) | 
|  | 75 | +        .build(); | 
|  | 76 | + | 
|  | 77 | +    @Rule | 
|  | 78 | +    public LogsRule logsRule = new LogsRule("org.opensearch.security.auth.http.jwt.HTTPJwtAuthenticator"); | 
|  | 79 | + | 
|  | 80 | +    // TODO write tests for scenarios where roles are in nested claim. i.e. rolesKey: ['attributes', 'roles'] | 
|  | 81 | +    @Test | 
|  | 82 | +    public void shouldAuthenticateWithNestedRolesClaim() { | 
|  | 83 | +        // Create nested claims structure | 
|  | 84 | +        Map<String, Object> attributes = new HashMap<>(); | 
|  | 85 | +        List<String> rolesClaim = Arrays.asList("all_access", "securitymanager"); | 
|  | 86 | +        attributes.put("roles", rolesClaim); | 
|  | 87 | + | 
|  | 88 | +        Map<String, Object> nestedClaims = new HashMap<>(); | 
|  | 89 | +        nestedClaims.put("attributes", attributes); | 
|  | 90 | + | 
|  | 91 | +        // Generate token with nested claims | 
|  | 92 | +        Header header = tokenFactory1.generateValidTokenWithCustomClaims(USER_SUPERHERO, null, nestedClaims); | 
|  | 93 | + | 
|  | 94 | +        try (TestRestClient client = cluster.getRestClient(header)) { | 
|  | 95 | +            HttpResponse response = client.getAuthInfo(); | 
|  | 96 | + | 
|  | 97 | +            response.assertStatusCode(200); | 
|  | 98 | +            String username = response.getTextFromJsonBody(POINTER_USERNAME); | 
|  | 99 | +            assertThat(username, equalTo(USER_SUPERHERO)); | 
|  | 100 | +            List<String> roles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); | 
|  | 101 | +            assertThat(roles, hasSize(2)); | 
|  | 102 | +            assertThat(roles, containsInAnyOrder("all_access", "securitymanager")); | 
|  | 103 | +        } | 
|  | 104 | +    } | 
|  | 105 | + | 
|  | 106 | +    @Test | 
|  | 107 | +    public void shouldHandleMissingNestedRolesClaim() { | 
|  | 108 | +        // Create invalid nested claims structure | 
|  | 109 | +        Map<String, Object> attributes = new HashMap<>(); | 
|  | 110 | +        attributes.put("wrong", "missing"); // Invalid format - should be a list | 
|  | 111 | + | 
|  | 112 | +        Map<String, Object> nestedClaims = new HashMap<>(); | 
|  | 113 | +        nestedClaims.put("attributes", attributes); | 
|  | 114 | + | 
|  | 115 | +        Header header = tokenFactory1.generateValidTokenWithCustomClaims(USER_SUPERHERO, null, nestedClaims); | 
|  | 116 | + | 
|  | 117 | +        try (TestRestClient client = cluster.getRestClient(header)) { | 
|  | 118 | +            HttpResponse response = client.getAuthInfo(); | 
|  | 119 | + | 
|  | 120 | +            response.assertStatusCode(200); | 
|  | 121 | +            String username = response.getTextFromJsonBody(POINTER_USERNAME); | 
|  | 122 | +            assertThat(username, equalTo(USER_SUPERHERO)); | 
|  | 123 | +            List<String> roles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); | 
|  | 124 | +            assertThat(roles, hasSize(0)); | 
|  | 125 | +        } | 
|  | 126 | +    } | 
|  | 127 | +} | 
0 commit comments