Skip to content

fix(sso): merge Keycloak realm/client roles from access_token, not just userinfo/id_token#5330

Merged
cafalchio merged 6 commits into
Release/v1.0.4from
fix/keycloak-sso-role-claims
Jun 22, 2026
Merged

fix(sso): merge Keycloak realm/client roles from access_token, not just userinfo/id_token#5330
cafalchio merged 6 commits into
Release/v1.0.4from
fix/keycloak-sso-role-claims

Conversation

@msureshkumar88

Copy link
Copy Markdown
Collaborator

Summary

  • Keycloak's built-in realm roles / client roles client-scope mappers default access.token.claim=true but id.token.claim=userinfo.token.claim=false. Since the gateway reads SSO claims from the userinfo response (falling back to id_token only on the documented split-host 401 case), realm_access/resource_access were silently absent on any stock Keycloak setup — even with SSO_KEYCLOAK_ROLE_MAPPINGS configured 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-hand access_token and merge realm_access/resource_access/groups when missing from userinfo and id_token — fixes both the normal 200-OK path and the existing 401 split-host fallback.
  • Fixed a related inconsistency in _map_groups_to_roles(): role_mappings lookups were case-sensitive while _should_user_be_admin() already matched case-insensitively. This 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 docs/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-sso equivalent: docker compose -f docker-compose.yml -f docker-compose.sso.yml --profile sso up -d):

  • Gateway: http://localhost:8080, 3 replicas behind nginx
  • Keycloak: http://localhost:8180 (admin console admin / changeme)
  • Realm: mcp-gateway, client: mcp-gateway
  • SSO_KEYCLOAK_BASE_URL=http://keycloak:8080, SSO_KEYCLOAK_PUBLIC_BASE_URL=http://localhost:8180
  • SSO_KEYCLOAK_ROLE_MAPPINGS={"gateway-admin":"platform_admin","gateway-developer":"developer","gateway-viewer":"viewer"}
  • SSO_KEYCLOAK_MAP_REALM_ROLES=true, SSO_KEYCLOAK_DEFAULT_ROLE=viewer

Test 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):

    1. docker compose -f docker-compose.yml -f docker-compose.sso.yml --profile sso build gateway
    2. Recreate Keycloak with a fresh volume so the updated realm-export.json mappers actually get imported (Keycloak's --import-realm won't retrofit mapper config onto an existing client):
      docker compose -f docker-compose.yml -f docker-compose.sso.yml --profile sso stop keycloak gateway
      docker compose -f docker-compose.yml -f docker-compose.sso.yml --profile sso rm -f keycloak
      docker volume rm mcp-context-forge_keycloakdata
      docker compose -f docker-compose.yml -f docker-compose.sso.yml --profile sso up -d keycloak gateway
      
    3. Create a fresh Keycloak user via the Admin REST API, assign it the gateway-admin realm role.
    4. Drive the real browser-facing OAuth flow: GET /auth/sso/login/keycloak -> Keycloak login form -> GET /auth/sso/callback/keycloak.
    5. Decode the resulting jwt_token cookie and call GET /admin/overview/partial (RBAC-gated, allow_admin_bypass=False) and GET /admin/ with Accept: 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.

…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>
Suresh Kumar Moharajan 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>
@msureshkumar88 msureshkumar88 force-pushed the fix/keycloak-sso-role-claims branch from 73ed318 to 771ecd0 Compare June 20, 2026 09:45

@madhu-mohan-jaishankar madhu-mohan-jaishankar left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!

@msureshkumar88 msureshkumar88 changed the base branch from main to Release/v1.0.4 June 22, 2026 08:54
@cafalchio cafalchio merged commit a5b8a77 into Release/v1.0.4 Jun 22, 2026
39 checks passed
@cafalchio cafalchio deleted the fix/keycloak-sso-role-claims branch June 22, 2026 12:22
@Lang-Akshay

Copy link
Copy Markdown
Collaborator

Thanks for the PR - @msureshkumar88

Changes are surgical and only touches Keycloak

Both the configuration gap and RBAC issue is resolved

Provider Impact Notes
Keycloak Fixed Fixes role claim visibility issue
Okta None Uses generic OIDC path; configure groups_claim in metadata
Azure AD / Entra None Has own provider.id == "entra" path, untouched
Auth0 None Generic OIDC path; needs groups_claim configured
Google Workspace None provider.id == "google" path untouched
GitHub SSO None provider.id == "github" path untouched
Generic OIDC Case-insensitive match improved Fix 2 benefits all providers

Google SSO Tested and works ✅

SSO_ENABLED=true
SSO_GOOGLE_ENABLED=true
SSO_GOOGLE_CLIENT_ID=your-client-id.googleusercontent.com
SSO_GOOGLE_CLIENT_SECRET=your-client-secret

# Optional: auto-grant admin to these email domains
SSO_GOOGLE_ADMIN_DOMAINS=gmail.com

# Optional: restrict login to these domains only
SSO_TRUSTED_DOMAINS=gmail.com

# SSO General Settings
SSO_AUTO_CREATE_USERS=true

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG]: Keycloak SSO silently drops role claims (admin grant fails) when userinfo 401s on split-host config

4 participants