SCIM 2.0: the standard that automates provisioning
How SCIM 2.0 standardizes user provisioning between identity providers and apps — the REST resource model (User, Group, EnterpriseUser), the /Users, /Groups, /Bulk and /Schemas endpoints, the CRUD + Search + PATCH operations, how SCIM drives the JML lifecycle and replaces custom scripts, and its real limitations around granular entitlements and dynamic roles.
From login-time to lifecycle-time
The protocols so far — SAML, OAuth, OIDC — all answer questions at the moment a user logs in: who are you, may this app act for you. But none of them creates the account in the first place. Before Sara can sign into a SaaS app with SSO, an account for Sara has to exist in that app, with the right attributes and group memberships — and when she leaves, it has to be removed. That’s provisioning, and SCIM 2.0 is the standard that automates it.
SCIM (System for Cross-domain Identity Management), defined in RFC 7643 and RFC 7644 and finalized in 2015, is the protocol wiring underneath the JML lifecycle we studied earlier. When a joiner is hired, SCIM creates their accounts; when a mover changes teams, SCIM updates them; when a leaver departs, SCIM deactivates and deletes them. If SAML/OAuth/OIDC are about using an identity, SCIM is about maintaining the accounts that identity owns across every application.
The problem SCIM solves
Imagine an organization with one identity provider and forty SaaS applications. Without a standard, connecting them means building forty integrations — each app with its own API, its own field names, its own quirks. Add a second IdP (after an acquisition, say) and the count multiplies: it’s an N-by-M problem, where every identity source needs bespoke wiring to every application, and the maintenance never ends. Worse, many apps offered only proprietary “lifecycle hooks” or no automation at all, so teams resorted to custom scripts: brittle glue code that broke whenever an app changed its API, that no one fully understood, and whose deprovisioning half was the first to be skipped or to fail silently — leaving exactly the orphan accounts reconciliation has to hunt down.
SCIM replaces that N-by-M explosion with one contract. Every SCIM-compliant app exposes the same resource schemas, the same endpoints, and the same operations, so the IdP speaks one protocol to all of them. Add a SCIM-capable app and provisioning largely just works; the custom-script era — and its silent deprovisioning failures — is what SCIM was built to end.
flowchart LR accTitle: SCIM as the provisioning bridge between IdP and apps accDescr: The HR system feeds the identity provider, which acts as the SCIM client. The identity provider pushes standardized SCIM REST calls to each application's SCIM endpoint, which acts as the SCIM service provider, to create, update, and deactivate accounts. One standard protocol replaces a separate custom connector per application. HR[HR / source] --> IDP[IdP<br/>SCIM client] IDP -->|SCIM REST| A1[App A<br/>SCIM endpoint] IDP -->|SCIM REST| A2[App B<br/>SCIM endpoint] IDP -->|SCIM REST| A3[App C<br/>SCIM endpoint]
In SCIM terms, the IdP (Okta, Entra ID, etc.) is the SCIM client, and each application is a SCIM service provider that exposes a SCIM endpoint. The client pushes changes; the service provider applies them to its own user store.
The resource model: User, Group, EnterpriseUser
SCIM standardizes not just the API but the shape of the data, through schemas identified by URN. Three matter most:
- User — the core person resource:
userName,name(withgivenName/familyName),emails,active,groups, and more. - Group — a collection resource with
members, used to drive group-based access in the target app. - EnterpriseUser — an extension schema adding workplace attributes the core User lacks:
employeeNumber,department,manager,costCenter,division.
Every SCIM resource is JSON and self-describing: it lists the schemas it conforms to, so a service provider knows exactly how to interpret each field it receives. A SCIM User resource looks like this — note how it declares its schemas and carries the enterprise extension as a namespaced block:
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User",
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
],
"id": "2819c223-7f76-453a-919d-413861904646",
"userName": "sara@example.com",
"name": { "givenName": "Sara", "familyName": "Lopez" },
"emails": [{ "value": "sara@example.com", "primary": true }],
"active": true,
"groups": [{ "value": "engineering", "display": "Engineering" }],
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {
"employeeNumber": "EMP-08531",
"department": "Engineering",
"manager": { "value": "26118915-6090-4610-87e4-49d8ca9f808d" }
}
}Two fields deserve attention. The id is the service provider’s own immutable identifier for the resource — distinct from the userName and from the IdP’s identifier — and it’s what subsequent operations target. The active boolean is the deprovisioning switch: flipping it to false is how SCIM disables a user without deleting them, which maps directly onto the JML “disable first” rule.
The endpoints
A SCIM service provider exposes a small, standard set of REST endpoints:
| Endpoint | Purpose |
|---|---|
/Users | Create, read, update, delete, and search user resources |
/Groups | The same operations for groups and their membership |
/Bulk | Submit many operations in a single request, for efficiency at scale |
/Schemas | Discover the schemas (and attributes) the server supports |
/ServiceProviderConfig | Discover the server’s capabilities — which operations and filters it supports |
/ResourceTypes | Discover the resource types the server exposes |
The discovery endpoints (/Schemas, /ServiceProviderConfig, /ResourceTypes) matter more than they look: because SCIM implementations vary in what they support, a well-behaved client queries these to learn what a given service provider can actually do — for example whether it supports PATCH or bulk operations — rather than assuming.
The operations
SCIM maps the lifecycle onto standard HTTP verbs against those endpoints:
| Operation | HTTP | Example |
|---|---|---|
| Create | POST | POST /Users with the new resource |
| Read | GET | GET /Users/{id} |
| Replace | PUT | PUT /Users/{id} — full resource replacement |
| Update | PATCH | PATCH /Users/{id} — partial change |
| Delete | DELETE | DELETE /Users/{id} |
| Search | GET / POST | GET /Users?filter=userName eq "sara@example.com" |
Two distinctions matter in practice. PUT vs PATCH: PUT replaces the whole resource — omit a field and you may clear it — so the client must send the complete object, whereas PATCH applies a partial change (add/replace/remove specific attributes) and is the safer, lighter choice for routine updates. Search with filters: SCIM defines a filter syntax (userName eq "...", active eq true) so a client can find a resource by attribute, which is how an IdP correlates its user to the service provider’s id before updating.
SCIM driving the JML lifecycle
Put the operations together and you get the automated JML lifecycle in protocol form — the concrete wiring behind joiner, mover, and leaver:
sequenceDiagram
accTitle: SCIM operations across the JML lifecycle
accDescr: When a person joins, the identity provider sends POST to /Users to create the account with active set to true, and the app returns the created resource id. When the person changes roles, the identity provider sends PATCH to update attributes and group membership. When the person leaves, the identity provider first sends PATCH to set active to false, suspending access, and later sends DELETE to remove the account after the retention period, mirroring the disable-then-delete discipline.
participant IdP as IdP (SCIM client)
participant App as App (service provider)
Note over IdP,App: Joiner
IdP->>App: POST /Users (active=true)
App->>IdP: 201 Created (id)
Note over IdP,App: Mover
IdP->>App: PATCH /Users/{id} (new dept + groups)
App->>IdP: 200 OK
Note over IdP,App: Leaver
IdP->>App: PATCH /Users/{id} (active=false)
App->>IdP: 200 OK
IdP->>App: DELETE /Users/{id} (after retention)
App->>IdP: 204 No ContentNotice how cleanly the protocol encodes the lifecycle discipline from the JML article. The leaver is two steps, not one: a PATCH active=false cuts access immediately, and the DELETE comes only after the retention window — disable first, delete later, expressed in HTTP. This is exactly why SCIM matters for security, not just convenience: it makes reliable, fast deprovisioning a standard capability instead of the most-skipped custom script.
JIT provisioning vs SCIM
There’s a second way accounts get created, and you should be able to contrast it with SCIM: just-in-time (JIT) provisioning. With JIT, the account is created lazily at first login — when a user authenticates via SAML or OIDC for the first time, the app reads the assertion or ID token and creates a local account on the spot from those claims. No prior provisioning call needed.
The trade-off is decisive:
- JIT is simple and needs no SCIM integration — but it only ever creates accounts, at login. It doesn’t update them as attributes change, and crucially it never deletes them. A JIT-provisioned user who leaves simply stops logging in, leaving a dormant account behind. JIT has no deprovisioning story at all.
- SCIM provisions ahead of login and, decisively, handles updates and deprovisioning — the whole lifecycle, not just the first moment.
The practical rule: use SCIM whenever you need real lifecycle management, which is most workforce scenarios, and treat JIT as a lightweight convenience for creation-only cases or as a complement to SCIM. If an app supports only JIT, understand that you’ve just inherited the very orphan-account problem SCIM exists to solve — a perfectly reasonable trade for a throwaway internal tool, and a quietly dangerous one for anything holding sensitive data or subject to audit.
Why SCIM replaces custom scripts and proprietary hooks
The case for SCIM over the old approaches is concrete:
- One integration model, not forty. Learn SCIM once and every compliant app provisions the same way; onboarding a new SaaS becomes configuration, not a coding project.
- Reliable deprovisioning. Because
active=falseandDELETEare standard, offboarding is a first-class operation rather than the brittle tail of a script — directly shrinking the orphan-account problem. - Maintained by the ecosystem. Major IdPs (Okta, Entra ID, OneLogin) and thousands of SaaS apps ship SCIM support, so the connector is vendor-maintained, not your fragile glue.
- Bidirectional and inspectable. It’s plain REST/JSON over HTTPS — easy to test, log, and reason about, unlike opaque proprietary sync.
SCIM at scale: bulk, pagination, and initial sync
Onboarding SCIM to an app that already has tens of thousands of users surfaces practical concerns the happy path hides:
- Initial sync. The first run must match existing app accounts to IdP identities — usually via a filter like
userName eq "..."— before it can manage them. Get this correlation wrong and you create duplicate accounts instead of adopting the existing ones. - Pagination.
GET /Usersreturns results in pages (startIndex,count,totalResults); a client must page through them, never assuming a single response holds everyone. - Bulk. The
/Bulkendpoint lets a client submit many operations in one request, cutting round-trips for mass changes — though, as ever, support is uneven. - Rate limits. Service providers throttle; a well-behaved client backs off and retries rather than hammering the endpoint during a large sync.
These operational details are where SCIM rollouts actually succeed or stall — the protocol is simple, but onboarding an existing user base at scale is where the engineering time goes.
Securing the SCIM endpoint
A SCIM endpoint is among the most powerful interfaces an application exposes: it can create, modify, and delete any user. That makes it both a high-value target and a serious responsibility:
- Authentication. SCIM calls are authenticated, almost always with an OAuth 2.0 bearer token issued to the IdP connector. That token is effectively an admin credential for the entire user store — treat it as one.
- Protect the token. Store it in a secrets manager, rotate it, scope it as narrowly as the provider allows, and monitor its use. A leaked SCIM token lets an attacker create backdoor accounts or mass-delete users.
- Least privilege and logging. The service provider should authorize the client tightly, validate inputs, and log every operation — provisioning actions are exactly what an auditor and an incident responder need to reconstruct.
- TLS always. It’s REST over HTTPS; neither the user data nor the bearer token should ever cross a plaintext channel.
The irony worth internalizing: the protocol built to reliably remove access is itself a powerful access path. Securing the SCIM channel is part of securing identity, not an afterthought to it.
The limitations: what SCIM does not do
SCIM is excellent at what it’s for and important to understand at its edges, because its limitations define where other layers take over:
- No granular entitlements. SCIM provisions accounts and group memberships, not fine-grained, app-internal permissions. It can put Sara in the
Engineeringgroup; it cannot express “may approve refunds up to $5,000” or “read-only on the EU dataset.” Those entitlements live inside each application or in a governance/authorization layer. - No dynamic or policy-based roles. SCIM pushes concrete attribute and group values; it doesn’t evaluate rules like “grant this role to anyone in Finance in Germany.” That logic belongs to the IdP/IGA computing what to send, or to a policy-based authorization engine at access time.
- Coarse model. The standard deliberately models the common denominator — users and groups — which is why it’s interoperable, but also why deep, app-specific structures don’t map cleanly.
- Implementation drift. As the callout warns, vendor differences in PATCH and filtering mean “SCIM” in practice is a family of dialects, not one uniform behavior.
In practice this boundary is the source of a common misconception: teams expect SCIM to “sync permissions” and are surprised it only syncs accounts and group names. The group is a coarse handle — the target app still decides what Engineering means internally. When stakeholders ask SCIM to carry rich entitlements, the right move is to push that need to the governance and authorization layers, not to bend the provisioning protocol around it.
None of this is a flaw — it’s scope. SCIM owns account lifecycle; fine-grained authorization is a different problem solved by IGA (governance) and by the authorization models we’ll meet later in Access Management (RBAC, ABAC, ReBAC, policy engines). Knowing the boundary keeps you from trying to force entitlement logic into a provisioning protocol that was never meant to hold it.
How SCIM fits with the other protocols
Step back and SCIM clicks into place alongside the federation protocols — they’re complementary, not alternatives:
- SCIM runs ahead of time and keeps the account existing and correct: it creates Sara’s account in the app, sets her attributes, puts her in the right groups, and removes her at offboarding.
- SAML or OIDC runs at login time and proves it’s really Sara: she authenticates at the IdP and is admitted to the app she already has an account in.
A typical enterprise setup wires both to the same app from the same IdP: SCIM for lifecycle, SAML/OIDC for authentication. Drop SCIM and you have SSO into accounts nobody maintains — including departed users who can still authenticate. Drop SSO and you have well-managed accounts with no clean way to log in. The pair is the standard pattern: SCIM administers the account, federation authenticates the human.
That also frames the whole module’s arc: directory (where identity lives) → SAML / OAuth / OIDC (how it travels at login) → SCIM (how the accounts are maintained over time). Five protocols, one coherent system — the common language this module set out to teach.
Recap
SCIM 2.0 is the standard that automates the account lifecycle:
- It’s a REST/JSON provisioning standard (RFC 7643/7644) between an IdP (the SCIM client) and apps (service providers).
- Three schemas: User, Group, and the EnterpriseUser extension (department, manager, employeeNumber).
- Standard endpoints —
/Users,/Groups,/Bulk,/Schemas,/ServiceProviderConfig— and standard operations mapping CRUD + Search + PATCH onto HTTP verbs. - It is JML in protocol form: POST for joiners, PATCH for movers,
active=falsethen DELETE for leavers — disable-first deprovisioning, standardized. - It replaces custom scripts and proprietary hooks, making reliable deprovisioning a first-class capability and shrinking orphan accounts.
- Its limits define the next layers: SCIM does accounts and groups, not granular entitlements or dynamic roles — those belong to IGA and to authorization models. And in a real deployment it pairs with SAML or OIDC against the same app: SCIM administers the account, federation authenticates the human.
Hands-on exercises
These are practical — do them, don’t just read them:
- Read a real ServiceProviderConfig. Find a SCIM-enabled app’s docs (Okta, Slack, GitHub, and many others publish their SCIM schemas) and identify which operations it supports — does it do PATCH? Bulk? What filter operators?
- Model a User (use case). Write the JSON for a SCIM User resource for a new hire in Finance, including the EnterpriseUser extension with department and manager. Mark which field the service provider would assign rather than you.
- Script the lifecycle (use case). For a joiner, mover (moves to Sales), and leaver, write the exact HTTP verb, endpoint, and key payload for each SCIM call — and explain why the leaver is two operations, not one.
- PUT vs PATCH (use case). You need to change only a user’s department. Show the PATCH request, then explain what could go wrong if you used PUT with a partial body instead.
- Find the limit (use case). Your app needs to grant “approver, up to $10,000, EU region only.” Explain why SCIM can’t express this, what it can do toward it (the group/account part), and which layer must handle the rest.
- Choose JIT or SCIM (use case). For each, pick JIT provisioning or SCIM and justify it: a workforce app where leavers must lose access within an hour; a low-risk internal wiki where accounts can be created on first login and never need updating. Then state what you’d lose by choosing the other.
Three questions to test yourself
- A company connects its IdP to a new SaaS app that advertises “SCIM support,” but terminated users keep retaining access. List three concrete reasons this can happen despite SCIM, and how you’d diagnose each.
- Explain how SCIM expresses the JML “disable first, delete later” rule at the protocol level, and why that two-step matters for both security and audit.
- Why is SCIM deliberately unable to model granular entitlements, and what does that tell you about where authorization logic must actually live?
Frequently asked questions
What is SCIM 2.0?
SCIM (System for Cross-domain Identity Management) 2.0 is a REST/JSON standard, defined in RFC 7643 and RFC 7644, for automatically provisioning and de-provisioning user and group accounts between an identity provider and applications. It gives every app the same API — the same resource schemas, endpoints, and operations — so creating, updating, and deactivating accounts becomes standardized instead of bespoke per app.
What problem does SCIM solve?
Before SCIM, connecting an identity provider to each application meant a custom script or a proprietary connector per app — brittle, inconsistent, and a maintenance burden, with deprovisioning especially unreliable. SCIM replaces that N-by-M mess with one standard REST contract every app implements the same way, so the IdP can create, update, and remove accounts everywhere through a single, well-defined protocol.
What are the core SCIM schemas?
SCIM defines three: User (the core person resource — userName, name, emails, active, groups), Group (a collection with members), and EnterpriseUser (an extension adding workplace attributes like employeeNumber, department, and manager). Each resource declares its schemas by URN, and servers expose what they support at the /Schemas endpoint, so clients know the exact shape of the data.
How does SCIM handle deprovisioning?
Two ways, mirroring the JML disable-then-delete discipline. To deactivate, the IdP sends a PATCH setting the user's active attribute to false, which suspends access while preserving the account. To fully remove it, the IdP sends DELETE. Mature setups disable immediately on a leaver event and delete only after the retention window, so access is cut fast but audit and data survive.
What is the difference between PUT and PATCH in SCIM?
PUT replaces the entire resource with the payload you send — anything omitted can be cleared — so the client must send the full object. PATCH applies a partial change (add, replace, or remove specific attributes) without resending everything, which is safer and more efficient for routine updates like a department change. PATCH is preferred for most lifecycle updates, though provider support for its operations varies in practice.
What are SCIM's limitations?
SCIM provisions accounts and group memberships well, but it does not model granular, app-internal entitlements or dynamic, policy-based roles — it can put a user in the 'Engineering' group, not grant 'approve refunds up to $5,000.' Fine-grained authorization stays inside each app or in an IGA/authorization layer. SCIM also suffers real-world inconsistency, as vendors implement PATCH and filtering with subtle differences.