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)
| Operation | Status |
|---|---|
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
- In EUnifyer, open Admin → SCIM provisioning.
- Copy the SCIM 2.0 endpoint at the top of the page. It looks like
https://app.example.com/scim/v2. - Click Generate token, give it a recognisable name (e.g.
Entra production tenant), confirm. - 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
- Sign in to the Microsoft Entra admin centre → Applications → Enterprise applications.
- 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. - In the left nav, open Provisioning → Get started → Provisioning Mode = Automatic.
- 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.
- Tenant URL: paste the SCIM endpoint from Step 1 (e.g.
- Click Test connection. Entra calls
GET /scim/v2/ServiceProviderConfigwith 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:
- Open Provisioning → Mappings → Provision Microsoft Entra ID Users.
- 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 attribute | EUnifyer field |
|---|---|
userPrincipalName | userName (= EUnifyer email) |
Switch([IsSoftDeleted], , "False", "True", "True", "False") | active |
givenName | name.givenName |
surname | name.familyName |
mail | emails[type eq "work"].value |
telephoneNumber | phoneNumbers[type eq "work"].value |
jobTitle | title |
department | urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department |
employeeId | urn:…enterprise:User:employeeNumber |
manager | urn:…enterprise:User:manager |
Phase 1 does not apply
managerrecursively (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
- Open Provisioning → Settings → Scope.
- 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.
- Set Provisioning Status to On.
- 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.deletedappear in Admin → Audit log withactor_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:
PATCH /scim/v2/Users/{id} {active: false}first (in Phase 1 we accept this asPUT, 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.- 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
userPrincipalNamevsmail. EUnifyer treatsuserNameas the login email. If youruserPrincipalNameis not an email (alice@tenant.onmicrosoft.com), remapuserNametomailin attribute mappings.- Provisioning starts but no users appear. Check the Scope setting:**
Sync only assigned usersrequires 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:
- Generate a second token in Admin → SCIM provisioning.
- Update Entra’s Secret Token field with the new value.
- Run Provision on demand to verify it works.
- 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. bulkIdfrom a POST is resolvable inside subsequent ops (either in the JSON value tree or in/Groups/bulkId:alice-style path ids).failOnErrors: Nshort-circuits after the Nth failure; per-op errors otherwise carry on.- Each op gets its own success/error envelope in
Operations[].response.
Operational limits
| Limit | Value | What happens on overflow |
|---|---|---|
| Operations per Bulk request | 100 | HTTP 413 scimType=tooMany |
| Bulk request body size | 1 MB | HTTP 413 scimType=tooMany |
| Requests per SCIM token | ~200 / minute | HTTP 429 scimType=tooMany. Entra and Okta back off automatically |
| Page size on list endpoints | 200 (default 100) | clamped server-side |
| Manager chain depth (cycle check) | 64 hops | HTTP 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.managerto 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=mutabilityif 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:Userkeep their original casing (siteCode, notsitecode) on GET, even though SCIM ingest is otherwise case-insensitive.
What is not in scope
- Self-service
/Meendpoint. 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.