Skip to content

IdP → Organization Mapping ​

This guide shows how to drive Hegemony organization membership from an identity provider, so that logging in with the right group automatically grants the right role in the right org — no manual invites. It supports Keycloak-local users and any brokered upstream IdP (Microsoft Entra ID / Azure AD, Okta, Google, ADFS, another Keycloak), including several IdPs at once.

For the underlying tenancy model, see Multi-Tenancy; for the auth layer, see Authentication & Authorization.

The model in one picture ​

Hegemony follows the industry-standard federated-identity pattern used by Grafana, GitLab, and Vault: the application owns the org model; the IdP is only an authentication source.

text
  Azure AD ┐
  Okta     ┼─▶ Keycloak (broker, single realm) ──JWT with `groups`──▶ Hegemony
  Local    ┘        maps upstream groups → KC groups           maps `groups` → org role
  • Keycloak is the single trust anchor. Every user — local or brokered from an upstream IdP — ends up as a Keycloak user and receives a Keycloak-signed JWT. Hegemony validates exactly one issuer.
  • The JWT carries a groups claim (a list of group paths/ids).
  • Hegemony stores IdP mappings per org: "a token whose groups claim contains X grants role R in this org."
  • On each login, Hegemony reconciles the user's memberships from their claims (JIT provisioning). The app database stays the source of truth for who is in which org.

Because membership is derived from a claim value — not from which upstream IdP issued it — adding a second or third IdP needs zero Hegemony changes: broker it in Keycloak, make sure its users land in the right groups, done.

What happens on login ​

On the first authenticated request of a session, the RBAC middleware (apps/api/auth/rbac_middleware.py) runs, best-effort and never fatal:

  1. Upsert the local users row by sub.
  2. If HEGEMONY_ORG_IDP_SYNC=true, call reconcile_idp_memberships() (apps/api/services/org_provisioning.py):
    • Read the configured claim (default groups) from the token.
    • Match every active IdP mapping whose value is present in the claim; collect the desired {org → strongest role}.
    • Add / upgrade idp-sourced memberships to match.
    • Never touch manual-sourced memberships (an explicit grant in the UI/API always wins over the IdP).
    • In authoritative mode (default), remove idp-sourced memberships the claims no longer justify.
  3. Otherwise (or additionally, as a fallback for users with no memberships), the default-org auto-join applies (see Multi-Tenancy → provisioning).

Each membership row records a source (manual or idp) so the two systems never fight. The Org Members admin page shows idp members read-only and points you to IdP Mappings to manage them.

Settings ​

SettingEnv varDefaultPurpose
org_idp_syncHEGEMONY_ORG_IDP_SYNCfalseMaster switch: derive org membership from IdP group claims on login
org_idp_group_claimHEGEMONY_ORG_IDP_GROUP_CLAIMgroupsJWT claim to read group values from
org_idp_sync_authoritativeHEGEMONY_ORG_IDP_SYNC_AUTHORITATIVEtrueWhen true, revoke idp memberships the claims no longer grant (mirror). When false, only ever add (additive)

Recommended rollout: leave org_idp_sync=false while you author mappings, then flip it on. Start additive (authoritative=false) to observe, then switch to authoritative once mappings are trusted so leaving a group actually removes access.

manual grants are always preserved regardless of mode — authoritative sync only reconciles rows it created.

Step 1 — Keycloak must emit the group claim ​

