Skip to content

fix(sso): extract user roles from JWT access token for Keycloak compa…#20591

Merged
ishaan-jaff merged 1 commit intoBerriAI:mainfrom
michelligabriele:fix/sso-keycloak-role-mapping
Feb 7, 2026
Merged

fix(sso): extract user roles from JWT access token for Keycloak compa…#20591
ishaan-jaff merged 1 commit intoBerriAI:mainfrom
michelligabriele:fix/sso-keycloak-role-mapping

Conversation

@michelligabriele
Copy link
Collaborator

Relevant issues

Fixes #20407

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have Added testing in the tests/litellm/ directory, Adding at least 1 test is a hard requirement - see details
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible, it only solves 1 specific problem

CI (LiteLLM team)

CI status guideline:

  • 50-55 passing tests: main is stable with minor issues.
  • 45-49 passing tests: acceptable but needs attention
  • <= 40 passing tests: unstable; be careful with your merges and assess the risk.
  • Branch creation CI run
    Link:

  • CI run for the last commit
    Link:

  • Merge / cherry-pick CI run
    Links:

Type

🐛 Bug Fix
✅ Test

Changes

Problem

Keycloak (and similar OIDC providers) include role claims in the JWT access token but not in the UserInfo endpoint response. Previously, process_sso_jwt_access_token() only extracted team_ids from the JWT — roles were only read from the UserInfo response via generic_response_convertor(). This caused all Keycloak SSO users to default to internal_user_view_only regardless of their actual role assignment.

Additionally, get_litellm_user_role() expected a plain string but Keycloak returns roles as arrays (e.g. ["proxy_admin"]), causing an AttributeError that was silently caught, returning None.

Root causes

  1. Roles not extracted from JWT access tokenprocess_sso_jwt_access_token() decoded the JWT but only used it for team_ids extraction. Role claims present in the token were ignored.
  2. List-type role values not handledget_litellm_user_role() called .lower() directly on the input. When Keycloak passes ["proxy_admin"] (a list), this raised AttributeError → caught → returned None → user got internal_user_view_only.

Fix

  • ui_sso.pyprocess_sso_jwt_access_token() now extracts user roles from the JWT access token when UserInfo doesn't provide them. It tries role_mappings first (group-based), then falls back to GENERIC_USER_ROLE_ATTRIBUTE. Only sets the role if existing_role is None (never overrides UserInfo).
  • types.pyget_litellm_user_role() now handles list inputs by taking the first element before validation.
  • test_ui_sso.py — Added 9 new unit tests for role extraction from JWT and list handling. Updated 3 existing tests to match new behavior.

Reproduction

Reproduced end-to-end with a local Keycloak instance (realm with client roles + protocol mapper) and LiteLLM proxy. Before fix: /sso/debug/login showed no user_role. After fix: correctly shows proxy_admin / internal_user based on Keycloak role assignment.

Screenshots

Screenshot 2026-02-06 alle 18 37 05

…tibility

Keycloak (and similar OIDC providers) include role claims in the JWT
access token but not in the UserInfo endpoint response. Previously,
roles were only extracted from UserInfo, causing all SSO users to
default to internal_user_view_only regardless of their actual role.

Changes:
- Extract user roles from JWT access token in process_sso_jwt_access_token()
  when UserInfo doesn't provide them (tries role_mappings first, then
  GENERIC_USER_ROLE_ATTRIBUTE)
- Handle list-type role values in get_litellm_user_role() since Keycloak
  returns roles as arrays (e.g. ["proxy_admin"] instead of "proxy_admin")
- Add 9 new unit tests covering role extraction and list handling
- Update 3 existing tests for new JWT decode behavior

Closes BerriAI#20407
@vercel
Copy link

vercel bot commented Feb 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Ready Ready Preview, Comment Feb 6, 2026 5:39pm

Request Review

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 6, 2026

Greptile Overview

Greptile Summary

Fixes Keycloak SSO role mapping by extracting user roles from JWT access tokens. Keycloak includes role claims in JWT access tokens but not in UserInfo responses, causing all users to default to internal_user_view_only. The fix adds role extraction from JWTs in process_sso_jwt_access_token() and handles list-type role values (Keycloak returns ["proxy_admin"] instead of "proxy_admin").

Key changes:

  • get_litellm_user_role() now handles list inputs by taking the first element
  • process_sso_jwt_access_token() extracts roles from JWT using role_mappings or GENERIC_USER_ROLE_ATTRIBUTE
  • Role extraction only occurs when UserInfo didn't provide a role (never overrides)
  • Comprehensive test coverage with 9 new tests covering list handling and JWT extraction scenarios

The implementation correctly prioritizes UserInfo roles over JWT roles, preventing unintended overrides. JWT decoding without signature verification is acceptable here since tokens are obtained through verified OAuth flows.

Confidence Score: 5/5

  • This PR is safe to merge with no identified risks
  • The implementation is well-designed with proper defensive checks: roles from UserInfo take precedence (never overridden), comprehensive test coverage validates all edge cases (empty lists, invalid roles, multiple roles), and JWT decoding without verification is safe since tokens come from verified OAuth flows. The fix solves a real Keycloak compatibility issue without introducing security vulnerabilities or breaking existing functionality.
  • No files require special attention

Important Files Changed

Filename Overview
litellm/proxy/management_endpoints/types.py Added list handling to get_litellm_user_role() for Keycloak compatibility - takes first element from list inputs
litellm/proxy/management_endpoints/ui_sso.py Extended process_sso_jwt_access_token() to extract user roles from JWT access tokens using role_mappings or GENERIC_USER_ROLE_ATTRIBUTE, only when UserInfo doesn't provide roles
tests/test_litellm/proxy/management_endpoints/test_ui_sso.py Added 9 tests for list role handling and JWT role extraction, updated 3 existing tests to handle new JWT decoding behavior

Sequence Diagram

sequenceDiagram
    participant User
    participant LiteLLM as LiteLLM Proxy
    participant Keycloak as Keycloak IdP
    participant UserInfo as UserInfo Endpoint
    
    User->>LiteLLM: SSO Login Request
    LiteLLM->>Keycloak: OAuth2 Authorization
    Keycloak-->>LiteLLM: Authorization Code
    LiteLLM->>Keycloak: Token Exchange
    Keycloak-->>LiteLLM: Access Token (JWT with roles)
    
    LiteLLM->>UserInfo: Request User Info
    UserInfo-->>LiteLLM: User Data (no roles)
    
    Note over LiteLLM: generic_response_convertor()<br/>extracts user data from UserInfo<br/>but user_role remains None
    
    LiteLLM->>LiteLLM: process_sso_jwt_access_token()
    Note over LiteLLM: Decode JWT Access Token<br/>(without signature verification)
    
    alt Role Mappings Available
        Note over LiteLLM: Extract groups from JWT<br/>determine_role_from_groups()
    else Fallback to GENERIC_USER_ROLE_ATTRIBUTE
        Note over LiteLLM: Extract role from JWT field<br/>get_litellm_user_role()
    end
    
    Note over LiteLLM: Handle list-type roles<br/>["proxy_admin"] → "proxy_admin"
    
    LiteLLM->>LiteLLM: Set user_role if not already set
    LiteLLM-->>User: Authenticated with correct role
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

3 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

Copy link
Member

@ishaan-jaff ishaan-jaff left a comment

Choose a reason for hiding this comment

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

lgtm

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.

2 participants