fix(sso): merge Keycloak realm/client roles from access_token, not just userinfo/id_token#5330
Merged
Merged
Conversation
…st userinfo/id_token Keycloak's built-in "realm roles" and "client roles" client-scope mappers default access.token.claim=true but id.token.claim=userinfo.token.claim=false. Since SSO role mapping reads claims from the userinfo response (with an id_token fallback for split-host 401s), realm_access/resource_access were silently missing on any stock Keycloak setup, even when the operator assigned the correct role and configured SSO_KEYCLOAK_ROLE_MAPPINGS correctly. New admins (and any role-mapped user) would fall through to the default role instead. _get_user_info()/_enrich_user_data_from_claims() now also decode the already-in-hand access_token and merge realm_access/resource_access/groups when missing from userinfo and id_token, covering both the normal 200 path and the existing 401 split-host fallback. Also fix a related inconsistency in _map_groups_to_roles(): role_mappings lookups were case-sensitive while _should_user_be_admin() already matched case-insensitively, which could grant is_admin=True with no matching RBAC role row when IdP role casing differed from the configured mapping key. Updated the local Keycloak dev seed (infra/keycloak/realm-export.json) to explicitly enable id_token/userinfo claim inclusion on the realm/client role mappers, and documented the default-mapper gotcha in the Keycloak SSO tutorial. Closes #5327 Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
added 5 commits
June 20, 2026 10:19
Line numbers shifted in mcpgateway/services/sso_service.py and tests/unit/mcpgateway/services/test_sso_service.py after the role-claims merge fix; regenerated via make detect-secrets-scan. Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
Previous baseline update had an off-by-one line number for the existing admin.html allowlisted entry; regenerated to match current file state. Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
Baseline had residual stale line-number drift from earlier regenerations run against transient working-tree states. Re-scanned against the clean checkout; two consecutive scans now agree with no diff. Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
Earlier regeneration used the Makefile's pinned detect-secrets release via 'make detect-secrets-scan', which computes slightly different line offsets than the IBM fork pinned in .pre-commit-config.yaml that CI actually runs. Regenerate using 'pre-commit run detect-secrets' so the baseline matches what CI checks against. Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
…line Previous commit captured the baseline before the post-stabilization scan result; the realm-export.json entry shifts from line 172 to 200 because this PR adds 28 lines of protocolMappers above it. Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
73ed318 to
771ecd0
Compare
madhu-mohan-jaishankar
approved these changes
Jun 22, 2026
madhu-mohan-jaishankar
left a comment
Collaborator
There was a problem hiding this comment.
Looks good to me. Addresses the Keycloak role-claim issue, covers both the normal and split-host fallback paths, and adds solid regression tests. The docs and local Keycloak config updates are also helpful. LGTM!
Collaborator
Thanks for the PR - @msureshkumar88Changes are surgical and only touches KeycloakBoth the configuration gap and RBAC issue is resolved
Google SSO Tested and works ✅ |
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.
Summary
access.token.claim=truebutid.token.claim=userinfo.token.claim=false. Since the gateway reads SSO claims from the userinfo response (falling back toid_tokenonly on the documented split-host 401 case),realm_access/resource_accesswere silently absent on any stock Keycloak setup — even withSSO_KEYCLOAK_ROLE_MAPPINGSconfigured correctly and the right role assigned. New admins (or any role-mapped user) fell through to the default role._get_user_info()/_enrich_user_data_from_claims()(mcpgateway/services/sso_service.py) now also decode the already-in-handaccess_tokenand mergerealm_access/resource_access/groupswhen missing from userinfo and id_token — fixes both the normal 200-OK path and the existing 401 split-host fallback._map_groups_to_roles():role_mappingslookups were case-sensitive while_should_user_be_admin()already matched case-insensitively. This could grantis_admin=Truewith no matching RBAC role row when IdP role casing differed from the configured mapping key.infra/keycloak/realm-export.json) to explicitly enableid_token/userinfoclaim inclusion on the realm/client role mappers, and documented the default-mapper gotcha indocs/docs/manage/sso-keycloak-tutorial.md(Step 4.4 + troubleshooting section).Closes #5327. Both findings posted on the issue (#5327 (comment)) are addressed by this PR.
Config used to reproduce/verify locally
Local Docker Compose SSO stack (
make compose-ssoequivalent:docker compose -f docker-compose.yml -f docker-compose.sso.yml --profile sso up -d):http://localhost:8080, 3 replicas behind nginxhttp://localhost:8180(admin consoleadmin/changeme)mcp-gateway, client:mcp-gatewaySSO_KEYCLOAK_BASE_URL=http://keycloak:8080,SSO_KEYCLOAK_PUBLIC_BASE_URL=http://localhost:8180SSO_KEYCLOAK_ROLE_MAPPINGS={"gateway-admin":"platform_admin","gateway-developer":"developer","gateway-viewer":"viewer"}SSO_KEYCLOAK_MAP_REALM_ROLES=true,SSO_KEYCLOAK_DEFAULT_ROLE=viewerTest plan
pytest tests/unit/mcpgateway/services/test_sso_service.py tests/unit/mcpgateway/services/test_sso_entra_role_mapping.py tests/unit/mcpgateway/services/test_sso_generic_oidc_role_mapping.py tests/unit/mcpgateway/services/test_sso_user_normalization.py tests/unit/mcpgateway/services/test_sso_admin_assignment.py tests/unit/mcpgateway/utils/test_sso_bootstrap.py tests/unit/mcpgateway/routers/test_sso_router.py tests/integration/test_sso_adfs_integration.py— all pass, including 4 new regression tests added in this PR.Manual end-to-end verification against a rebuilt local stack (fresh Keycloak volume re-imported with the updated mapper config, gateway image rebuilt from this branch):
docker compose -f docker-compose.yml -f docker-compose.sso.yml --profile sso build gatewayrealm-export.jsonmappers actually get imported (Keycloak's--import-realmwon't retrofit mapper config onto an existing client):gateway-adminrealm role.GET /auth/sso/login/keycloak-> Keycloak login form ->GET /auth/sso/callback/keycloak.jwt_tokencookie and callGET /admin/overview/partial(RBAC-gated,allow_admin_bypass=False) andGET /admin/withAccept: text/html.Before the fix:
scopes.permissions: [],/admin/overview/partial->403 Admin privileges required.After the fix:
scopes.permissions: ["*"],/admin/overview/partial->200,/admin/->200.