Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

SCIM provisioning

Every tenant exposes a SCIM 2.0 server (RFC 7643 and RFC 7644), so an HR system or a workforce identity provider can create, update, disable and remove the tenant’s users and groups on its own schedule. rIDM is the SCIM service provider: the other system pushes, rIDM stores. rIDM does not push to other SCIM servers.

Endpoints

The base URL is {PUBLIC_URL}/scim/v2/{slug}, for example https://id.example.com/scim/v2/acme. It is not under /t/{slug} and has no custom-domain form.

PathMethods
/ServiceProviderConfigGET
/ResourceTypesGET
/SchemasGET
/UsersGET (list, filter), POST
/Users/{id}GET, PUT, PATCH, DELETE
/GroupsGET (list, filter), POST
/Groups/{id}GET, PUT, PATCH, DELETE

Requests and responses use application/scim+json. Errors are SCIM error documents (urn:ietf:params:scim:api:messages:2.0:Error) with status, detail and, where it applies, scimType: invalidFilter, invalidSyntax, invalidValue, invalidPath, noTarget, uniqueness (a 409, for example a username or externalId already taken) or tooMany. POST answers 201 with a Location header; DELETE answers 204.

ServiceProviderConfig advertises what is supported: PATCH and filtering (up to 200 results per page) yes; bulk operations, sorting, ETags and the password-change operation no.

Provisioning tokens

A provisioning system authenticates with a bearer token minted for one tenant. Tokens start with rscim_, carry 256 random bits, are shown once, stored only as a SHA-256 hash, may expire, and can be revoked. A token only works against its own tenant’s base URL; any other tenant answers 401.

In the console: Provisioning (/console/provisioning/), which also shows the base URL to paste into the other system. Through the admin API, under ridm:scim:read and ridm:scim:write (held by owners, administrators and user managers):

# Mint a token (expires_in_days is optional, 1..3650; omit it for no expiry)
curl -X POST https://id.example.com/admin/tenants/acme/scim/tokens \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"name": "Okta", "expires_in_days": 365}'
{
  "id": "0192f5c4-…",
  "tenant_id": "0192f0a1-…",
  "name": "Okta",
  "expires_at": "2027-09-18T09:12:44Z",
  "last_used_at": null,
  "revoked_at": null,
  "created_at": "2026-09-18T09:12:44Z",
  "token": "rscim_Qm9i…"
}

GET /admin/tenants/{slug}/scim/tokens lists the tokens (never their values) with the base_url: every live one (a tenant may have 100), then the most recent revoked ones, 200 rows at most; DELETE /admin/tenants/{slug}/scim/tokens/{token} revokes one at once. last_used_at is updated at most once a minute per token. Creating and revoking a token are audited as scim_token.created and scim_token.revoked, and every change made with a token is recorded in the audit log with actor type client and the token’s id as actor, so provisioning changes can be told apart from administrators’ changes.

Revoked and expired tokens are deleted by the hourly cleanup job after RETENTION_DAYS.

How a SCIM User maps onto a user

SCIM attributerIDM user
idthe user’s id (read-only)
userName (required)username
externalIdexternal_id, unique per tenant
emailsemail: the entry marked primary, else the first; one address is kept
phoneNumbersphone: the primary entry, else the first
activetrue = active, false = disabled (default true)
localelocale
name.givenNameprofile attribute given_name
name.familyNameprofile attribute family_name
displayNameprofile attribute display_name
groupsdirect group memberships, read-only
metacreated, lastModified, location

The three name attributes are stored only when the tenant’s profile schema declares given_name, family_name and display_name, or allows undeclared attributes; otherwise they are dropped silently. name.formatted is produced on read and ignored on write. Other profile attributes are left alone by every SCIM write. SCIM writes count as an import, so they may set attributes the schema marks editable_by: none.

A user created through SCIM:

  • has its email marked verified (the provisioning system is trusted for it) and its phone unverified;
  • has no password. It signs in by whatever the tenant offers that needs none (a magic link, an email code, a brokered identity provider) or sets a password through recovery;
  • is not held back by required profile attributes: they are asked for at the next sign-in instead.

Setting active: false disables the user and DELETE /Users/{id} soft-deletes it, exactly as the admin API does: every session ends at once and the user’s clients get back-channel logout. A deleted account is gone from SCIM and sign-in at once, and the purge job removes it for good after the tenant’s account.deletion_retention_days (default 30). A deleted user answers 404.

PUT replaces the document: an attribute left out is cleared. A PUT without emails removes the user’s email address, one without externalId clears it. Most provisioning systems send the full document, but check before pointing a hand-written integration at PUT.

How a SCIM Group maps onto a group

SCIM attributerIDM group
idthe group’s id
displayName (required)name
externalIdattributes.externalId
membersmember users, by user id (value)