Hegemony reads groups from the access token. The bundled demo realm already does this; for your own realm add a Group Membership mapper to the client scope the hegemony-ui / hegemony-automation clients use (usually a dedicated roles/groups scope or the client's dedicated mappers):

  • Mapper type: Group Membership (oidc-group-membership-mapper)
  • Token Claim Name: groups
  • Full group path: ON → values look like /NetOps, /Admins (turn OFF to emit bare names like NetOps — then use bare names as the mapping value)
  • Add to ID token / Access token / userinfo: ON

The shared application realm mapper (deploy/compose/keycloak/realm-export.json):

json
{
  "name": "groups",
  "protocol": "openid-connect",
  "protocolMapper": "oidc-group-membership-mapper",
  "config": {
    "full.path": "true",
    "id.token.claim": "true",
    "access.token.claim": "true",
    "claim.name": "groups",
    "userinfo.token.claim": "true"
  }
}

Verify the claim is present by decoding a user's access token (jwt.io, or Authenticate → inspect in the browser dev tools) and confirming a "groups": ["/NetOps", ...] array.

Step 2 — Create IdP mappings in Hegemony ​

In the UI: Settings → Organizations → (row) → IdP Mappings → Add Mapping. Each mapping is claim + value → role:

FieldExampleNotes
Token claimgroupsUsually groups; must match org_idp_group_claim. Immutable after creation
Claim value/NetOpsThe exact value your IdP emits (full path, bare name, or an Azure AD group object id). Immutable
RoleoperatorThe org role granted. When a user matches several mappings for one org, the strongest role wins
ActiveonDisabled mappings are ignored during reconciliation

Equivalent REST call (org admin or platform admin):

bash
curl -X POST "$HEGEMONY_URL/api/orgs/$ORG/idp-mappings" \
  -H "Authorization: Bearer $TOKEN" \
  -H "X-Org-Id: $ORG" \
  -H 'Content-Type: application/json' \
  -d '{"claim": "groups", "value": "/NetOps", "role": "operator", "is_active": true}'

(claim, value, org) is unique — a duplicate returns 409. List / patch / delete live under the same /api/orgs/{org_ref}/idp-mappings prefix.

Brokering external IdPs in Keycloak ​

The Hegemony side is identical for every IdP; only the Keycloak brokering differs. The goal is always the same: the brokered user ends up in a Keycloak group that appears in the groups claim.

Microsoft Entra ID (Azure AD) — OIDC ​

  1. In Entra ID → App registrations, register an app for Keycloak. Redirect URI: https://<keycloak>/realms/<realm>/broker/oidc/endpoint. Add a client secret.
  2. Under Token configuration, add the groups optional claim (emit Group ID — Entra sends group object ids, not names).
  3. In Keycloak → Identity providers → Add OpenID Connect v1.0:
    • Discovery URL: https://login.microsoftonline.com/<tenant-id>/v2.0/.well-known/openid-configuration
    • Client ID / secret from step 1; default scopes openid profile email.
  4. Surface the Entra groups in Keycloak's own groups claim, one of:
    • Recommended — map into Keycloak groups. Add an IdP mapper of type Advanced Claim to Group (or Claim to Group) that matches an Entra group id in the incoming groups claim and assigns a Keycloak group (e.g. /NetOps). Then use /NetOps as the Hegemony mapping value — clean, human-readable, and identical to local users.
    • Or pass through. Add an Attribute Importer copying the incoming groups claim to a user attribute and emit it via a User Attribute mapper. Then use the raw Entra group object id as the Hegemony mapping value (opaque but works).

Either way Hegemony only sees Keycloak's groups claim; the choice is purely whether mapping values are readable names or Entra ids.

Generic OIDC (Okta, Google, another Keycloak, …) ​

Same shape as Entra: add an OpenID Connect v1.0 identity provider with the vendor's discovery URL and client credentials, ensure the vendor emits a groups/roles claim, and add a Claim to Group IdP mapper so brokered users land in the matching Keycloak groups.

SAML ​

Add a SAML v2.0 identity provider, then a SAML Attribute to Group mapper keyed on the assertion's group/memberOf attribute. From Hegemony's perspective nothing changes — it still reads Keycloak's groups claim.

Running several IdPs at once ​

Because Hegemony keys on the claim value, not the issuer, multiple IdPs coexist naturally:

  • Broker each IdP in the same Keycloak realm.
  • Normalize every IdP's groups into a shared Keycloak group taxonomy (/NetOps, /Auditors, /Admins, …) using per-IdP Claim to Group mappers. Employees from Entra and contractors from Okta can both land in /NetOps.
  • Write Hegemony IdP mappings once against that taxonomy.

If you deliberately want IdP-specific access (e.g. only Entra users become org admins), keep the values distinct (map Entra /NetOps and Okta /ext-netops to different roles/orgs).

Demo walk-through ​

The demo-data realm ships the groups mapper and three groups (/Admins, /NetOps, /Auditors) with demo users assigned. To see cross-org, claim-driven membership end to end:

bash
# Bring up the demo stack (Keycloak + API + UI + OpenBao + Temporal).
# See docs/demo.md for prerequisites (plugin wheels, demo data).
task compose:demo:up

Then, as the platform admin (admin / hegemony):

  1. Settings → Organizations → Add Organization: create netops-team ("NetOps Team").

  2. Open its IdP Mappings and add:

    • groups = /NetOps → operator
    • groups = /Admins → admin
  3. Enable sync (additive first is fine for the demo):

    bash
    # set on the API service, then restart it
    HEGEMONY_ORG_IDP_SYNC=true
  4. Log in as operator / hegemony. That user is in /NetOps, so on login they are auto-provisioned into NetOps Team as operator. The Org Switcher now lists it; the membership shows source IdP on the Members page. viewer (in no group) gets no NetOps membership.

Add a second org and map /Auditors there to watch one user land in multiple orgs with different roles — all from group claims, no manual invites.

Troubleshooting ​

  • No memberships appear. Confirm HEGEMONY_ORG_IDP_SYNC=true, the token actually carries the claim (org_idp_group_claim, default groups), and the mapping value matches the claim exactly (full path vs bare name).
  • Brokered user has an empty groups claim. The Claim-to-Group / group mapper on the identity provider is missing or the upstream didn't send the groups claim (Entra: add the optional groups claim; large group counts fall back to the Graph "overage" — filter to app-assigned groups).
  • A user won't lose access after leaving a group. That is additive mode — set HEGEMONY_ORG_IDP_SYNC_AUTHORITATIVE=true for mirror semantics.
  • A manual grant is being "ignored" by sync. By design: manual memberships are never modified by IdP sync. Change or remove it on the Members page.
  • Role didn't change after editing a mapping. Reconciliation runs on the user's next login; existing sessions keep their current membership until they re-authenticate.

Released as open source under the AGPL-3.0-or-later license. Development is sponsored by Rexonix s.r.o.. Contact — [email protected].