Configuring SSO Roles¶
How to map your IdP groups to CampusCore roles so that signed-in users automatically get the right access. Run through this once when standing up SSO for a new institution, then revisit when their group structure changes.
What you need from the institution¶
Before you start, get from the institution's IT contact:
- The exact name of the IdP claim that contains group/role membership. Common names:
groups,member_of,roles,urn:oid:1.3.6.1.4.1.5923.1.5.1.1. Microsoft Entra (Azure AD) typically usesgroups. Active Directory Federation Services often usesmember_ofwith LDAP DNs. - The exact group values they want to map to CampusCore roles. For LDAP-style providers this means full DNs like
CN=Advisors,OU=Staff,DC=vsu,DC=edu. For OIDC providers this is usually a short name likeadvisors. - Whether their group values are case-sensitive. Active Directory groups are case-insensitive; OIDC group names are usually case-sensitive.
Step 1 - Configure the SSO claim mapping¶
- Log into Django admin at
https://<deployment>/cc_admin/django/as a CampusCore platform admin (superuser). - Open SSO Providers → click the institution's provider.
- Scroll to the Map your IdP claims to CampusCore fields section.
- Fill in the structured claim columns. Each value is the claim name on the IdP side. The table below is the complete, fixed list of claims CampusCore reads. Institutions choose which claims to release, never which fields exist. For the identity fields (email, first name, last name), blank means the protocol's standard claim names - only fill a column when the institution's IdP uses a non-standard name.
| CampusCore field | What it's for | Example values |
|---|---|---|
| Email claim | The user's email address. Blank uses the standard names. | mail, EmailAddress (SAML); email (OIDC) |
| External ID claim | SAML only - optional. The assertion attribute holding the stable subject ID. Blank links accounts by the NameID. Set before go-live: changing it on a provider with existing users changes how their accounts are linked. OIDC always links by the sub claim. |
urn:oasis:names:tc:SAML:attribute:subject-id, oid |
| First name claim | Optional. Blank uses the standard names. | givenName, firstName (SAML); given_name (OIDC) |
| Last name claim | Optional. Blank uses the standard names. | sn, lastName (SAML); family_name (OIDC) |
| Groups/roles claim | Where to look for the user's groups. Leave blank to disable role sync from this provider. | groups, member_of, roles |
| Department claim | Optional. Copied to the user's profile on every login. | department, ou |
| Student ID claim | Optional. Copied to the user's profile on every login. | employeeNumber, student_id |
-
(Optional) Set Default role to whichever CampusCore role users should get when they sign in but don't match any group mapping. Leave it blank to grant nothing to unmatched users - the right choice when, say, alum accounts also sign in through the same IdP.
-
Save.
Step 2 - Create the group mappings on the Roles page¶
The mappings are institution self-service: any workspace admin creates them in the app, role by role.
- In the app, open Settings → Roles & Permissions.
- Expand the role you want the institution's group to grant, and click Map an IdP group.
- Provider: The institution's SSO provider. A provider without a groups claim shows an inline warning - go back to Step 1 if you see it.
- IdP group: The exact value the IdP returns. Paste it verbatim from what the institution gave you.
- Match mode: Pick
Exact matchfor case-sensitive providers (most OIDC). PickCase-insensitivefor Active Directory and other case-insensitive providers. - One IdP group can map to multiple roles (add the same group under each role), and multiple IdP groups can map to the same role (add several rules under one role) - useful when, e.g., both
advising_freshmanandadvising_seniorAD groups should grantadvisor. - A role flagged Unassigned clears the moment it gets a rule or a manual grant.
The same rules are also editable as a set on the provider's form under Settings → Single Sign-On; the Roles page is the role-centric view of the same table. Deleting a rule that is a role's last way of being assigned, while enabled features are scoped to that role, asks for an explicit confirmation naming the affected features.
Step 3 - Verify with a test user¶
- Find a real user in one of the mapped groups and have them log in.
- Open Settings → User Management and search for their email; the role should appear on their row with a "Synced from SSO" tooltip.
- Open Settings → Roles & Permissions and expand the role: the member counts show the new SSO member, and the mapping is listed under the provider.
- Open Settings → Audit Log and look for the most recent
role_syncentry for that user - it shows the added/removed role IDs.
Common gotchas¶
- The user logs in but no roles get assigned. Most often the
Groups/roles claimis misspelled or the institution sends groups under a nested key. CampusCore looks for the claim in the OIDCuserinfo/id_tokenpayload, at the top level, and under SAMLattributes/saml_attributes, so make sure the configured claim name matches one of those. Check the AuditLog for arole_syncentry whose details saymappings_matched: false, which means CampusCore saw groups but none matched a mapping. - You want to know from the logs why a login granted nothing. A login that ends without changing any role usually writes one INFO line,
RoleSync no-op: user_id=<id> reason=<word>, and the word at the end says which case it was. Search the application logs forRoleSync no-op: no_account- the login carried no linked SSO account, so there were no claims to read groups from.no_groups_claim_configured- the provider's Groups/roles claim is blank, which switches role sync off for that provider (Step 1).empty_groups_in_assertion- the configured claim yielded no groups, so existing SSO roles were deliberately preserved. Either the IdP really sent an empty claim or the claim name is misspelled; only the first also writes aRoleSyncService: empty groups claim in assertionwarning, so no warning points at the spelling.no_mappings_matched- CampusCore saw groups and none of them matched a mapping.exception- the sync raised, the failure was contained, and the login went through with roles untouched.
Two cases write no line at all. A login whose provider does not resolve to a configured SSO provider is routine rather than a role-sync problem, and a login that matched its mappings and changed nothing is the ordinary steady state.
- Active Directory DN strings get split into multiple groups. They shouldn't - RoleSyncService detects = in the string and treats it as a single LDAP DN. If you see multiple roles being granted from one DN, file a bug.
- A workspace's Settings close right after somebody signs in. That login's groups no longer mapped to institution_admin, and nobody else held the role, so the sync removed the last administrator grant. CampusCore does not block that login - the identity provider is the source of truth for SSO grants - but it records and announces the loss. Look for a role_sync_removed_last_admin entry in the Audit Log, and for the ⚠ SSO sync removed the last administrator message in #client_workflow_runs. To recover, grant institution_admin to somebody from the Django admin, then fix the provider's group mapping so the next login keeps it.
- A user's roles disappear after a known-good login. The IdP might be returning an empty groups array. CampusCore deliberately does NOT wipe roles when the assertion's groups claim is empty (this would lock people out during transient IdP bugs). Check the IdP-side group membership.
- You add a new mapping but a user doesn't get the new role. Roles only sync at login time. The user has to log out and log back in.
- A platform admin grants a role manually and the next SSO login wipes it. Manual grants are preserved across SSO syncs as long as they were created with source='manual' (which the Django admin form does automatically). If you're scripting it, set source='manual' explicitly.
- SAML logins start failing with "signature validation failed" after the institution rotates its IdP certificate. For providers configured with a metadata URL, CampusCore caches the IdP metadata (including the signing cert) for 15 minutes, so a rotation self-heals within that window. To pick it up immediately, re-save the provider, or select it in Django admin and run the "Refresh IdP metadata" action. Providers configured with a pasted certificate need the new cert entered by hand.
Reference: how role sync runs under the hood¶
On every successful SSO login, CampusCore's user_logged_in signal receiver calls sync_user_roles_from_sso in role_sync_service. The service:
- Reads
claim_groupsfrom the SSO provider config. - Extracts the group values from the assertion, normalizing across four shapes (list of strings, comma-separated string, Azure-AD-style list of dicts, LDAP DN string).
- Looks up matching
SSOGroupMappingrows. - Computes the diff against the user's existing
source='sso'UserRole rows. - Applies the diff under
select_for_updateso concurrent logins don't race. - Logs a
role_syncaudit entry containing the SSOGroupMapping IDs (never the raw group strings - avoids PII leak). - Cycles the session key on role change.
If anything fails, the failure is logged but the login is allowed to proceed. A broken role sync must never lock users out.
For full design details see docs/project/rbac-and-feature-flags.md.