Description
Programmatic permission evaluation in Keycloak extensions follows a standard pattern: an extension receives a bearer token, constructs a KeycloakIdentity from it, and invokes AuthorizationProvider.evaluators() to evaluate permissions against role-based policies. Under certain configurations — specifically when a role-based policy references a client role defined in a different client than the resource server, and that client is not part of the requesting client's scope — this pattern yields authorization decisions inconsistent with the admin console's Authorization > Evaluate feature.
The admin console's PolicyEvaluationService.createIdentity() already compensates for this internally by projecting all of the user's role mappings onto the access token before constructing the identity. This logic is currently private. This enhancement promotes it to a public static helper, TokenIdentityEnricher.addAllUserRoles(AccessToken, UserModel), co-located with KeycloakIdentity in the services module, and refactors PolicyEvaluationService to delegate to the same helper.
Value Proposition
- Extensions performing programmatic authorization evaluation gain a supported, public API for producing decisions consistent with Keycloak's own admin console, without replicating internal enrichment logic in their own code.
- Eliminates duplication between
PolicyEvaluationService and any downstream extension that needs the same scope-bypassing enrichment.
- Cross-client role policies — a legitimate and common configuration — become reliable from extension code paths, not only from the admin console.
Goals
- Introduce
TokenIdentityEnricher.addAllUserRoles(AccessToken, UserModel) as a public static helper in org.keycloak.authorization.common.
- Refactor
PolicyEvaluationService.createIdentity() to delegate to the same helper, so the admin console and extensions share a single implementation of the enrichment loop.
- Preserve backwards compatibility: no changes to existing
KeycloakIdentity constructors or their externally observable behavior; PolicyEvaluationService's admin-API surface is unchanged.
- Cover the new helper with unit tests (null-argument guards) and an integration test demonstrating admin-console parity end-to-end.
Non-Goals
- Not introducing a higher-level
KeycloakIdentity.fromUser(session, realm, userId) factory in this
issue. See "Refinement from discussion KeycloakIdentity should support creating a user-model-bound identity for server-side authorization evaluation #46661" in Notes for the rationale; it remains an open option for a follow-up if an ergonomic one-line API is desired in addition to the helper.
- Not modifying the synthetic-token-construction path used by
PolicyEvaluationService for admin-console evaluation (authentication session, user session, and client session creation, plus TokenManager.createClientAccessToken()). That flow remains internal and unchanged.
- Not addressing other OIDC scope-related limitations beyond role projection.
Discussion
#46661
Notes
Problem in detail
When a resource is protected by a role-based policy referencing a client role defined in a different client than the resource server, a KeycloakIdentity constructed from the bearer token can produce incorrect authorization decisions:
- A resource on
client-a is protected by a policy requiring a client role from client-b.
- The user holds the
client-b role.
- The token issued for
client-a, however obtained, may not include client-b roles in resource_access, because OIDC scope configuration filters which clients appear there.
new KeycloakIdentity(session, token) derives role attributes exclusively from the token's resource_access.
- Since
client-b roles are absent, the kc.client.client-b.roles attribute is missing from the identity, and RolePolicyProvider evaluates to DENY.
Meanwhile, the admin console's Authorization > Evaluate feature correctly returns PERMIT for the same user, resource, and policy.
Root cause
PolicyEvaluationService.createIdentity() compensates for this internally: after constructing a token via
TokenManager.createClientAccessToken(), it injects all of the user's role mappings into the token. This ensures the identity reflects every role the user actually holds, regardless of token scope constraints.
This logic is currently private to PolicyEvaluationService and not exposed as a public API. Extension developers performing authorization evaluation must either replicate the same enrichment loop in their own code (duplicating Keycloak-internal logic) or accept incorrect DENY decisions for cross-client role policies.
Proposed API
package org.keycloak.authorization.common;
public final class TokenIdentityEnricher {
public static void addAllUserRoles(AccessToken token, UserModel user) {
// Realm roles → realm_access
// Client roles → resource_access[clientId]
}
}
Usage from an extension:
AccessToken token = Tokens.getAccessToken(session);
UserModel user = session.users().getUserById(realm, token.getSubject());
TokenIdentityEnricher.addAllUserRoles(token, user);
Identity identity = new KeycloakIdentity(token, session, realm);
PolicyEvaluationService.createIdentity() would delegate to the same helper, removing the duplicated implementation.
Refinement from discussion #46661
I previously opened discussion #46661 to surface this gap with the community. With no maintainer feedback over the ~2.5 months since, I'm promoting it to an issue with a refined proposal.
That original discussion suggested a higher-level KeycloakIdentity.fromUser(session, realm, userId) factory
mirroring the full behavior of PolicyEvaluationService.createIdentity(). While preparing the implementation, two distinct concerns emerged within that method:
- Synthetic token construction — creating an authentication session, user session, client session, and invoking
TokenManager.createClientAccessToken(). This is required by the admin console scenario, where evaluation runs on behalf of an arbitrary user without a bearer token.
- Role enrichment — projecting all role mappings from
UserModel.getRoleMappingsStream() onto the access token, bypassing OIDC scope filtering.
The cross-client role problem is caused exclusively by (2). Extensions performing authorization evaluation already hold a bearer access token; they need only the role enrichment, not the synthetic session machinery.
A fromUser() factory that performed both operations would:
- Force every caller to incur user/auth session creation overhead even when a valid bearer token is already in scope.
- Hide non-trivial side effects (writes to the user/auth session stores) behind a one-line API.
- Couple a small public surface to the admin-console-specific token construction logic.
This issue therefore proposes the minimum public API needed to address the root cause: the TokenIdentityEnricher.addAllUserRoles static helper. A higher-level fromUser() factory remains an open option for a follow-up.
Verification plan
- Unit (
services/.../TokenIdentityEnricherTest): null-argument guards on addAllUserRoles.
- Integration (
testsuite/integration-arquillian/.../KeycloakIdentityCrossClientRoleTest): two scenarios against a realm with a role policy referencing a client role in a different client:
- Admin console
Authorization > Evaluate → PERMIT. Guards against regressions in the admin-console code path after the helper refactor.
- Enriched token via
TokenIdentityEnricher → PERMIT. Proves the helper produces the same outcome as the admin console when called explicitly on a token-bound identity.
- Existing tests covering
PolicyEvaluationService (e.g. PolicyEvaluationTest, PolicyEvaluationCompositeRoleTest) continue to pass, confirming the refactor preserves admin-console
behavior.
Backwards compatibility
No existing KeycloakIdentity constructor or signature is modified; PolicyEvaluationService.createIdentity()'s externally observable behavior is preserved.
Description
Programmatic permission evaluation in Keycloak extensions follows a standard pattern: an extension receives a bearer token, constructs a
KeycloakIdentityfrom it, and invokesAuthorizationProvider.evaluators()to evaluate permissions against role-based policies. Under certain configurations — specifically when a role-based policy references a client role defined in a different client than the resource server, and that client is not part of the requesting client's scope — this pattern yields authorization decisions inconsistent with the admin console's Authorization > Evaluate feature.The admin console's
PolicyEvaluationService.createIdentity()already compensates for this internally by projecting all of the user's role mappings onto the access token before constructing the identity. This logic is currently private. This enhancement promotes it to a public static helper,TokenIdentityEnricher.addAllUserRoles(AccessToken, UserModel), co-located withKeycloakIdentityin theservicesmodule, and refactorsPolicyEvaluationServiceto delegate to the same helper.Value Proposition
PolicyEvaluationServiceand any downstream extension that needs the same scope-bypassing enrichment.Goals
TokenIdentityEnricher.addAllUserRoles(AccessToken, UserModel)as a public static helper inorg.keycloak.authorization.common.PolicyEvaluationService.createIdentity()to delegate to the same helper, so the admin console and extensions share a single implementation of the enrichment loop.KeycloakIdentityconstructors or their externally observable behavior;PolicyEvaluationService's admin-API surface is unchanged.Non-Goals
KeycloakIdentity.fromUser(session, realm, userId)factory in thisissue. See "Refinement from discussion KeycloakIdentity should support creating a user-model-bound identity for server-side authorization evaluation #46661" in Notes for the rationale; it remains an open option for a follow-up if an ergonomic one-line API is desired in addition to the helper.
PolicyEvaluationServicefor admin-console evaluation (authentication session, user session, and client session creation, plusTokenManager.createClientAccessToken()). That flow remains internal and unchanged.Discussion
#46661
Notes
Problem in detail
When a resource is protected by a role-based policy referencing a client role defined in a different client than the resource server, a
KeycloakIdentityconstructed from the bearer token can produce incorrect authorization decisions:client-ais protected by a policy requiring a client role fromclient-b.client-brole.client-a, however obtained, may not includeclient-broles inresource_access, because OIDC scope configuration filters which clients appear there.new KeycloakIdentity(session, token)derives role attributes exclusively from the token'sresource_access.client-broles are absent, thekc.client.client-b.rolesattribute is missing from the identity, andRolePolicyProviderevaluates to DENY.Meanwhile, the admin console's Authorization > Evaluate feature correctly returns PERMIT for the same user, resource, and policy.
Root cause
PolicyEvaluationService.createIdentity()compensates for this internally: after constructing a token viaTokenManager.createClientAccessToken(), it injects all of the user's role mappings into the token. This ensures the identity reflects every role the user actually holds, regardless of token scope constraints.This logic is currently private to
PolicyEvaluationServiceand not exposed as a public API. Extension developers performing authorization evaluation must either replicate the same enrichment loop in their own code (duplicating Keycloak-internal logic) or accept incorrect DENY decisions for cross-client role policies.Proposed API
Usage from an extension:
PolicyEvaluationService.createIdentity()would delegate to the same helper, removing the duplicated implementation.Refinement from discussion #46661
I previously opened discussion #46661 to surface this gap with the community. With no maintainer feedback over the ~2.5 months since, I'm promoting it to an issue with a refined proposal.
That original discussion suggested a higher-level
KeycloakIdentity.fromUser(session, realm, userId)factorymirroring the full behavior of
PolicyEvaluationService.createIdentity(). While preparing the implementation, two distinct concerns emerged within that method:TokenManager.createClientAccessToken(). This is required by the admin console scenario, where evaluation runs on behalf of an arbitrary user without a bearer token.UserModel.getRoleMappingsStream()onto the access token, bypassing OIDC scope filtering.The cross-client role problem is caused exclusively by (2). Extensions performing authorization evaluation already hold a bearer access token; they need only the role enrichment, not the synthetic session machinery.
A
fromUser()factory that performed both operations would:This issue therefore proposes the minimum public API needed to address the root cause: the
TokenIdentityEnricher.addAllUserRolesstatic helper. A higher-levelfromUser()factory remains an open option for a follow-up.Verification plan
services/.../TokenIdentityEnricherTest): null-argument guards onaddAllUserRoles.testsuite/integration-arquillian/.../KeycloakIdentityCrossClientRoleTest): two scenarios against a realm with a role policy referencing a client role in a different client:Authorization > Evaluate→ PERMIT. Guards against regressions in the admin-console code path after the helper refactor.TokenIdentityEnricher→ PERMIT. Proves the helper produces the same outcome as the admin console when called explicitly on a token-bound identity.PolicyEvaluationService(e.g.PolicyEvaluationTest,PolicyEvaluationCompositeRoleTest) continue to pass, confirming the refactor preserves admin-consolebehavior.
Backwards compatibility
No existing
KeycloakIdentityconstructor or signature is modified;PolicyEvaluationService.createIdentity()'s externally observable behavior is preserved.