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 withno_mappings_matched— that means CampusCore saw groups but none matched a mapping. - Active Directory DN strings get split into multiple groups. They shouldn't —
RoleSyncServicedetects=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 user's roles disappear after a known-good login. The IdP might be returning an empty
groupsarray. 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, setsource='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 RoleSyncService.sync_user_roles_from_sso. 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.