Skip to content

Conversation

leondape
Copy link
Contributor

@leondape leondape commented Oct 7, 2025

Pull Request: OIDC Group Synchronization for Keycloak and Generic OpenID Providers

Summary

Implements JWT token-based group synchronization to enable the granular permissions system with any OIDC provider (Keycloak, Auth0, Okta, etc.)

This PR addresses the need for non-Microsoft OIDC providers to utilize LibreChat's granular permissions system introduced in v0.7.9. Currently, group discovery only works with Microsoft Entra ID via Graph API. This implementation allows any OIDC provider to sync groups/roles from JWT tokens.

Related to #10006

Features:

  • Extract groups/roles from configurable JWT claim paths
  • Automatically sync group memberships on login
  • Create groups and add users atomically
  • Remove users from groups when roles are revoked
  • Support for any OIDC provider via flexible claim path configuration

Changes:

  • Add JWT claim extraction utilities with sanitization (api/utils/extractJwtClaims.js)
  • Implement syncUserOidcGroupsFromToken in PermissionService
  • Fix idOnTheSource to use sub as fallback for non-Microsoft providers (Keycloak compatibility)
  • Integrate OIDC group sync into OAuth login flow
  • Add 'oidc' as valid group source in schema
  • Add comprehensive unit tests (119 test cases)
    • api/utils/extractJwtClaims.spec.js - JWT claim extraction tests
    • api/server/services/PermissionService.oidc.spec.js - Service sync logic tests
  • Add detailed documentation in .env.example

Configuration Example:

OPENID_SYNC_GROUPS_FROM_TOKEN=true
OPENID_GROUPS_CLAIM_PATH=realm_access.roles
OPENID_GROUPS_TOKEN_KIND=access
OPENID_GROUP_SOURCE=oidc

Future Improvements (noted in code comments):

  • Bulk query optimization for large group counts (>20 groups)
  • Transaction wrapping for full atomicity
  • Group name transformation/mapping
  • Role filtering capabilities (exclude system/default roles)
  • Orphaned group cleanup

Change Type

  • New feature (non-breaking change which adds functionality)
  • This change requires a documentation update

Testing

Test Configuration:

  • OIDC Provider: Keycloak 26.x
  • Environment: Development (Docker setup)
  • Database: MongoDB
  • Configuration:
    • OPENID_REUSE_TOKENS=true
    • OPENID_SYNC_GROUPS_FROM_TOKEN=true
    • OPENID_GROUPS_CLAIM_PATH=realm_access.roles
    • OPENID_GROUPS_TOKEN_KIND=access
    • OPENID_GROUP_SOURCE=keycloak

Test Process:

  1. Setup Keycloak 26.x with realm roles: leonine, default-roles-mediawan, access_leonine
  2. Assigned roles to test user
  3. Logged in via Keycloak SSO
  4. Verified groups were created in MongoDB:
    db.groups.find({ source: 'keycloak' })
    # Result: 3 groups created with user in memberIds
  5. Tested removing a role in Keycloak → user automatically removed from group on next login
  6. Verified groups are available in LibreChat UI for sharing resources

Test Results:

Realm roles tested and working:

  • Groups extracted from JWT token successfully
  • Groups created in database with correct source
  • User automatically added to groups
  • User removed from groups when roles revoked
  • Groups available in permissions/sharing UI
  • Works with sub claim (Keycloak) instead of oid (Microsoft-specific)

📝 Keycloak groups (via group mapper) not yet tested but should work:

  • Implementation supports any JWT claim path
  • Documentation includes setup instructions for group mappers
  • Logic is provider-agnostic and should handle group claims identically to role claims

Unit Test Coverage:

  • ✅ JWT claim extraction (multiple scenarios, edge cases)
  • ✅ Group name sanitization (special characters, length limits)
  • ✅ Token decoding (access/id tokens)
  • ✅ Sync logic (create, update, remove memberships)
  • ✅ Error handling and validation
  • ✅ Session support
  • ✅ 119 test cases passing

