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 create, read, update, delete, search, and PATCH operations, how SCIM drives the JML lifecycle and replaces custom scripts, and its real limits around granular entitlements and dynamic roles.
The account that should already be there
Sara López starts in Finance on a Monday. Single sign-on works: she signs in at the identity provider and the dashboard shows her apps. She clicks the expense tool and gets an error, “user not found.” The login was fine. The problem is that nobody created her account in that tool, and the ticket to do it is sitting in someone’s queue. Three months later, when David leaves the company, the opposite happens: his account in the same tool stays active for weeks because the offboarding script for it failed silently.
The protocols so far, SAML, OAuth, and OIDC, all answer questions at the moment a user signs in: who are you, and may this app act for you. None of them creates the account in the first place. Before Sara can sign in to a SaaS app with SSO, an account for her has to exist in that app with the right attributes and groups, and when she leaves it has to go away. 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 under the JML lifecycle we studied earlier. When someone joins, SCIM creates their accounts; when they move, it updates them; when they leave, it deactivates and then deletes them. SAML, OAuth, and OIDC are about using an identity. SCIM is about maintaining the accounts that identity owns across every application, ahead of time, so that when the user does sign in the account is already there and correct, and at offboarding it’s already gone.
The problem SCIM solves
Picture an organization with one identity provider and forty SaaS applications. Without a standard, connecting them means forty integrations, each app with its own API, its own field names, and its own quirks. Add a second identity provider after an acquisition and the count multiplies. It’s an N-by-M problem in which every identity source needs custom wiring to every application, and the maintenance never ends.
Many apps offered only proprietary hooks or no automation at all, so teams wrote custom scripts: glue code that broke whenever an app changed its API, that nobody fully understood, and whose deprovisioning half was the first to be skipped or to fail without anyone noticing. That’s how David’s account survived, and it’s exactly the kind of orphan account reconciliation has to go looking for.
SCIM replaces the tangle with one contract. Every compliant app exposes the same resource schemas, the same endpoints, and the same operations, so the identity provider speaks one protocol to all of them. Add a SCIM-capable app and provisioning mostly just works.
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’s vocabulary, the identity provider (Okta, Entra ID, and so on) is the SCIM client, and each application is a SCIM service provider that exposes a SCIM endpoint. The client pushes changes and the service provider applies them to its own user store.
The resource model: User, Group, EnterpriseUser
SCIM standardizes the shape of the data as well as the API, through schemas identified by URN. Three matter most. User is the core person resource, with userName, name (split into givenName and familyName), emails, active, groups, and more. Group is a collection with members, used to drive group-based access in the target app. EnterpriseUser is an extension that adds the workplace attributes the core User lacks: employeeNumber, department, manager, costCenter, division.
Every SCIM resource is JSON and describes itself: it lists the schemas it conforms to, so the service provider knows how to read each field. Here’s the User resource the identity provider would have sent for Sara on her first day, with the enterprise extension carried 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": "López" },
"emails": [{ "value": "sara@example.com", "primary": true }],
"active": true,
"groups": [{ "value": "finance", "display": "Finance" }],
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {
"employeeNumber": "EMP-08531",
"department": "Finance",
"manager": { "value": "26118915-6090-4610-87e4-49d8ca9f808d" }
}
}Two fields deserve attention. The id is the service provider’s own immutable identifier for the resource. It’s different from the userName and from the identity provider’s identifier, and it’s what every later operation targets. The active boolean is the deprovisioning switch: setting it to false is how SCIM disables a user without deleting them, which maps directly onto the JML rule of disabling first.
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 | Many operations in one request, for efficiency at scale |
/Schemas | The schemas and attributes the server supports |
/ServiceProviderConfig | The server’s capabilities: which operations and filters it supports |
/ResourceTypes | The resource types the server exposes |
The last three, the discovery endpoints, matter more than they look. Implementations vary in what they support, so a well-behaved client queries them to learn what a given service provider can actually do, whether it handles PATCH or bulk operations for example, instead of assuming.
The operations
SCIM maps the lifecycle onto ordinary 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}, replacing the whole resource |
| Update | PATCH | PATCH /Users/{id}, a partial change |
| Delete | DELETE | DELETE /Users/{id} |
| Search | GET or POST | GET /Users?filter=userName eq "sara@example.com" |
Two distinctions matter in practice. The first is PUT versus PATCH. PUT replaces the whole resource, so if you leave out a field you may clear it, and the client must send the complete object. PATCH applies a partial change, adding, replacing, or removing specific attributes, and is the safer and lighter choice for routine updates. The second is 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 the identity provider matches its user to the service provider’s id before updating it.
SCIM driving the JML lifecycle
Put the operations together and you have the automated JML lifecycle in protocol form.
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 ContentThe protocol encodes the discipline from the JML article neatly. The leaver is two steps, not one: PATCH active=false cuts access right away, and DELETE comes only after the retention window. That’s why SCIM matters for security and not only for convenience. It makes fast, reliable deprovisioning a standard capability instead of the custom script most likely to be skipped. Had the expense tool been connected by SCIM, David’s leaver event would have flipped active to false the same afternoon.
JIT provisioning versus SCIM
There’s a second way accounts get created, and it’s worth contrasting with SCIM: just-in-time (JIT) provisioning. With JIT the account is created at first sign-in. When a user authenticates through 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 is needed.
JIT is simple and needs no SCIM integration, but it only ever creates accounts, and only at sign-in. It doesn’t update them as attributes change, and it never deletes them. A JIT-provisioned user who leaves simply stops signing in, and the account sits there dormant. JIT has no deprovisioning story at all. SCIM provisions before sign-in and handles updates and deprovisioning too, the whole lifecycle rather than the first moment.
So use SCIM wherever you need real lifecycle management, which covers 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, you’ve inherited the very orphan-account problem SCIM exists to solve. That’s a reasonable trade for a throwaway internal tool and a quietly dangerous one for anything that holds sensitive data or gets audited.
Why SCIM beats custom scripts
The case against the old approaches is concrete. You learn one integration model instead of forty, so connecting a new SaaS app becomes configuration rather than a coding project. Deprovisioning becomes reliable, because active=false and DELETE are standard operations rather than the fragile tail of a script, and that directly shrinks the orphan-account problem. The connectors are maintained by the ecosystem: the major identity providers (Okta, Entra ID, OneLogin) and thousands of SaaS apps ship SCIM support, so you aren’t maintaining your own glue. And it’s plain REST and JSON over HTTPS, easy to test, log, and reason about, unlike opaque proprietary sync.
There’s a catch hidden in the phrase “supports SCIM,” though. Some service providers implement POST and PUT but not PATCH, some support only a small slice of the filter syntax, and some treat DELETE as a soft disable. Read the target’s /ServiceProviderConfig and test the deprovisioning path specifically. The distance between “SCIM-compliant” on a datasheet and “reliably deactivates users” in production is where real offboarding failures hide.
SCIM at scale: initial sync, pagination, and bulk
Connecting SCIM to an app that already has tens of thousands of users raises problems the happy path hides.
The first run has to match existing app accounts to identities in the identity provider, usually with a filter like userName eq "...", before it can manage them. Get that matching wrong and you create duplicate accounts instead of adopting the existing ones. GET /Users returns results in pages (startIndex, count, totalResults), and a client has to walk through them rather than assume one response holds everyone. The /Bulk endpoint lets a client submit many operations in one request, cutting round trips for mass changes, though support is uneven as usual. And service providers throttle, so a well-behaved client backs off and retries instead of hammering the endpoint during a large sync.
The protocol itself is simple. These operational details are where SCIM rollouts actually succeed or stall, and where most of the engineering time goes.
Securing the SCIM endpoint
A SCIM endpoint is one of the most powerful interfaces an application exposes: it can create, change, and delete any user. That makes it a valuable target and a serious responsibility.
SCIM calls are authenticated, almost always with an OAuth 2.0 bearer token issued to the identity provider’s connector. That token is in effect an administrator credential for the whole user store, so treat it as one. Keep it in a secrets manager, rotate it, scope it as narrowly as the provider allows, and watch how it’s used. A leaked SCIM token lets an attacker create backdoor accounts or delete users en masse. The service provider should authorize the client tightly, validate inputs, and log every operation, because provisioning actions are exactly what an auditor and an incident responder will need to reconstruct events. And it’s always TLS: neither the user data nor the token should ever cross a plaintext channel.
There’s an irony here worth remembering. The protocol built to remove access reliably is itself a powerful path to access. Securing the SCIM channel is part of securing identity, not something to get to later.
What SCIM doesn’t do
SCIM is very good at what it’s for, and its edges matter, because they show where other layers take over.
It doesn’t handle granular entitlements. SCIM provisions accounts and group memberships, not fine-grained permissions inside an app. It can put Sara in the Finance group; it can’t say “may approve refunds up to $5,000” or “read-only on the EU dataset.” Those entitlements live inside each application or in a governance or authorization layer. It doesn’t evaluate dynamic or policy-based roles either. SCIM pushes concrete attribute and group values; it doesn’t evaluate rules like “give this role to anyone in Finance in Germany.” That logic belongs to the identity provider or the IGA platform deciding what to send, or to a policy engine at access time. Its model is deliberately coarse, covering the common denominator of users and groups, which is what makes it interoperable and also why deep, app-specific structures don’t map cleanly. And vendor differences in PATCH and filtering mean that, in practice, SCIM is a family of dialects rather than one uniform behavior.
This boundary is behind a common misunderstanding. Teams expect SCIM to “sync permissions” and are surprised it only syncs accounts and group names. The group is a coarse handle, and the target app still decides what Finance means internally. When someone asks SCIM to carry rich entitlements, the right move is to take that need to the governance and authorization layers instead of bending the provisioning protocol around it.
None of this is a flaw; it’s scope. SCIM owns the account lifecycle. Fine-grained authorization is a different problem, solved by IGA and by the authorization models we’ll meet later in this domain: RBAC, ABAC, ReBAC, and policy engines.
How SCIM fits with the other protocols
SCIM sits alongside the federation protocols as a complement, not an alternative. 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 when she leaves. SAML or OIDC runs at sign-in and proves it’s really Sara: she authenticates at the identity provider and is let into an app where she already has an account.
A typical enterprise setup connects both to the same app from the same identity provider, SCIM for the lifecycle and SAML or OIDC for authentication. Drop SCIM and you have single sign-on into accounts nobody maintains, including those of people who left and can still authenticate if something else fails. Drop SSO and you have well-kept accounts with no clean way in. SCIM administers the account; federation authenticates the person.
That also frames the arc of this part of the module: the directory, where identity lives; SAML, OAuth, and OIDC, how it travels at sign-in; and SCIM, how the accounts are kept up over time.
Recap
SCIM 2.0 is the standard that automates the account lifecycle.
- It’s a REST and JSON provisioning standard (RFC 7643 and 7644) between an identity provider, the SCIM client, and apps, the service providers.
- It has three schemas: User, Group, and the EnterpriseUser extension (department, manager, employeeNumber).
- It defines standard endpoints (
/Users,/Groups,/Bulk,/Schemas,/ServiceProviderConfig) and standard operations that map create, read, update, delete, search, and PATCH onto HTTP verbs. - It’s JML in protocol form: POST for joiners, PATCH for movers,
active=falseand later DELETE for leavers. - It replaces custom scripts and proprietary hooks, makes reliable deprovisioning a standard capability, and shrinks the orphan-account problem. JIT provisioning creates accounts but never removes them.
- Its limits mark where the next layers begin: SCIM handles accounts and groups, not granular entitlements or dynamic roles. In real deployments it pairs with SAML or OIDC against the same app.
Hands-on exercises
These are meant to be done, not just read.
- Read a real ServiceProviderConfig. Find the SCIM documentation of an app that supports it (Okta, Slack, GitHub, and many others publish their SCIM schemas) and work out which operations it supports. Does it do PATCH? Bulk? Which filter operators?
- Model a user. 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 assigns rather than you.
- Script the lifecycle. For a joiner, a mover who goes to Sales, and a leaver, write the exact HTTP verb, endpoint, and key payload for each SCIM call, and explain why the leaver takes two operations.
- PUT versus PATCH. You need to change only a user’s department. Write the PATCH request, then explain what could go wrong if you used PUT with a partial body.
- Find the limit. 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 account and group part), and which layer has to handle the rest.
- JIT or SCIM. Choose one for each case and justify it: a workforce app where leavers must lose access within an hour, and a low-risk internal wiki where accounts can be created on first sign-in and never need updating. Then say what you’d lose by choosing the other.
Three questions to test yourself
- A company connects its identity provider to a new SaaS app that advertises “SCIM support,” yet people who’ve left keep their access. Give three concrete reasons this can happen despite SCIM, and how you’d diagnose each.
- Explain how SCIM expresses the JML rule of disabling first and deleting later at the protocol level, and why the two steps matter for both security and audit.
- Why is SCIM deliberately unable to model granular entitlements, and what does that tell you about where authorization logic actually has to live?
Frequently asked questions
What is SCIM 2.0?
SCIM (System for Cross-domain Identity Management) 2.0 is a REST and JSON standard, defined in RFC 7643 and RFC 7644, for automatically creating, updating, and removing user and group accounts between an identity provider and applications. Every app exposes the same API, with the same resource schemas, endpoints, and operations, so account management stops being a custom integration 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 expensive to maintain, with deprovisioning especially unreliable. SCIM replaces that N-by-M tangle with one REST contract every app implements the same way, so the identity provider can create, update, and remove accounts everywhere through one well-defined protocol.
What are the core SCIM schemas?
There are three. User is the core person resource (userName, name, emails, active, groups). Group is a collection with members. EnterpriseUser is an extension that adds workplace attributes such as employeeNumber, department, and manager. Each resource declares its schemas by URN, and servers publish what they support at the /Schemas endpoint, so clients know the exact shape of the data.
How does SCIM handle deprovisioning?
In two steps, mirroring the JML rule of disabling before deleting. To deactivate, the identity provider sends a PATCH setting the user's active attribute to false, which cuts access while keeping the account. To remove it for good, it sends DELETE. Mature setups disable immediately on a leaver event and delete only after the retention window, so access goes away fast but the audit trail and data survive.
What is the difference between PUT and PATCH in SCIM?
PUT replaces the whole resource with the payload you send, and anything you leave out may be cleared, so the client has to send the complete object. PATCH applies a partial change (add, replace, or remove specific attributes) without resending everything, which is safer and lighter for routine updates like a department change. PATCH is the usual choice, though vendor support for its operations varies.
What are SCIM's limitations?
SCIM provisions accounts and group memberships well, but it doesn't model granular, app-internal entitlements or dynamic, policy-based roles. It can put a user in the 'Engineering' group; it can't grant 'approve refunds up to $5,000.' Fine-grained authorization stays inside each app or in an IGA or authorization layer. Implementations also differ in practice, especially around PATCH and filtering.