Skip to content

JWT Token Organization Object Embedding#328

Open
jacob6838 wants to merge 7 commits into
developfrom
jacob6838/feature/organization-id-migration
Open

JWT Token Organization Object Embedding#328
jacob6838 wants to merge 7 commits into
developfrom
jacob6838/feature/organization-id-migration

Conversation

@jacob6838
Copy link
Copy Markdown
Collaborator

@jacob6838 jacob6838 commented May 15, 2026

PR Details

Description

Previously, Keycloak was embedding organization name and role pairs into the JWT token cvmanager_data claim. Now, the organization ID and email are also included.
This update enables the Intersection API to generate postgres Organization objects from the decoded JWT token and use them in queries, reducing joins and complexity throughout. The webapp has also been updated to process the new token format (the python api does not read org permissions from the JWT).

New Token Format:

{
  ...
  "cvmanager_data": {
    "super_user": "0",
    "organizations": [
      {
        "org_id": 1,
        "org_name": "Test Org",
        "org_email": "example@gmail.com",
        "role": "admin"
      },
    }
  }
}

How Has This Been Tested?

This was tested by making manual requests to the admin endpoints through postman and

Types of changes

  • Defect fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that cause existing functionality to change)

Checklist:

  • My changes require new environment variables:
    • I have updated the docker-compose, K8s YAML, and all dependent deployment configuration files.
  • My changes require updates to the documentation:
    • I have updated the documentation accordingly.
  • My changes require updates and/or additions to the unit tests:
    • I have modified/added tests to cover my changes.
  • All existing tests pass.

@jacob6838 jacob6838 changed the title JWT Token Embedding Organization Object JWT Token Organization Object Embedding May 15, 2026
@jacob6838 jacob6838 marked this pull request as ready for review May 15, 2026 17:15
Copilot AI review requested due to automatic review settings May 15, 2026 17:15
Copy link
Copy Markdown

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.

Pull request overview

Updates the cvmanager JWT cvmanager_data.organizations claim format to embed organization identifiers and metadata (id, name, email), then refactors the Intersection API and webapp to consume the new structure (notably enabling API queries to work from decoded org objects rather than org-name joins).

Changes:

  • Webapp AuthToken typing + auth parsing updated to use org_name (and accept new org fields).
  • Intersection API permission/token plumbing refactored so getQualifiedOrgList(...) returns Organization objects and repositories accept org entities for authorization queries.
  • Keycloak custom user provider and extensive test updates to emit/validate the new token organization payload shape.

Reviewed changes

