EUREKA-755 PoC - Are lightweight access tokens a viable solution to the "too many roles" problem?

EUREKA-755 PoC - Are lightweight access tokens a viable solution to the "too many roles" problem?

Overview

Objective:
Address the “too many roles” problem by implementing lightweight access tokens in Keycloak, and outline the necessary changes for adoption and migration.

Background:
Access token size in Keycloak increases as more roles are assigned to users. Large tokens can cause request failures at various infrastructure layers (e.g., load balancers, reverse proxies) due to header size limits.

Problem Statement:
Configure Keycloak and supporting modules to issue lightweight tokens, and enable their usage throughout the entitlement and authorization workflow for both regular and system users.

Scope and Tasks

  • Provide detailed guidance for enabling lightweight access tokens in Keycloak.

  • List all affected classes in the entitlement manager module that require updates to support lightweight tokens during the entitlement process.

  • Confirm lightweight tokens operate as expected for permission checks.

  • Recommend solutions and migration strategies for integrating lightweight tokens into existing clusters.

Approach & Solution

  • Enabling lightweight access tokens for clients in Keycloak reduces token size by excluding role lists from tokens by default. Existing authorization endpoints and sidecar logic remain unchanged—no additional sidecar modifications are required.

    Screenshot 2025-08-11 at 18.03.31.png
  • To ensure proper authorization, configure protocol mappers to explicitly include the “sub” claim (subject) and user ID in lightweight tokens.

    Screenshot 2025-08-11 at 18.38.44.png
Screenshot 2025-08-11 at 18.38.09.png
  • For permission policies, set fetchRoles = true in policy configurations, so Keycloak evaluates user roles from its database, not from the token payload.

    Screenshot 2025-08-11 at 18.43.04.png

Token Examples

Below is a comparison between a regular access token and a lightweight access token issued by Keycloak after enabling the use.lightweight.access.token setting.

Regular Access Token

json { "exp": 1754573870, "iat": 1754573570, "jti": "f418abef-972f-4a72-9dbf-d82d3379c926", "iss": "http://keycloak:8080/realms/diku1", "aud": "account", "sub": "viktor", "typ": "Bearer", "azp": "diku1-login-app", "realm_access": { "roles": [ "offline_access", "default-roles-diku1", "uma_authorization", "System" ] }, "resource_access": { "account": { "roles": [ "manage-account", "manage-account-links", "view-profile" ] } }, "scope": "email profile", "preferred_username": "viktor", "email": "viktor@mail.com" }

Lightweight Access Token

json { "exp": 1754923204, "iat": 1754922904, "jti": "1f3349f7-a13f-4271-b29b-50cd280400bf", "iss": "http://keycloak:8080/realms/diku1", "sub": "35f02c2f-90ef-4801-ac3f-3185a076b570", "typ": "Bearer", "azp": "diku1-login-app", "scope": "email profile" }

Key Differences

  • Size reduction: Roles and other claims are omitted in the lightweight token.

  • Role evaluation moved server-side: When fetchRoles=true is enabled in policies, Keycloak fetches roles from its database instead of relying on token payload.

  • sub claim retained: By adding the “Subject (sub)” protocol mapper with "Add to lightweight access token", the subject identifier is preserved for downstream authorization checks.

