External groups

External memberships can restrict authentication and synchronize local groups. Both capabilities are opt-in. Extraction does not grant local permissions.

Enable extraction

For OpenID Connect, Keycloak, and Okta, select a literal claim name:

SOCIAL_AUTH_OIDC_GROUPS_KEY = 'groups'
SOCIAL_AUTH_KEYCLOAK_GROUPS_KEY = 'groups'
SOCIAL_AUTH_OKTA_OAUTH2_GROUPS_KEY = 'groups'
SOCIAL_AUTH_OKTA_OPENIDCONNECT_GROUPS_KEY = 'groups'

Okta OAuth2 reads UserInfo; Okta OpenID Connect uses the validated ID token first, then UserInfo with a matching subject. Configure Okta to issue the claim. Org authorization servers require the groups scope; custom authorization servers require any scope associated with the claim, if any. See Okta for backend-specific scopes and a Django synchronization example.

For Azure, use the setting corresponding to the selected authentication backend:

Azure group extraction settings

Backend class

Setting

AzureADOAuth2

SOCIAL_AUTH_AZUREAD_OAUTH2_GROUPS_KEY

AzureADOAuth2V2

SOCIAL_AUTH_AZUREAD_OAUTH2_V2_GROUPS_KEY

AzureADTenantOAuth2

SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_GROUPS_KEY

AzureADV2TenantOAuth2

SOCIAL_AUTH_AZUREAD_V2_TENANT_OAUTH2_GROUPS_KEY

AzureADB2COAuth2

SOCIAL_AUTH_AZUREAD_B2C_OAUTH2_GROUPS_KEY

Set the selected Azure setting to 'roles' for Microsoft Entra application roles, or 'groups' for group identifiers. Configure the provider to issue the selected claim. OIDC uses the validated ID token first, then UserInfo with a matching subject. Claim names are literal keys; nested claim paths are not supported.

GitLab, MediaWiki, and Discourse use GROUPS_ENABLED instead:

SOCIAL_AUTH_GITLAB_GROUPS_ENABLED = True
SOCIAL_AUTH_GITLAB_GROUPS_IDENTIFIER = 'full_path'

With GROUPS_ENABLED = True, GitLab requests read_api automatically unless read_api or api is already included in the requested scopes. Enable the matching permission in the GitLab OAuth application’s settings; read_user alone cannot retrieve groups.

GitLab returns full group paths by default. Set GROUPS_IDENTIFIER = 'id' for stable numeric IDs, represented as strings. Paths are readable but renames, moves, and reuse can change authorization. Memberships are fetched from every page on the configured API_URL; visible groups without membership do not qualify. Insufficient scope or a failed page aborts authentication.

For SAML, configure each identity provider independently:

SOCIAL_AUTH_SAML_ENABLED_IDPS = {
    'company': {
        # Existing IdP configuration goes here.
        'attr_groups': 'https://example.com/claims/groups',
        'allow_groups': ['translators', 'reviewers'],
        'groups_map': {'translators': ['Translators']},
    },
}

The complete SAML attribute is read; a singleton string is accepted.

Missing and empty memberships

get_user_groups(response) returns a list of exact, nonempty string identifiers, with duplicates removed. None means extraction is disabled; [] means the provider reported no memberships.

A missing configured claim fails authentication by default. Providers that omit the claim for users without assignments can explicitly enable:

SOCIAL_AUTH_OIDC_GROUPS_MISSING_AS_EMPTY = True

SAML uses groups_missing_as_empty inside the IdP configuration. Malformed claims always fail. Entra group overage also fails when reading groups, even with this option enabled. There is no Microsoft Graph fallback; prefer application roles or configure application-scoped groups.

Only trust group claims from the intended issuer and tenant. In particular, restrict Azure’s common endpoint appropriately before granting local access.

Restrict authentication

The existing auth_allowed pipeline step enforces ALLOW_GROUPS:

SOCIAL_AUTH_OIDC_ALLOW_GROUPS = ['translators', 'reviewers']

Membership in any listed group qualifies. Email/domain restrictions still apply. An empty allow list imposes no group restriction. A nonempty allow list requires enabled extraction. SAML uses per-IdP allow_groups.

Existing SOCIAL_AUTH_CAS_ALLOW_GROUPS settings keep their behavior without new extraction settings or pipeline steps. CAS reads groups when a nonempty allow list or group mapping is configured, or when SOCIAL_AUTH_CAS_GROUPS_ENABLED = True explicitly enables extraction for a custom pipeline. With group handling disabled, CAS ignores the group attribute and returns None. When enabled, CAS validates the membership list and continues to treat a missing claim as empty membership.

Synchronize Django groups

Map external identifiers to lists of existing Django group names:

SOCIAL_AUTH_OIDC_GROUPS_MAP = {
    'translators': ['Translators'],
    'reviewers': ['Reviewers', 'Translators'],
}

Append social_core.pipeline.user.sync_groups after user creation and all application authentication checks in SOCIAL_AUTH_PIPELINE. Keep the existing social_details and auth_allowed steps; no extra extraction or restriction steps are required. For example, extend the standard pipeline:

from social_core.pipeline import DEFAULT_AUTH_PIPELINE

SOCIAL_AUTH_PIPELINE = (
    *DEFAULT_AUTH_PIPELINE,
    'social_core.pipeline.user.sync_groups',
)

The mapping targets define the memberships managed by this provider. Desired memberships are added and obsolete managed memberships removed atomically. Other groups are preserved. Manual membership in managed groups is replaced at the next authentication. Empty memberships remove all managed memberships.

Django synchronization uses a transaction so a failed membership update rolls back the changes. It does not lock user or group rows. Concurrent authentications with different membership snapshots can interleave and leave a combination of their memberships. Applications requiring serialized synchronization should override strategy.sync_user_groups and coordinate membership updates using their own locking policy. Synchronization does not prevent administrators from renaming or deleting mapped groups concurrently.

An empty mapping disables synchronization. Unknown external groups grant nothing; use ALLOW_GROUPS to restrict login independently. Missing local groups, invalid configuration, disabled extraction with an enabled mapping, and competing provider/IdP ownership of a local group cause errors before membership changes. No groups are created and staff/superuser flags are not modified. The default Django implementation requires auth.Group and an automatically created membership table. Custom group models and explicit intermediary models must override strategy.sync_user_groups to supply their application-specific membership behavior. These unsupported relations are rejected before any membership changes when synchronization is configured.

Synchronization runs during authentication, including registration and linking. It does not provide background revocation, account deactivation, or SCIM provisioning. Association-only backends do not synchronize memberships.

Custom strategies and pipelines

social_details exposes memberships as the top-level pipeline argument groups, separate from profile details. Partial pipelines preserve this argument. There is no automatic external-group snapshot in association extra_data.

Override strategy.sync_user_groups(user, groups, *, backend, response, **kwargs) for application-specific behavior. DjangoStrategy supplies the standard Django implementation. BaseStrategy rejects configured synchronization without an implementation. Application strategies should validate all targets before changing memberships and retain their own audit, transaction, and permission-cache behavior.

social_core.groups.group_sync_targets(backend, groups, response) returns the desired and managed sets of local target identifiers, validating mapping syntax and ownership across configured providers. SAML resolves its mapping from response['idp_name']. Strategies interpret local target identifiers; for example, Weblate uses team IDs instead of Django group names.

MediaWiki and Discourse no longer return groups inside details. Existing custom consumers should enable extraction and use the groups pipeline argument. This prevents profile updates from assigning a many-to-many field.