Copilot reviewed 34 out of 35 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
webapp/src/models/AuthToken.d.ts Updates JWT token typing to the new organizations payload structure (id/name/email).
webapp/src/apis/auth-api.ts Adjusts auth parsing to read org_name from the updated token.
webapp/src/apis/auth-api.test.ts Updates auth parsing tests to match the new token organization object shape.
services/intersection-api/api/src/test/java/us/dot/its/jpo/ode/api/services/UserManagementServiceTest.java Updates mocks/expectations for qualified org lists now returning Organization objects.
services/intersection-api/api/src/test/java/us/dot/its/jpo/ode/api/services/ScmsHealthServiceTest.java Updates fixtures to create orgs with email/id inputs.
services/intersection-api/api/src/test/java/us/dot/its/jpo/ode/api/services/RsuManagementServiceTest.java Updates tests for org-entity-based authorization and revised org-add/remove behaviors.
services/intersection-api/api/src/test/java/us/dot/its/jpo/ode/api/services/PermissionServiceTest.java Refactors permission tests for new repository methods and org-entity qualified lists.
services/intersection-api/api/src/test/java/us/dot/its/jpo/ode/api/services/AdminIntersectionServiceTest.java Refactors integration tests to mock permission/token org lists using embedded org objects.
services/intersection-api/api/src/test/java/us/dot/its/jpo/ode/api/models/keycloak/CvManagerAuthTokenTest.java Updates token parsing tests for org_id/org_name/org_email and org-entity return types.
services/intersection-api/api/src/test/java/us/dot/its/jpo/ode/api/fixtures/TestFixtures.java Extends org fixture creation to include id and email.
services/intersection-api/api/src/test/java/us/dot/its/jpo/ode/api/controllers/users/UserControllerTest.java Updates controller tests to use hasRoleInOrgNames transition method.
services/intersection-api/api/src/test/java/us/dot/its/jpo/ode/api/controllers/devices/RsuControllerTest.java Updates controller tests to use hasRoleInOrgNames transition method.
services/intersection-api/api/src/test/java/us/dot/its/jpo/ode/api/controllers/admin/AdminIntersectionControllerTest.java Updates controller tests to work with qualified org lists as Organization objects.
services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/services/UserManagementService.java Refactors allowed selections and org-change handling to operate on authorized Organization entities.
services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/services/RsuManagementService.java Refactors RSU org-change handling to use authorized Organization entities from the token.
services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/services/PermissionService.java Changes permission checks to flow Organization entities through repository queries; adds transitional hasRoleInOrgNames.
services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/services/AdminIntersectionService.java Adjusts allowed selections to return org names while using org entities for RSU lookups.
services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/repositories/UserRepository.java Refactors org-scoped user existence checks to use organization entities.
services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/repositories/SnmpCredentialRepository.java Refactors credential-org checks to use organization entities.
services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/repositories/RsuRepository.java Refactors RSU-org existence and “allowed RSU IPs” query to accept organization entities.
services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/repositories/RsuCredentialRepository.java Refactors credential-org checks to use organization entities.
services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/repositories/OrganizationRepository.java Removes org-name-only helper query in favor of entity retrieval.
services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/repositories/IntersectionRepository.java Replaces explicit “exists by id/org names” query with derived method naming.
services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/models/postgres/tables/Organization.java Adds Hibernate-safe equals/hashCode based on id.
services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/models/keycloak/CvManagerAuthToken.java Updates token parsing to build Organization objects from JWT org entries; returns org entities for qualified lists.
services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/controllers/users/UserController.java Switches org qualification check to transitional hasRoleInOrgNames.
services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/controllers/devices/RsuController.java Switches org qualification check to transitional hasRoleInOrgNames.
services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/controllers/admin/AdminIntersectionController.java Updates operator-qualified org enforcement to derive name sets from org entities.
resources/keycloak/realm.json Updates realm export content (now includes federated users/credentials structure).
resources/keycloak/custom-user-provider/src/test/java/com/cvmanager/auth/provider/user/pojos/UserObjectTest.java Updates user-provider tests to parse/emit new org JSON payload (id/name/email/role).
resources/keycloak/custom-user-provider/src/test/java/com/cvmanager/auth/provider/user/pojos/OrganizationObjectTest.java Updates org object parsing/serialization tests for new org payload fields.
resources/keycloak/custom-user-provider/src/test/java/com/cvmanager/auth/provider/user/CustomUserStorageProviderTest.java Updates SQL expectations and org JSON aggregation for id/name/email.
resources/keycloak/custom-user-provider/src/test/java/com/cvmanager/auth/provider/mapper/CustomProtocolMapperTest.java Updates protocol mapper test data to the new org JSON structure.
resources/keycloak/custom-user-provider/src/main/java/com/cvmanager/auth/provider/user/pojos/OrganizationObject.java Updates org POJO serialization/deserialization to org_id/org_name/org_email.
resources/keycloak/custom-user-provider/src/main/java/com/cvmanager/auth/provider/user/CustomUserStorageProvider.java Updates SQL JSON aggregation to embed org_id/org_name/org_email into token claim.
Comments suppressed due to low confidence (4)

