Resolve service account roles when no user session exists#50971
Open
igraecao wants to merge 3 commits into
Open
Resolve service account roles when no user session exists#50971igraecao wants to merge 3 commits into
igraecao wants to merge 3 commits into
Conversation
When a service account uses client_credentials grant with fullScopeAllowed=false, the access token contains no roles and no user session is created. resolveLightweightAccessTokenRoles previously returned silently in this case, leaving the token with empty role attributes. This caused all admin API permission checks to fail with 403, regardless of the roles actually assigned to the service account user. Add a fallback: when no user session is found but the token has a subject claim, resolve roles directly from the UserModel role mappings. Closes keycloak#50950
Contributor
There was a problem hiding this comment.
Pull request overview
Adds fallback role resolution for sessionless service-account access tokens used by admin authorization.
Changes:
- Loads service-account roles from the user model when no session exists.
- Populates realm and client role claims dynamically.
Comment on lines
+1690
to
1697
| } else if (accessToken.getSubject() != null) { | ||
| // Fallback for service accounts using client_credentials grant without a | ||
| // persistent user session. Resolve roles directly from the user's role mappings. | ||
| UserModel user = session.users().getUserById(realm, accessToken.getSubject()); | ||
| if (user != null) { | ||
| resolveRolesFromUserModel(session, accessToken, realm, user); | ||
| } | ||
| } |
| private static void resolveRolesFromUserModel(KeycloakSession session, AccessToken accessToken, RealmModel realm, UserModel user) { | ||
| // Resolve realm roles | ||
| AccessToken.Access realmAccess = new AccessToken.Access(); | ||
| user.getRealmRoleMappingsStream().forEach(role -> realmAccess.addRole(role.getName())); |
| // persistent user session. Resolve roles directly from the user's role mappings. | ||
| UserModel user = session.users().getUserById(realm, accessToken.getSubject()); | ||
| if (user != null) { | ||
| resolveRolesFromUserModel(session, accessToken, realm, user); |
When a service account uses client_credentials grant with fullScopeAllowed=false, the lightweight access token contains no roles. Role resolution from the user session fails because client_credentials does not create a persistent session. V2 fixes: - Only activates for service accounts linked to the issuing client - Uses TokenManager.getAccess() which respects fullScopeAllowed and client scope mappings - Expired user tokens cannot regain roles through this path Closes keycloak#50950
…50950) Verifies that client_credentials grant with fullScopeAllowed=false correctly resolves only scoped roles, and that unscoped roles are not leaked into the resolved token. Three cases: 1. fullScopeAllowed=true: all assigned roles appear 2. fullScopeAllowed=false + role in scope: role appears, access works 3. fullScopeAllowed=false + role NOT in scope: 403 as expected
Comment on lines
+1708
to
+1711
| Stream<ClientScopeModel> clientScopes = Stream.concat( | ||
| client.getClientScopes(true).values().stream(), | ||
| Stream.of(client)); | ||
| Set<RoleModel> allowedRoles = TokenManager.getAccess(user, client, clientScopes); |
Comment on lines
+76
to
+81
| ClientRepresentation limitedScopeClient = ClientBuilder.create() | ||
| .id(KeycloakModelUtils.generateId()) | ||
| .clientId(CLIENT_LIMITED_SCOPE) | ||
| .secret(CLIENT_SECRET) | ||
| .serviceAccountsEnabled(true) | ||
| .fullScopeEnabled(false) |
Comment on lines
+133
to
+134
| // Add view-users to the client's scope mappings (but NOT manage-users) | ||
| addRealmManagementRoleToClientScope(realmResource, CLIENT_LIMITED_SCOPE, "view-users"); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When a service account client uses
client_credentialsgrant withfullScopeAllowed=false, the resulting lightweight access token contains no roles in its body. The role resolution path inresolveLightweightAccessTokenRolesrelies on finding a valid user session - butclient_credentialsdoes not create a persistent user session. Result: all admin API calls return 403 regardless of assigned roles.This affects ALL admin REST API endpoints, not just user profile (the originally reported symptom).
Root cause
resource_access: null(lightweight - roles stripped from JWT)client_credentialscreates no persistent user sessionfindValidSessionForAccessTokenreturns nullKeycloakIdentityhas empty role attributes - 403 on everythingFix (V2 - scope-aware)
Adds a fallback in
resolveLightweightAccessTokenRolesthat:user.getServiceAccountClientLink()matches the issuing client. Expired user tokens cannot regain roles through this path.TokenManager.getAccess(user, client, clientScopes)which correctly handlesfullScopeAllowed=falseby intersecting user roles with scope mappings.V1 → V2 changes (addressing review feedback)
fullScopeAllowed=false)Test coverage
ServiceAccountScopedRolesTestverifies:fullScopeAllowed=true: all assigned roles resolved, admin API accessiblefullScopeAllowed=false+ role in scope: only scoped roles appear, access worksfullScopeAllowed=false+ role NOT in scope: 403 as expectedHow to reproduce
Closes #50950