Microsoft EntraID Role and Group Claim MappingΒΆ
OverviewΒΆ
This feature enables automatic role assignment for Microsoft EntraID (formerly Azure AD) SSO users based on their group memberships or app role assignments. Users are automatically assigned ContextForge RBAC roles based on their EntraID groups, providing granular access control without manual intervention.
FeaturesΒΆ
- β Extract groups and roles from EntraID tokens
- β Map EntraID groups to ContextForge RBAC roles
- β Support for both Security Groups (Object IDs) and App Roles
- β Automatic role synchronization on login
- β Platform admin assignment via admin groups
- β Default role assignment for users without group mappings
- β Automatic Microsoft Graph fallback for groups overage claims
- β Consistent with Keycloak implementation pattern
ArchitectureΒΆ
Role SystemΒΆ
ContextForge includes a comprehensive RBAC system with the following default roles:
-
platform_admin(global scope) -
Permissions:
["*"](all permissions) -
Full platform access
-
team_admin(team scope) -
Permissions: Team management, tools, resources, prompts
-
Can manage team members and settings
-
developer(team scope) -
Permissions: Tool execution, resource access, prompts
-
Can execute tools and access resources
-
viewer(team scope) -
Permissions: Read-only access to tools, resources, prompts
- Cannot execute tools or modify resources
Implementation ComponentsΒΆ
-
Configuration (
mcpgateway/config.py) -
sso_entra_groups_claim: JWT claim for groups (default: "groups") sso_entra_admin_groups: Groups granting platform_admin rolesso_entra_role_mappings: Map groups to rolessso_entra_default_role: Default role for unmapped userssso_entra_sync_roles_on_login: Sync roles on each loginsso_entra_graph_api_enabled: Enable Graph fallback on overagesso_entra_graph_api_timeout: Timeout for Graph fallback request-
sso_entra_graph_api_max_groups: Optional cap on Graph-fetched groups -
Token Parsing (
_get_user_info()and_decode_jwt_claims()) -
Parses the
id_tokenJWT to extract claims (Microsoft's userinfo endpoint doesn't return groups) - Extracts
groupsclaim (Security Groups as Object IDs) - Extracts
rolesclaim (App Roles) -
Falls back to userinfo for basic profile claims (email, name, etc.)
-
Group Normalization (
_normalize_user_info()) -
Combines groups and roles from id_token
- Deduplicates and returns normalized user info
-
Supports custom groups claim via provider metadata
-
Role Mapping (
_map_groups_to_roles()) -
Maps EntraID groups to ContextForge roles
- Checks admin groups first (case-insensitive)
- Applies role mappings from configuration
-
Assigns default role if no mappings found
-
Role Synchronization (
_sync_user_roles()) -
Synchronizes roles on user creation and login
- Revokes SSO-granted roles no longer in groups
- Assigns new roles based on current groups
- Preserves manually assigned roles
-
Maintains audit trail with
grant_source='sso'andgranted_by=<user_email> -
Admin Status Synchronization (
authenticate_or_create_user()) -
Upgrades
is_adminflag when user gains admin group membership - Never downgrades
is_admin- manual admin grants via Admin UI/API are preserved - To revoke admin access, use the Admin UI/API directly
ConfigurationΒΆ
1. EntraID App Registration SetupΒΆ
Token Configuration (Azure Portal)ΒΆ
Important: Groups and roles must be configured in the ID token, not just the access token. Microsoft's OIDC userinfo endpoint does not return group claims - ContextForge extracts them from the ID token.
- Navigate to Azure Portal β App Registrations β Your App
- Go to Token Configuration
- Click + Add groups claim
- Select token types: ID (required), Access (optional)
-
Select group types:
-
Security groups (recommended for most use cases)
-
Or All groups if you need Microsoft 365 groups
-
Choose Group ID format for stability (Object IDs won't change)
- For App Roles: Go to App Roles and create roles (they are automatically included in tokens)
App Roles (Recommended Approach)ΒΆ
- Navigate to App Roles in your App Registration
- Create roles with semantic names:
2. Environment VariablesΒΆ
# Basic EntraID SSO
SSO_ENTRA_ENABLED=true
SSO_ENTRA_CLIENT_ID=your-client-id
SSO_ENTRA_CLIENT_SECRET=your-secret
SSO_ENTRA_TENANT_ID=your-tenant-id
# Role Mapping Configuration
SSO_ENTRA_GROUPS_CLAIM=groups # or "roles" for app roles
SSO_ENTRA_DEFAULT_ROLE=viewer
SSO_ENTRA_SYNC_ROLES_ON_LOGIN=true
SSO_ENTRA_GRAPH_API_ENABLED=true
SSO_ENTRA_GRAPH_API_TIMEOUT=10
SSO_ENTRA_GRAPH_API_MAX_GROUPS=0 # 0 = unlimited
# Admin Groups (Object IDs or App Role names)
SSO_ENTRA_ADMIN_GROUPS=["a1b2c3d4-1234-5678-90ab-cdef12345678","Admin"]
# Group to Role Mapping (JSON format)
SSO_ENTRA_ROLE_MAPPINGS={"e5f6g7h8-1234-5678-90ab-cdef12345678":"developer","i9j0k1l2-1234-5678-90ab-cdef12345678":"team_admin","Developer":"developer","TeamAdmin":"team_admin","Viewer":"viewer"}
3. Provider Metadata (Database Configuration)ΒΆ
You can also configure role mappings in the SSO provider metadata:
{
"groups_claim": "roles",
"graph_api_enabled": true,
"graph_api_timeout": 10,
"graph_api_max_groups": 0,
"role_mappings": {
"Admin": "platform_admin",
"Developer": "developer",
"TeamAdmin": "team_admin",
"Viewer": "viewer"
}
}
Usage ExamplesΒΆ
Example 1: Using App Roles (Recommended)ΒΆ
EntraID Configuration:
- App Roles:
Admin,Developer,Viewer - Token includes
rolesclaim
Environment Variables:
SSO_ENTRA_GROUPS_CLAIM=roles
SSO_ENTRA_ADMIN_GROUPS=["Admin"]
SSO_ENTRA_ROLE_MAPPINGS={"Developer":"developer","Viewer":"viewer"}
SSO_ENTRA_DEFAULT_ROLE=viewer
Result:
- User with
Adminrole βplatform_admin(global scope) - User with
Developerrole βdeveloper(team scope) - User with
Viewerrole βviewer(team scope) - User with no roles β
viewer(default)
Example 2: Using Security Groups (Object IDs)ΒΆ
EntraID Configuration:
- Security Groups with Object IDs
- Token includes
groupsclaim
Environment Variables:
SSO_ENTRA_GROUPS_CLAIM=groups
SSO_ENTRA_ADMIN_GROUPS=["a1b2c3d4-1234-5678-90ab-cdef12345678"]
SSO_ENTRA_ROLE_MAPPINGS={"e5f6g7h8-1234-5678-90ab-cdef12345678":"developer","i9j0k1l2-1234-5678-90ab-cdef12345678":"viewer"}
Result:
- User in group
a1b2c3d4-...βplatform_admin - User in group
e5f6g7h8-...βdeveloper - User in group
i9j0k1l2-...βviewer
Example 3: Mixed ApproachΒΆ
EntraID Configuration:
- Both Security Groups and App Roles
- Token includes both
groupsandrolesclaims
Environment Variables:
SSO_ENTRA_GROUPS_CLAIM=groups
SSO_ENTRA_ADMIN_GROUPS=["Admin","a1b2c3d4-1234-5678-90ab-cdef12345678"]
SSO_ENTRA_ROLE_MAPPINGS={"Developer":"developer","e5f6g7h8-1234-5678-90ab-cdef12345678":"team_admin"}
Role SynchronizationΒΆ
On User CreationΒΆ
When a new user logs in via EntraID SSO:
- User info is extracted including groups
- Groups are mapped to roles via
_map_groups_to_roles() - Roles are assigned via
_sync_user_roles() - User is created with
is_adminflag if in admin groups - RBAC roles are assigned with
grant_source='sso'(self-granted by the user)
On User LoginΒΆ
When an existing user logs in:
- User info is updated (name, provider, etc.)
-
If
sso_entra_sync_roles_on_login=true: -
Current groups are extracted
- Groups are mapped to roles
- Old SSO-granted roles are revoked if no longer in groups
- New roles are assigned based on current groups
Manual Role ManagementΒΆ
- Admins can manually assign additional roles via the Admin UI
- Manually assigned roles (without
grant_source='sso') are preserved - Only SSO-granted roles (
grant_source='sso') are synchronized on login
Token ClaimsΒΆ
Groups Claim FormatsΒΆ
EntraID can return groups in different formats:
-
Object IDs (default):
-
Group Names (requires configuration):
-
App Roles:
Token Size ConsiderationsΒΆ
EntraID has a token size limit (~200 groups). When a user belongs to more groups than can fit in the token, EntraID returns a "group overage" indicator (_claim_names/_claim_sources) instead of the actual groups array.
ContextForge behavior on overage: 1. Detects the overage claim in the ID token 2. Calls POST https://graph.microsoft.com/v1.0/me/getMemberObjects 3. Uses returned group IDs for admin checks and RBAC mappings 4. Logs retrieval details (Retrieved {count} groups from Graph API for {user})
If Graph fallback fails, login still succeeds with safe defaults (no group-based elevation).
Example warning log:
Group overage detected for user user@example.com - token contains too many groups (>200).
Attempting Microsoft Graph fallback to resolve complete group membership.
Prevent overage at the source (preferred):
- Use App Roles (Recommended) β App roles are always included in the token and not subject to overage limits
- Azure group filtering β In Azure Portal β App Registration β Token Configuration, filter groups to only include specific security groups
- Claims transformation β Use Azure claims mapping policies to reduce group claims
- Direct group assignment β Assign groups directly to the application instead of user-level membership
Runtime fallback (when overage still occurs):
SSO_ENTRA_GRAPH_API_ENABLED=true(default)SSO_ENTRA_GRAPH_API_TIMEOUT=10(adjust for network latency)SSO_ENTRA_GRAPH_API_MAX_GROUPS=0for unlimited (or set a cap for large tenants)- Ensure delegated
User.Readpermission is granted to the app (required for/me/getMemberObjects)
Reference: Microsoft Groups Overage Claim
Security ConsiderationsΒΆ
Group ID vs Name MappingΒΆ
- Object IDs: Stable but not human-readable
- Group Names: Readable but can change
- App Roles: Stable, semantic, and recommended
Best PracticesΒΆ
- Use App Roles for stable, semantic mappings
- Limit admin groups to minimize security risk
- Enable role sync to keep permissions current
- Audit role assignments via permission audit logs
- Test mappings before production deployment
TroubleshootingΒΆ
Issue: Users not getting rolesΒΆ
Check:
- Token includes
groupsorrolesclaim SSO_ENTRA_GROUPS_CLAIMmatches claim name in token- Group IDs/names match
SSO_ENTRA_ROLE_MAPPINGS - Roles exist in ContextForge (check via Admin UI)
Debug:
# Check user's SSO metadata
SELECT sso_metadata FROM pending_user_approval WHERE email='user@example.com';
# Check user's current roles
SELECT r.name, ur.scope, ur.granted_by
FROM user_roles ur
JOIN roles r ON ur.role_id = r.id
WHERE ur.user_email='user@example.com';
Issue: Admin users not getting admin accessΒΆ
Check:
- User's group is in
SSO_ENTRA_ADMIN_GROUPS - Group ID/name matches exactly (case-insensitive)
is_adminflag is set on user record
Debug:
# Check user's admin status
SELECT email, is_admin, auth_provider FROM email_users WHERE email='user@example.com';
Issue: Roles not syncing on loginΒΆ
Check:
SSO_ENTRA_SYNC_ROLES_ON_LOGIN=true- User has groups in token
- No errors in application logs
Debug:
# Check application logs for role sync messages
grep "Assigned SSO role" /var/log/mcpgateway.log
grep "Revoked SSO role" /var/log/mcpgateway.log
Issue: Group overage - user has too many groupsΒΆ
Symptoms:
- User doesn't receive expected roles
- Application logs show:
Group overage detected for user ... token contains too many groups (>200)
Cause: User belongs to more than ~200 security groups. EntraID cannot fit all groups in the token and instead includes a "groups overage" indicator.
Solutions (prevent overage):
- Use App Roles (Recommended) β Not subject to token size limits
- Configure group filtering β In Azure Portal, limit which groups are included in the token
- Assign groups to the application β Use application-assigned groups instead of user memberships
Solutions (runtime fallback):
- Ensure
SSO_ENTRA_GRAPH_API_ENABLED=true - Increase
SSO_ENTRA_GRAPH_API_TIMEOUTif Graph calls are timing out - Raise or disable
SSO_ENTRA_GRAPH_API_MAX_GROUPSif group lists are being truncated
Debug:
# Check for overage warnings in logs
grep "Group overage detected" /var/log/mcpgateway.log
# Verify Graph fallback retrieval
grep "Retrieved .* groups from Graph API" /var/log/mcpgateway.log
See Token Size Considerations for detailed solutions.
Migration GuideΒΆ
Migrating from Admin-Only to RBACΒΆ
If you previously used only the is_admin flag:
-
Identify current admin users:
-
Configure admin groups:
-
Configure role mappings for non-admin users:
-
Enable role sync:
-
Test with non-admin users first
- Roll out to all users
API ReferenceΒΆ
Configuration FieldsΒΆ
| Field | Type | Default | Description |
|---|---|---|---|
sso_entra_groups_claim | str | "groups" | JWT claim for groups |
sso_entra_admin_groups | list[str] | [] | Groups granting platform_admin |
sso_entra_role_mappings | dict[str,str] | {} | Map groups to roles |
sso_entra_default_role | str | None | Default role for unmapped users (None = no automatic role) |
sso_entra_sync_roles_on_login | bool | true | Synchronize mapped roles at every login |
sso_entra_graph_api_enabled | bool | true | Enable Microsoft Graph fallback for overage claims |
sso_entra_graph_api_timeout | int | 10 | Timeout (seconds) for Graph fallback request |
sso_entra_graph_api_max_groups | int | 0 | Max Graph groups retained (0 = unlimited) |
MethodsΒΆ
_normalize_user_info(provider, user_data)ΒΆ
Extracts user info and groups from EntraID token.
Returns:
{
"email": str,
"full_name": str,
"provider": "entra",
"groups": list[str] # Group IDs or role names
}
_map_groups_to_roles(user_email, user_groups, provider)ΒΆ
Maps EntraID groups to ContextForge roles.
Returns:
_sync_user_roles(user_email, role_assignments, provider)ΒΆ
Synchronizes user's role assignments.
Side Effects:
- Revokes old SSO-granted roles
- Assigns new roles from current groups
- Commits changes to database
Comparison with Other ProvidersΒΆ
| Feature | Keycloak | EntraID | GitHub | |
|---|---|---|---|---|
| Group extraction | β | β | β | β |
| Role mapping | β | β | β | β |
| Admin groups | β | β | β | β |
| Role sync on login | β | β | β | β |
| Custom claim name | β | β | N/A | N/A |
Future EnhancementsΒΆ
- Team-scoped role assignments based on groups
- Role mapping UI in Admin panel
- Group-to-team mapping
- Conditional role assignment based on additional claims
- Role assignment expiration based on group membership duration
Design DecisionsΒΆ
This section documents key architectural decisions. See ADR-034 for full details.
Admin Status SynchronizationΒΆ
Behavior: SSO can only upgrade is_admin to True, never downgrade.
| Scenario | Behavior |
|---|---|
| User gains admin group | is_admin upgraded to True |
| User loses admin group | is_admin preserved (not revoked) |
| Manual admin grant | Preserved across all SSO logins |
Rationale:
- Manual admin grants via Admin UI/API are intentional decisions
- RBAC roles (
platform_admin) already handle group-based role sync with revocation - To revoke admin access, use the Admin UI/API explicitly
Note: The is_admin flag provides a platform-level bypass. RBAC roles provide granular, auditable access control with proper granted_by tracking.
Configuration Precedence (Bootstrap)ΒΆ
Behavior: Smart merge - environment provides defaults, database values preserved.
| Key Source | Behavior |
|---|---|
| Only in env config | Applied (new features) |
| Only in database | Preserved (Admin API changes) |
| In both | Database wins |
Example:
Env: {"groups_claim": "groups", "new_feature": true}
DB: {"groups_claim": "custom", "sync_roles": false}
Result: {"groups_claim": "custom", "new_feature": true, "sync_roles": false}
To change a key that exists in database:
- Use Admin API to update the provider
- Or delete the provider and restart (bootstrap recreates from env)
ID Token Trust ModelΒΆ
Groups and roles are extracted from the id_token JWT received from the token endpoint. The token is trusted without signature validation because:
- Received directly from IdP over HTTPS after valid code exchange
- OAuth flow (state, PKCE) already validated
- Consistent with standard OAuth client library behavior
Important: Configure group claims in the ID token in Azure Portal. The OIDC userinfo endpoint does not return groups.
ReferencesΒΆ
- Microsoft identity platform UserInfo endpoint - Why groups aren't in userinfo
- Configure optional claims - Group limits and token configuration
- ID token claims reference - Groups overage claim details
- Configure group claims for applications - Advanced group claim configuration
SupportΒΆ
For issues or questions:
- Check application logs for error messages
- Verify EntraID token configuration in Azure Portal
- Test with a single user before rolling out
- Consult the troubleshooting section above
- Open an issue on GitHub with logs and configuration (redact secrets)