Skip to content

Extract groups from OAuth JWT token #11430

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

pnartowski
Copy link
Contributor

@pnartowski pnartowski commented Feb 28, 2025

Extract array of groups from string

Fix #11429

Describe changes proposed in this pull request:

  • Register Java Time Module in Object Mapper to prevent exception on serializing JWT claims ( ID token contains few dates )
  • for group properties if string instead of array is found deserialize this string to extract array from it

Checks

@pnartowski
Copy link
Contributor Author

@haynescd may I ask you to help progress with this PR ?

@pnartowski
Copy link
Contributor Author

@inodb could you help progress with this PR ?

@Gluks54
Copy link

Gluks54 commented Mar 31, 2025

@inodb please could you help progress with this PR ?

@kozaka4
Copy link

kozaka4 commented Mar 31, 2025

@inodb Is there any chance we can proceed with this PR?

@kozaka4
Copy link

kozaka4 commented Apr 8, 2025

Hello cbio team,
Could someone help us with this PR? It's blocked because ci/circleci: end_to_end_tests_localdb is failed?

@KingAlex1985
Copy link
Contributor

@pnartowski and @kozaka4

I have created a Unit-Test regarding your change / check "rolesCursor.isTextual()" at class "ClaimRoleExtractorUtil.java":
Maybe you can check this. If this is correct so far you can maybe als add it. So that others can better understand your change / new check.

package org.cbioportal.application.security.util;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import static org.junit.jupiter.api.Assertions.*;

@RunWith(MockitoJUnitRunner.class)
class ClaimRoleExtractorUtilTest {
    
    @Test
    void extractClientRolesTest01() {
        String claims = """
        {
            "roles": ["role1", "role2"]
        }
        """;
        String jwtRolesPath = "roles";
        var result = ClaimRoleExtractorUtil.extractClientRoles(claims, jwtRolesPath);
        assertNotNull(result);
        assertEquals(2, result.size());
        assertTrue(result.contains("role1"));
        assertTrue(result.contains("role2"));
    }
    
    @Test
    void extractClientRolesTest01b() {
        ObjectMapper objectMapper = new ObjectMapper();
        String roleString = """
            ["role1","role2","role3"]
        """;
        ObjectNode root = objectMapper.createObjectNode();
        root.put("roles", roleString);
        String jwtRolesPath = "roles";
        
        var result = ClaimRoleExtractorUtil.extractClientRoles(root, jwtRolesPath);
        
        assertNotNull(result);
        assertEquals(3, result.size());
        assertTrue(result.contains("role1"));
        assertTrue(result.contains("role2"));
        assertTrue(result.contains("role3"));
    }

    @Test
    void extractClientRolesTest02() {
        String claims = """
        {
            "groups": ["GROUP_A", "GROUP_B", "GROUP_C"]
        }
        """;
        String jwtRolesPath = "groups";
        var result = ClaimRoleExtractorUtil.extractClientRoles(claims, jwtRolesPath);
        assertNotNull(result);
        assertEquals(3, result.size());
        assertTrue(result.contains("GROUP_A"));
        assertTrue(result.contains("GROUP_B"));
        assertTrue(result.contains("GROUP_C"));
    }
    
    @Test
    void extractClientRolesTest02b() {
        ObjectMapper objectMapper = new ObjectMapper();
        String groupString = """
            ["GROUP_A","GROUP_B","GROUP_C"]
        """;
        ObjectNode root = objectMapper.createObjectNode();
        root.put("groups", groupString);
        String jwtRolesPath = "groups";

        var result = ClaimRoleExtractorUtil.extractClientRoles(root, jwtRolesPath);

        assertNotNull(result);
        assertEquals(3, result.size());
        assertTrue(result.contains("GROUP_A"));
        assertTrue(result.contains("GROUP_B"));
        assertTrue(result.contains("GROUP_C"));
    }

    @Test
    void extractClientRolesTest03() {
        String claims = """
            {
                "iss": "https://auth.example.com/",
                "sub": "user123",
                "aud": "my-app-client-id",
                "exp": 1712570400,
                "iat": 1712566800,
                "roles": ["admin", "editor"]
            }
            """;

        JsonNode claimsNode;
        
        try {
            claimsNode = new ObjectMapper().readTree(claims);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }

        if (claimsNode instanceof ObjectNode objectNode) {
            String groupString = """
                ["GROUP_A","GROUP_B","GROUP_C"]
            """;
            objectNode.put("groups", groupString);
        } else {
            throw new IllegalStateException("claimsNode is not an ObjectNode");
        }
        
        String jwtRolesPath = "groups";
        var result = ClaimRoleExtractorUtil.extractClientRoles(claimsNode, jwtRolesPath);
        assertNotNull(result);
        assertEquals(3, result.size());
        assertTrue(result.contains("GROUP_A"));
        assertTrue(result.contains("GROUP_B"));
        assertTrue(result.contains("GROUP_C"));
    }
}

@inodb inodb force-pushed the bugfix/11429_oauth_groups_extraction branch from 2d908d9 to df101f7 Compare April 10, 2025 21:27
@kozaka4
Copy link

kozaka4 commented Apr 11, 2025

I’d like to kindly ask for one more review and if everything looks good a merge. Thanks in advance!
@zainasir @dippindots @alisman 🙏

@KingAlex1985
Copy link
Contributor

I will also mention here @inodb
See the previous comment from @kozaka4

@inodb inodb requested review from Copilot and haynescd April 11, 2025 10:37
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

Comments suppressed due to low confidence (2)

src/main/java/org/cbioportal/application/security/util/ClaimRoleExtractorUtil.java:22

  • Instead of using a new ObjectMapper in the subsequent readTree call, reuse the configured 'objectMapper' to ensure that the JavaTimeModule remains registered and to avoid potential inconsistencies.
String jsonString = objectMapper.writeValueAsString(claims);

src/main/java/org/cbioportal/application/security/util/ClaimRoleExtractorUtil.java:56

  • Consider reusing the previously configured ObjectMapper for parsing the textual JSON instead of creating a new instance, ensuring consistent module registration and behavior.
if (rolesCursor.isTextual()) { rolesCursor = new ObjectMapper().readTree(rolesCursor.asText()); }

@inodb inodb requested review from sheridancbio and hweej and removed request for haynescd April 11, 2025 10:39
@pnartowski
Copy link
Contributor Author

pnartowski commented Apr 11, 2025

We might consider moving objectMapper to class field. As methods which use it are static the field needs to be static to.
Module registration is such case should be done in static block.
It isn't brilliant solution but this way we will avoid recreation each time of ObjectMapper.

private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

static { OBJECT_MAPPER.registerModule(new JavaTimeModule()); }

What do you think ?

@kozaka4
Copy link

kozaka4 commented Apr 15, 2025

@sheridancbio and @hweej we need your help, pls

@kozaka4
Copy link

kozaka4 commented Apr 16, 2025

@haynescd merge:)?

@inodb inodb added the security label Apr 16, 2025
@inodb inodb merged commit 356351e into cBioPortal:master Apr 16, 2025
11 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[BUG] Groups extraction from OAuth2 token
6 participants