Step-by-Step Implementation

  1. Enable Lightweight Tokens MGR-TENANTS (org.folio.tm.integration.keycloak.service.client.AbstractKeycloakClientServic):
    Set the client attribute use.lightweight.access.token = true for all relevant Keycloak clients.

    protected ClientRepresentation setupKeycloakClient(String realm) { var clientId = getClientId(realm); Assert.notNull(clientId, "client id must not be null"); log.info("Generating client representation: clientId = {}, realm = {}", clientId, realm); var clientRepresentation = new ClientRepresentation(); clientRepresentation.setName(getName(realm)); clientRepresentation.setClientId(clientId); clientRepresentation.setEnabled(true); clientRepresentation.setDirectAccessGrantsEnabled(true); clientRepresentation.setFrontchannelLogout(true); // Enable lightweight access tokens Map<String, String> attributes = clientRepresentation.getAttributes(); if (attributes == null) { attributes = new HashMap<>(); } attributes.put("use.lightweight.access.token", "true"); clientRepresentation.setAttributes(attributes); applyIfNotNull(getClientDescription(), clientRepresentation::setDescription); applyIfNotNull(getClientSecret(realm, clientId), clientRepresentation::setSecret); applyIfNotNull(getClientAuthenticatorType(), clientRepresentation::setClientAuthenticatorType); applyIfNotNull(isServiceAccountEnabled(), clientRepresentation::setServiceAccountsEnabled); applyIfNotNull(isAuthorizationServicesEnabled(), clientRepresentation::setAuthorizationServicesEnabled); applyIfNotNull(getWebOrigins(), clientRepresentation::setWebOrigins); applyIfNotNull(getRedirectUris(), clientRepresentation::setRedirectUris); applyIfNotNull(getAttributes(), clientRepresentation::setAttributes); applyIfNotNull(getProtocolMappers(), clientRepresentation::setProtocolMappers); applyIfNotNull(getAuthorizationSettings(), clientRepresentation::setAuthorizationSettings); var realmResource = keycloak.realm(realm); try (var response = realmResource.clients().create(clientRepresentation)) { processKeycloakResponse(clientRepresentation, response); clientRepresentation.setId(getKeycloakClientId(realm, response, clientId)); } catch (WebApplicationException exception) { throw new KeycloakException("Failed to create a Keycloak client: " + clientId, exception); } return clientRepresentation; }
  2. Update Protocol Mappers MGR-TENANTS:

    • Add the “Subject (sub)” mapper for all client scopes ImpersonationClientService, LoginClientService, and PasswordResetClientService, enabling “Add to lightweight access token.”

      // Create the Subject (sub) protocol mapper ProtocolMapperRepresentation subjectMapper = new ProtocolMapperRepresentation(); subjectMapper.setName("Subject (sub)"); subjectMapper.setProtocol("openid-connect"); subjectMapper.setProtocolMapper("oidc-usersession-sub-mapper"); Map<String, String> config = new HashMap<>(); config.put("id.token.claim", "true"); config.put("access.token.claim", "true"); config.put("userinfo.token.claim", "true"); config.put("add.to.lightweight.access.token", "true"); // <-- THIS enables lightweight token inclusion subjectMapper.setConfig(config);
    • Ensure the user ID mapper is similarly configured to support lightweight tokens.

  3. Configure Role Policies:

    • In LoginClientService, set fetchRoles = true alongside role definitions when building role policies.

      private PolicyRepresentation buildRolePolicy(String name, String roleName) { var policyRepresentation = new PolicyRepresentation(); policyRepresentation.setType("role"); policyRepresentation.setName(name); policyRepresentation.setLogic(POSITIVE); policyRepresentation.setDecisionStrategy(UNANIMOUS); var roleDefinitions = List.of(new RoleDefinition(roleName, false)); Map<String, String> roles = Map.of("roles", jsonHelper.asJsonString(roleDefinitions), "fetchRoles", "true"); policyRepresentation.setConfig(roles); return policyRepresentation; }
    • Update policy mappings in KeycloakPolicyMapper so fetchRoles is set for both role-based and user-based policy configurations (code location).

      private Map<String, String> getUserPolicyConfig(List<String> userIds) { return userIds != null ? Map.of("users", jsonHelper.asJsonStringSafe(userIds), "fetchRoles", "true") : emptyMap(); } private Map<String, String> getRolePolicyConfig(RolePolicy rolePolicy) { return rolePolicy != null ? Map.of("roles", jsonHelper.asJsonStringSafe(rolePolicy.getRoles()), "fetchRoles", "true") : emptyMap(); }
  4. Migration for Existing Clusters

    • Backward compatibility: The solution is fully backward-compatible; existing tenants will continue to work without any additional changes.

    • New tenants: Newly created tenants will automatically use lightweight tokens.

    • Updating existing tenants:

      • When a tenant is updated, the existing clients remain unchanged and will continue using the full token.

      • To migrate them, we need a small script or command-line application to iterate through all realms and enable the required settings for both clients and policies.

    • Purge scenario: If a tenant is uninstalled with purge=false, it will still use the default token without any issues. To fully migrate such tenants to lightweight tokens, the same external migration script/application should be run.

    • No migration required: In all other scenarios, no migration is necessary.

Migration Application Concept

We can develop a small command-line application using the Keycloak Java SDK.

  • Function: Loop through all realms in Keycloak and enable lightweight tokens for the required clients and update policies

  • Configuration: The application would use a properties file to specify which clients need to be updated.

  • Execution: Running this tool will apply the configuration to all relevant realms without manual intervention.

Validation

  • Lightweight token configuration was verified:

    • After enabling, role information is excluded from the token payload.

    • The “sub” claim is added as required for entitlement checks.

    • Setting fetchRoles = true ensures Keycloak fetches the latest role assignments from its database during policy evaluation, rather than using static token claims.

  • No changes needed in sidecar authorization code, as endpoints and permission model remain compatible.

Conclusion

By enabling lightweight access tokens and configuring protocol mappers and policies as described, Keycloak can support users with large numbers of roles without exceeding token size limits or requiring changes to downstream authorization logic.

Open Questions and Further Reading

  • Migration paths for existing clusters how the script will loks like

  • Impact of caching and Keycloak load considerations.