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.
To ensure proper authorization, configure protocol mappers to explicitly include the “sub” claim (subject) and user ID in lightweight tokens.
For permission policies, set
fetchRoles = truein policy configurations, so Keycloak evaluates user roles from its database, not from the token payload.
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=trueis enabled in policies, Keycloak fetches roles from its database instead of relying on token payload.subclaim 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
Enable Lightweight Tokens MGR-TENANTS (
org.folio.tm.integration.keycloak.service.client.AbstractKeycloakClientServic):
Set the client attributeuse.lightweight.access.token = truefor 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; }Update Protocol Mappers MGR-TENANTS:
Add the “Subject (sub)” mapper for all client scopes
ImpersonationClientService,LoginClientService, andPasswordResetClientService, 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.
Configure Role Policies:
In
LoginClientService, setfetchRoles = truealongside 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
KeycloakPolicyMappersofetchRolesis 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(); }
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 = trueensures 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.