Guide · SSO

The OIDC field nobody documents: groups_claim

You added SSO, wired "map the IdP's groups to my app's roles," pointed it at the groups claim, and shipped. Then everyone signs in as the default role, admins included. The mapping silently does nothing, and the reason is the one thing almost no tutorial mentions: OIDC providers do not agree on where they put groups.

If you hardcode the claim name groups, you have quietly hardcoded "works with Okta and Keycloak, breaks with everyone else."

Where each provider actually puts groups

ProviderClaimThe catch
OktagroupsYou must add a groups claim in the authorization server, and often request a groups scope. Not there by default.
KeycloakgroupsOnly if you add a group-membership mapper to the client scope.
Microsoft Entra IDgroupsOff by default: enable the groups optional claim. Returns object IDs, not names. Past ~200 groups you get an overage pointer and must call Graph.
AWS Cognitocognito:groupsAutomatic, but namespaced. Plain groups is always empty.
Auth0a namespaced claim, e.g. https://app.example/rolesAuth0 strips non-namespaced custom claims, so it can never be plain groups.
Googlenot emittedThe ID token has no groups. Use Workspace + the Cloud Identity / Admin SDK out of band.
GitHub (OAuth2)noneNo ID token. Org and team membership come from /user/orgs with the read:org scope.

A config that works for Cognito needs cognito:groups; Auth0 needs your namespaced URL; Entra needs an object-ID map plus a Graph fallback; Google needs a different mechanism entirely. One hardcoded string cannot cover that.

Two bugs hide behind "everyone gets the default role"

1. The claim name is wrong. You read token["groups"], the provider put them in cognito:groups, you get None, mapping falls through to the default.

2. The groups were never emitted. Even with the right claim name, several providers do not include groups unless you request the right scope or flip a setting. If you hardcode the scope to openid email profile, you can never ask for more. Both fail the same silent way: no error, everyone lands on the default role.

The fix: make the claim and the scope configurable

Do not bake the claim name into your code. Store it per provider, default it to groups, and let it be overridden:

# config per provider, not a constant
groups_claim = provider.get("groups_claim", "groups")   # cognito:groups, https://app/roles, ...
scope        = provider.get("scope") or DEFAULT_SCOPE     # so you can add "groups" for Okta

# read whatever claim this provider uses
groups = userinfo.get(groups_claim) or []
# first matching group wins, else default
role = next((role_map[g] for g in groups if g in role_map), default_role)

That is the whole difference between "SSO role mapping works" and "SSO role mapping silently doesn't": a groups_claim field and a scope override, per provider.

Debugging checklist when roles don't map

1. Decode the actual token. Log the decoded claims server-side (never the raw token) and look at what keys are really there. Usually the groups are under a name you did not expect, or missing.
2. If missing: the provider is not emitting them. Add the claim in its app config and request the scope it wants.
3. If present under another name: point groups_claim at it.
4. Entra past 200 groups: you get a _claim_names overage pointer, not groups. Call Graph memberOf, or use app-role assignments.
5. Google/GitHub: stop looking in the token. Google needs the directory API; GitHub needs the orgs API.

We hit exactly this building SSO for the Aggrete console, which is why the provider config exposes both a groups-claim and a scope override rather than assuming groups. If you are adding SSO to anything, make those two things configurable from day one.
Open source

Govern what your AI assistants can reach.

Aggrete is Apache-2.0 and runs on a laptop or a cluster.

Star on GitHub How it works