services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/services/UserManagementService.java:209

  • organizations_to_modify updates roles without verifying that the target organization is within authorizedOrgs. A non-superuser with ADMIN in one org could modify a user’s role in another org they aren’t authorized for (as long as the relationship exists). Add the same authorization filtering/check used for removals before allowing modifications, and return a 403-style error when unauthorized.
        if (patch.getOrganizationsToModify() != null && !patch.getOrganizationsToModify().isEmpty()) {
            for (UserOrganizationDto org : patch.getOrganizationsToModify()) {
                userOrganizationRepository.findByUserAndOrganization_Name(
                        user,
                        org.getOrganization()).ifPresent(userOrg -> {
                            Role role = roleRepository.findByNameIgnoreCase(org.getRole())
                                    .orElseThrow(
                                            () -> new IllegalArgumentException(
                                                    "Role not found: " + org.getRole()));
                            userOrg.setRole(role);
                            userOrganizationRepository.save(userOrg);
                        });
            }

services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/services/RsuManagementService.java:262

  • When an org is missing or unauthorized, this code throws IllegalArgumentException, which the API maps to HTTP 400. Previously these paths returned 403 (unauthorized) or 400/404 (not found). Conflating “not found” and “not authorized” into a 400 can break clients and makes error semantics misleading; consider throwing AccessDeniedException for unauthorized and a not-found/ResponseStatusException for missing organizations.
            for (String orgName : patch.getOrganizationsToAdd()) {
                Organization org = authorizedOrgs.stream()
                        .filter(o -> o.getName().equals(orgName))
                        .findFirst()
                        .orElseThrow(() -> new IllegalArgumentException(
                                "Organization not found or user not authorized for: " + orgName));

resources/keycloak/realm.json:567

  • This realm export now includes federatedUsers entries with password credential blobs (secretData/credentialData) and environment-specific IDs/timestamps. Even if hashed, committing credential material makes the repo harder to audit and can cause brittle local-dev imports when IDs differ. Consider stripping federated user credential data from realm.json (keep only required realm/client configuration) or regenerating a minimal realm export intended for version control.
    "federatedUsers": [
      {
        "id": "f:60b8a4e7-d427-4316-9ec0-cb8a6eeb34bd:fc3d8729-8526-4aaa-805b-d64bf3b93860",
        "attributes": {
          "fedNotBefore": ["0"]
        },
        "credentials": [
          {
            "id": "64f66e4b-868d-4805-835d-dba761c6e32b",
            "type": "password",
            "userLabel": "My password",
            "createdDate": 1746774661287,
            "secretData": "{\"value\":\"KXOpj1vu3jv+nf8Qvs7WJbd1NgZcOPg3ilp99kpW43U=\",\"salt\":\"+mN1WmKPXqvvdXKlhZF0LQ==\",\"additionalParameters\":{}}",
            "credentialData": "{\"hashIterations\":5,\"algorithm\":\"argon2\",\"additionalParameters\":{\"hashLength\":[\"32\"],\"memory\":[\"7168\"],\"type\":[\"id\"],\"version\":[\"1.3\"],\"parallelism\":[\"1\"]}}"
          }
        ],
        "notBefore": 0,
        "groups": []
      },
      {
        "id": "f:60b8a4e7-d427-4316-9ec0-cb8a6eeb34bd:9f0db92b-b703-40ae-a7e4-d9d61fd28f10",
        "attributes": {
          "ENABLED": ["true"]
        },
        "credentials": [
          {
            "id": "91837b06-6983-4602-821e-66410b8480b1",
            "type": "password",
            "userLabel": "My password",
            "createdDate": 1778705478359,
            "secretData": "{\"value\":\"5l7SNdx83IUexqSWbrocbxjCCh56/kYDTOTe22JQtJQ=\",\"salt\":\"dJ+UD5QKi7PrGmeO9L6Gew==\",\"additionalParameters\":{}}",
            "credentialData": "{\"hashIterations\":5,\"algorithm\":\"argon2\",\"additionalParameters\":{\"hashLength\":[\"32\"],\"memory\":[\"7168\"],\"type\":[\"id\"],\"version\":[\"1.3\"],\"parallelism\":[\"1\"]}}"
          }
        ],
        "notBefore": 0,
        "groups": []
      }

services/intersection-api/api/src/main/java/us/dot/its/jpo/ode/api/services/UserManagementService.java:165

  • Unauthorized organization adds now throw IllegalArgumentException (mapped to HTTP 400) rather than an authorization exception (403). If the user is authenticated but not allowed to add a user to the requested org, the response should be a forbidden-style error (e.g., AccessDeniedException) to preserve correct auth semantics and avoid breaking API clients.
                Organization organization = authorizedOrgs.stream()
                        .filter(o -> o.getName().equals(org.getOrganization()))
                        .findFirst()
                        .orElseThrow(() -> new IllegalArgumentException(
                                "Organization not found or user not authorized for: " + org.getOrganization()));


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

jacob6838 and others added 2 commits May 15, 2026 17:51
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants