Skip to Content
Identity & SSOSCIM 2.0 provisioning

SCIM 2.0 provisioning

Where federation is pull-mode (users land in EUnifyer the first time they sign in via your IdP), SCIM is push-mode: your IdP sends create / update / delete operations to EUnifyer the moment something changes in its directory. New hires exist in EUnifyer before day one; offboarded users lose access within minutes, not at next token refresh.

EUnifyer’s SCIM 2.0 surface is fully RFC 7644 / 7643 compliant: Users + Groups, PATCH, Bulk operations, the full filter grammar, ETag concurrency, and a tenant-namespaced extension URN for custom attributes.

What’s supported (current)

OperationStatus
POST /scim/v2/Users (create)
GET /scim/v2/Users/{id} (read)
GET /scim/v2/Users?filter=… (full grammar)
PUT /scim/v2/Users/{id} (full replace)
PATCH /scim/v2/Users/{id}✓: add/replace/remove with SCIM path syntax
DELETE /scim/v2/Users/{id}
/scim/v2/Groups (full CRUD + PATCH)
POST /scim/v2/Bulk (up to 100 ops, bulkId refs)
If-Match / ETag optimistic concurrency
active=false soft-disable
enterprise:User extension (department, manager, employeeNumber, costCenter)
extension:eunifyer:2.0:User (tenant custom attrs, round-tripped)
Group ↔ Role admin mapping screen
Filter operators: eq / ne / co / sw / ew / gt / ge / lt / le / pr
Logical operators: and, or, not, parentheses

Prerequisites

  • EUnifyer organisation admin account (you need the SCIM provisioning page).
  • An Entra ID tenant where you can configure provisioning on an enterprise application (Application Administrator role).
  • Your EUnifyer hostname, for example app.example.com.

Step 1: Generate a bearer token in EUnifyer

  1. In EUnifyer, open Admin → SCIM provisioning.
  2. Copy the SCIM 2.0 endpoint at the top of the page. It looks like https://app.example.com/scim/v2.
  3. Click Generate token, give it a recognisable name (e.g. Entra production tenant), confirm.
  4. Copy the token immediately. EUnifyer never stores it in plain text, you can only see it once. If you lose it, generate a new one and revoke the old.

The token is shaped scim_<random> so it is easy to spot in logs.

Step 2: Configure provisioning in Entra ID

  1. Sign in to the Microsoft Entra admin centre Applications → Enterprise applications.
  2. Either register a new enterprise application (Gallery → Non-gallery application → name it EUnifyer) or open the one you created for federation in the Entra ID guide.
  3. In the left nav, open Provisioning → Get started → Provisioning Mode = Automatic.
  4. Under Admin Credentials:
    • Tenant URL: paste the SCIM endpoint from Step 1 (e.g. https://app.example.com/scim/v2).
    • Secret Token: paste the bearer token from Step 1.
  5. Click Test connection. Entra calls GET /scim/v2/ServiceProviderConfig with the token; you should see a green success message. If it fails, check that the token has not been revoked and the URL is reachable from the public internet.

Step 3: Configure attribute mappings

Phase 1 ships with the minimal map. In Entra:

  1. Open Provisioning → Mappings → Provision Microsoft Entra ID Users.
  2. Leave the defaults, the fields below map cleanly. Remove the rest (Entra sends about 25 attributes by default, most of which we ignore; trimming them speeds up sync).
Entra ID attributeEUnifyer field
userPrincipalNameuserName (= EUnifyer email)
Switch([IsSoftDeleted], , "False", "True", "True", "False")active
givenNamename.givenName
surnamename.familyName
mailemails[type eq "work"].value
telephoneNumberphoneNumbers[type eq "work"].value
jobTitletitle
departmenturn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department
employeeIdurn:…enterprise:User:employeeNumber
managerurn:…enterprise:User:manager

Phase 1 does not apply manager recursively (we set it on the user but do not yet build the org chart). Setting it is harmless; it becomes useful once we surface the hierarchy in the UI.

Step 4: Scope users and start provisioning

  1. Open Provisioning → Settings → Scope.
  2. Pick Sync only assigned users and groups (recommended for production):** then assign people via Users and groups**. Or Sync all users and groups if you want everyone in the tenant.
  3. Set Provisioning Status to On.
  4. Click Save.

Entra performs an initial sync within ~40 minutes (Microsoft schedules this) and then incremental syncs every ~40 minutes after that. You can force a sync from Provisioning → Provision on demand.

Each created user lands in EUnifyer already federated to Entra (assuming you have set up federation). They appear in /admin/users with a status of pending invite and become active on first sign-in. There is no email, no manual step.

Verifying

  • In EUnifyer, open Admin → SCIM provisioning: the Last used column on your token updates as Entra calls in.
  • Audit events of the form scim.user.created / scim.user.updated / scim.user.deleted appear in Admin → Audit log with actor_type = scim_token.
  • In Entra, Provisioning → Provisioning logs shows the request/response pair for every operation, including the JSON body. This is the first place to look when a sync goes wrong.

Deprovisioning

When a user is removed from the assigned-users list in Entra (or hard-deleted from the directory), Entra sends:

  1. PATCH /scim/v2/Users/{id} {active: false} first (in Phase 1 we accept this as PUT, make sure the “PUT instead of PATCH” toggle is on in Entra’s mappings, see “Common gotchas” below). EUnifyer disables the user and signs them out of Keycloak immediately.
  2. If you have Delete users when fully de-provisioned enabled, Entra later sends DELETE /scim/v2/Users/{id}, which removes the user from EUnifyer entirely (GDPR-safe cascade).

Soft-disable (active=false) keeps the user row for audit purposes, recommended. Hard-delete is destructive and removes Drive/Mail/Chat content owned by the user.

Groups (Phase 2)

SCIM Groups map to EUnifyer Roles. Membership is reconciled against the IdP on every sync. Permissions are never set via SCIM: you keep control over what each group is allowed to do.

Two provisioning modes (set in Admin → SCIM groups):

  • Auto (default), every SCIM Group becomes an EUnifyer role automatically, with no permissions. Members start out with no extra access; you attach permissions in the role editor afterwards.
  • Mapped only: incoming groups land in Pending state. Members are tracked but get no role until you map the IdP group onto an existing EUnifyer role.

A stricter variant of mapped_only (reject_unmapped) makes EUnifyer return 404 for unmapped groups so the IdP admin must coordinate with you before provisioning. Use it for high-assurance setups where surprise roles are unacceptable.

The mapping screen at Admin → SCIM groups shows every IdP group ever seen by this org, the EUnifyer role it points at, current member count, and state (Active / Pending / Ignored). Map, re-map, or ignore from there.

Common gotchas

  • userPrincipalName vs mail. EUnifyer treats userName as the login email. If your userPrincipalName is not an email (alice@tenant.onmicrosoft.com), remap userName to mail in attribute mappings.
  • Provisioning starts but no users appear. Check the Scope setting:** Sync only assigned users requires an explicit assignment under Users and groups**.
  • Custom attributes. Phase 1 stores anything not in the standard mapping into users.attributes (JSON). It is accessible via the API but not yet surfaced in the UI.

Migrating between IdPs

Switching from one IdP to another (e.g. Entra → Okta) means the new IdP will push its own externalId values on the first sync. Until those replace the old ones, requests like PATCH /Users/{id} keyed on externalId match the wrong user.

To start fresh:

POST /api/v1/back-office/organizations/{org_id}/security/scim/reset-external-ids Authorization: Bearer <admin-session-token>

This wipes users.external_id for every user in the org and drops every SCIM group mapping. Roles are kept: admin-attached permissions survive. Only the SCIM linkage (and SCIM-sourced memberships) are removed.

Audit: emits admin.scim_external_ids_reset with retention class legal_hold and counts of cleared rows.

Rotating a token

Tokens are long-lived by design. To rotate without downtime:

  1. Generate a second token in Admin → SCIM provisioning.
  2. Update Entra’s Secret Token field with the new value.
  3. Run Provision on demand to verify it works.
  4. Revoke the old token in EUnifyer.

Revoked tokens fail with HTTP 401 immediately. Entra logs the failure and alerts the configured notification email.

API reference (for custom integrations)

If you are pushing from a non-Entra IdP or a homegrown HR system, the wire format is straight RFC 7644:

POST /scim/v2/Users Authorization: Bearer scim_<token> Content-Type: application/scim+json { "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], "userName": "alice@example.com", "name": {"givenName": "Alice", "familyName": "Example"}, "emails": [{"value": "alice@example.com", "primary": true}], "active": true }

The full schema and filter grammar is at /scim/v2/Schemas and /scim/v2/ServiceProviderConfig.

Filter examples

userName eq "alice@example.com" emails co "@example.com" and active eq true not (active eq false) and department sw "Eng" externalId pr meta.lastModified gt "2026-01-01T00:00:00Z"

Mixes of and / or / not and parentheses are honoured per RFC 7644 §3.4.2.2. Attribute names are case-insensitive; quoted-string values are case-insensitive for eq/ne/co/sw/ew (we compare on lower()). Unsupported attributes return 400 with scimType=invalidFilter.

Bulk operations

POST /scim/v2/Bulk Authorization: Bearer scim_<token> Content-Type: application/scim+json { "schemas": ["urn:ietf:params:scim:api:messages:2.0:BulkRequest"], "failOnErrors": 5, "Operations": [ {"method": "POST", "path": "/Users", "bulkId": "alice", "data": {"schemas":["urn:ietf:params:scim:schemas:core:2.0:User"], "userName": "alice@example.com"}}, {"method": "PATCH", "path": "/Groups/<group-uuid>", "data": {"schemas":["urn:ietf:params:scim:api:messages:2.0:PatchOp"], "Operations":[{"op":"add","path":"members", "value":[{"value":"bulkId:alice"}]}]}} ] }
  • Max 100 operations per request, exceeding returns HTTP 413 (scimType=tooMany).
  • Max 1 MB request body: also HTTP 413. Both limits are advertised in ServiceProviderConfig.
  • bulkId from a POST is resolvable inside subsequent ops (either in the JSON value tree or in /Groups/bulkId:alice-style path ids).
  • failOnErrors: N short-circuits after the Nth failure; per-op errors otherwise carry on.
  • Each op gets its own success/error envelope in Operations[].response.

Operational limits

LimitValueWhat happens on overflow
Operations per Bulk request100HTTP 413 scimType=tooMany
Bulk request body size1 MBHTTP 413 scimType=tooMany
Requests per SCIM token~200 / minuteHTTP 429 scimType=tooMany. Entra and Okta back off automatically
Page size on list endpoints200 (default 100)clamped server-side
Manager chain depth (cycle check)64 hopsHTTP 400 scimType=invalidValue

The rate limit is per token, not per organisation, issuing a separate token for each IdP tenant gives each its own budget.

Custom attributes

Anything you push under urn:ietf:params:scim:schemas:extension:eunifyer:2.0:User is preserved verbatim and echoed back on subsequent GETs. Useful when your IdP has bespoke fields that don’t fit the enterprise extension.

{ "schemas": [ "urn:ietf:params:scim:schemas:core:2.0:User", "urn:ietf:params:scim:schemas:extension:eunifyer:2.0:User" ], "userName": "alice@example.com", "urn:ietf:params:scim:schemas:extension:eunifyer:2.0:User": { "siteCode": "AMS-01", "badgePrintedAt": "2026-04-12T10:00:00Z" } }

Safety nets

  • Cycle detection on manager assignments. Setting enterprise:User.manager to a value that would close a reporting loop (A→B and then B→A) is rejected with HTTP 400 / scimType=invalidValue.
  • Last-admin protection on DELETE. DELETE /scim/v2/Users/{id} refuses with HTTP 409 / scimType=mutability if the target is the only remaining admin or owner of the organisation. The IdP can retry once your roster reflects the new admin.
  • Per-token rate limit. Each SCIM bearer token is limited to ~200 ops/minute. Excess returns HTTP 429 / scimType=tooMany. Entra and Okta both back off on 429 automatically.
  • Custom-attribute round-trip. Sub-attributes under extension:eunifyer:2.0:User keep their original casing (siteCode, not sitecode) on GET, even though SCIM ingest is otherwise case-insensitive.

What is not in scope

  • Self-service /Me endpoint. Users have the existing account page in the EUnifyer UI.
  • Inbound SCIM client. EUnifyer pushing users to another system is a separate ticket.
  • SCIM 1.1. Dead spec; nobody asks for it anymore.