Checklist

  • My code adheres to this project's style guidelines
  • I have performed a self-review of my own code
  • I have commented in any complex areas of my code
  • I have made pertinent documentation changes
  • My changes do not introduce new warnings
  • I have written tests demonstrating that my changes are effective or that my feature works
  • Local unit tests pass with my changes
  • Any changes dependent on mine have been merged and published in downstream modules
  • A pull request for updating the documentation has been submitted

Documentation PR

Documentation has been submitted to the docs repository:
📝 LibreChat-AI/librechat.ai#426

Additional Context

This implementation follows the same pattern as the existing Entra ID group sync but reads from JWT claims instead of making Graph API calls, making it more efficient and compatible with any OIDC provider.

Benefits:

  • Works with Keycloak, Auth0, Okta, and any OIDC provider
  • No additional API calls required (groups are in the token)
  • Automatic synchronization on every login
  • Consistent with existing Entra ID pattern
  • Enables full ACL permissions system for non-Microsoft users

Tested with: Keycloak 26.x (realm roles confirmed working)
Compatible with: Any OIDC provider that includes groups/roles in JWT tokens


AI-slop-Disclaimer:
To my best knowledge I've reviewed and guided the development of this feature. However I seek maintainer's guidance as this code is heavily developed by cursor.

Implements JWT token-based group synchronization to enable the granular
permissions system with any OIDC provider (Keycloak, Auth0, Okta, etc.)

Features:
- Extract groups/roles from configurable JWT claim paths
- Automatically sync group memberships on login
- Create groups and add users atomically
- Remove users from groups when roles are revoked
- Support for any OIDC provider via flexible claim path configuration

Changes:
- Add JWT claim extraction utilities with sanitization
- Implement syncUserOidcGroupsFromToken in PermissionService
- Fix idOnTheSource to use 'sub' as fallback for non-Microsoft providers
- Integrate OIDC group sync into OAuth login flow
- Add 'oidc' as valid group source in schema
- Add comprehensive unit tests (119 test cases)
- Add detailed documentation in .env.example

Configuration:
OPENID_SYNC_GROUPS_FROM_TOKEN=true
OPENID_GROUPS_CLAIM_PATH=realm_access.roles
OPENID_GROUPS_TOKEN_KIND=access
OPENID_GROUP_SOURCE=oidc

TODO: Future improvements noted in code comments:
- Bulk query optimization for large group counts
- Transaction wrapping for full atomicity
- Group name transformation/mapping
- Role filtering capabilities
- Orphaned group cleanup

Tested with Keycloak - successfully syncs groups on login

Related to danny-avila#10006
Copy link
Contributor

@github-advanced-security github-advanced-security bot left a comment

Choose a reason for hiding this comment

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

ESLint found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

Allow filtering out system/default roles from group synchronization:
- Add shouldExcludeGroup() function with exact match and regex support
- Support comma-separated exclusion patterns via OPENID_GROUPS_EXCLUDE_PATTERN
- Case-insensitive exact matching for role names
- Regex patterns with 'regex:' prefix (e.g., regex:^default-.*)
- Update extractGroupsFromToken to accept and apply exclusion filter
- Add comprehensive documentation in .env.example

Use cases:
- Exclude Keycloak default roles (default-roles-*, manage-account, etc.)
- Exclude Auth0 system scopes (offline_access, openid, profile, email)
- Exclude authentication-only roles that shouldn't become groups
- Filter out UMA authorization roles (uma_authorization)

Examples:
  OPENID_GROUPS_EXCLUDE_PATTERN=default-roles-mediawan,manage-account,offline_access
  OPENID_GROUPS_EXCLUDE_PATTERN=regex:^default-.*,regex:^manage-.*,offline_access

Tested with Keycloak 26.x - successfully filters out system roles
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.

1 participant