Groups created through SCIM are top-level. Members must be users of the same tenant; a nested group as a member is refused with invalidValue. A POST whose member list names a user that does not exist creates nothing. The group list covers every group of the tenant, including groups created in the console. Roles attached to a group apply to its SCIM-provisioned members like to any other member, which is the usual way to turn an upstream group into permissions: attach roles to the group in rIDM once, let SCIM keep the membership current.

That route stops at administrator access. Adding members to a group whose roles (directly, through its parent groups or through composite roles) grant any ridm:* admin permission is refused with 403 and changes nothing, whether by POST, PUT or PATCH. A provisioning token carries no administrator whose own permissions could bound what it hands out, so admin rights are granted in rIDM, not provisioned. Removing members, and requests that add nobody new, are unaffected.

Listing and filters

GET /Users and GET /Groups accept filter, startIndex (1-based) and count. count defaults to and is capped at 200; a startIndex beyond 2,000 is refused with tooMany. An unfiltered user list reads only the requested page, and its totalResults may lag writes by up to 30 seconds.

GET /Groups and GET /Groups/{id} accept excludedAttributes=members (RFC 7644 §3.9), which leaves the member list out: for a large group it is most of the document, and Microsoft Entra ID asks for groups this way. Without it, a group lists its members only for the groups on the requested page, unless the filter itself tests members.

Filters follow the RFC 7644 grammar: the operators eq ne co sw ew gt ge lt le pr, and, or, not, parentheses, dotted paths (name.givenName), value filters (emails[type eq "work"].value), schema-URN prefixes, and case-insensitive attribute names and string comparisons.

A user filter that is a single equality on userName, externalId, emails (or emails.value) or id is answered from an index, even when it is and-ed with further conditions. Any other user filter is evaluated over the tenant’s users and only works while the tenant has at most 2,000 of them; beyond that it is refused with tooMany. Provisioning systems look users up by userName or externalId before creating them, which is the indexed path. Group filters are evaluated over all groups; an equality on displayName skips the others before anything else is read.

PATCH

PATCH takes an RFC 7644 PatchOp with add, replace and remove operations, case-insensitive op names, with or without path:

{
  "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
  "Operations": [
    { "op": "replace", "path": "active", "value": false },
    { "op": "replace", "path": "emails[type eq \"work\"].value", "value": "alice@acme.example" },
    { "op": "add", "value": { "name.familyName": "Liddell" } }
  ]
}

Simple, dotted and filtered paths work, including members[value eq "<user id>"] to remove one member. Booleans may be sent as the strings "True" and "False", which some clients do. remove needs a path (noTarget otherwise) and add/replace need a value. The operations are applied to the current SCIM document and the result is stored as a full replace, so PATCH and PUT share one code path and one set of rules.

Example with curl

BASE=https://id.example.com/scim/v2/acme
SCIM=rscim_Qm9i…

# Create
curl -X POST "$BASE/Users" \
  -H "Authorization: Bearer $SCIM" -H "Content-Type: application/scim+json" \
  -d '{
        "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
        "userName": "alice",
        "externalId": "00u1a2b3c4",
        "name": {"givenName": "Alice", "familyName": "Liddell"},
        "emails": [{"value": "alice@acme.example", "type": "work", "primary": true}],
        "active": true
      }'

# Look up by externalId (indexed)
curl -G "$BASE/Users" -H "Authorization: Bearer $SCIM" \
  --data-urlencode 'filter=externalId eq "00u1a2b3c4"'

# Deactivate
curl -X PATCH "$BASE/Users/<id>" \
  -H "Authorization: Bearer $SCIM" -H "Content-Type: application/scim+json" \
  -d '{"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
       "Operations": [{"op": "replace", "path": "active", "value": false}]}'

Configuring a provisioning system

Okta, Microsoft Entra ID and similar systems need the same few things; the field names differ by product:

  • SCIM base URL / tenant URL: https://id.example.com/scim/v2/acme.
  • Authentication: HTTP header / bearer token, the rscim_ token. There is no OAuth or basic-auth mode.
  • Unique identifier for users: userName. Map it to whatever the source uses as a login name, and keep it stable; changing it upstream renames the user in rIDM.
  • Attribute mappings: userName, externalId, emails[type eq "work"].value, name.givenName, name.familyName, displayName, phoneNumbers, active, locale. Anything else the source sends is ignored.
  • Supported operations: create, update (PATCH or PUT), deactivate (active: false), delete, group push. Turn off password synchronisation; rIDM does not accept passwords over SCIM.

Run the product’s connection test first: it reads ServiceProviderConfig and filters /Users by userName, which exercises both the token and the base URL.

Caveats

  • The SCIM endpoints are not behind the tenant’s request ceilings or IP rules, which cover the sign-in and OAuth endpoint families only (see Rate limits, IP rules and CAPTCHA). Treat a provisioning token as an administrator credential: give it an expiry, one per upstream system, and revoke it when the integration goes away.
  • A disabled tenant answers every SCIM request with 403.
  • Bulk requests (/Bulk) are not supported; provisioning systems fall back to one request per change when ServiceProviderConfig says so.