DEV Community

Cover image for SCIM Provisioning for SaaS: A Complete Implementation Guide
SSOJet for SSOJet

Posted on • Originally published at ssojet.com on

SCIM Provisioning for SaaS: A Complete Implementation Guide

According to the Wing Security 2024 State of SaaS Security Report, 63% of businesses may have former employees who still retain access to organizational data after they leave. That gap is exactly what SCIM closes. When a SaaS app supports SCIM, the customer's identity provider can create, update, and deactivate accounts automatically, so an employee who is offboarded in Okta or Microsoft Entra ID loses access to your app within seconds instead of months.

SCIM provisioning lets an enterprise customer manage your app's user accounts from their own directory. You expose a standard set of HTTP endpoints, the customer points their IdP at those endpoints, and lifecycle events (joiner, mover, leaver) flow into your app without anyone filing a ticket. Most teams can ship a working SCIM 2.0 server in two to four weeks if they scope it to the core schema.

SCIM provisioning for SaaS: the practice of exposing a SCIM 2.0 (System for Cross-domain Identity Management) REST API in your application so that enterprise identity providers can automatically create, update, and deactivate user accounts and group memberships. It is defined by RFC 7643 (core schema) and RFC 7644 (protocol).

About this article:

  • Researched and written: June 2026. Last fact-checked: 2026-06-01.

  • Author hands-on experience with the topic: yes, I have built and shipped SCIM 2.0 servers tested against Okta and Microsoft Entra ID provisioning clients.

  • AI assistance: used for drafting, reviewed and edited by the named author.

  • Conflicts of interest: none.

  • Sponsorship: none.

Key Takeaways

  • SCIM 2.0 is defined by two IETF standards: RFC 7643 (the core User and Group schema) and RFC 7644 (the protocol, endpoints, and PATCH semantics).

  • A minimal SCIM server needs a base URL, bearer-token auth, and /Users and /Groups endpoints supporting GET, POST, PUT, PATCH, and DELETE.

  • Deprovisioning in SCIM almost never means DELETE: Okta and Microsoft Entra ID deactivate users by sending a PATCH that sets active to false.

  • SCIM gates enterprise deals because it closes the offboarding gap that left 63% of businesses with lingering ex-employee access (Wing Security, 2024).

  • SSOJet's Directory Sync gives you compliant SCIM 2.0 endpoints without building and maintaining the server yourself, which is the slow part.

What Is SCIM Provisioning and Why Does It Gate Enterprise Deals?

SCIM provisioning is automated user lifecycle management driven by the customer's identity provider over a standard REST API. When you support it, an enterprise admin assigns a user to your app inside Okta or Entra ID, and your app receives an HTTP request to create that account. The same machinery handles updates and deactivation.

Enterprise buyers treat SCIM as a hard requirement for a specific reason: offboarding security. Manual deprovisioning fails constantly. The Wing Security report cited above found that 63% of businesses may have former employees retaining access, and that the average employee uses 29 different SaaS applications, which is why no human can reliably revoke access app by app. SCIM removes the human from the loop.

There is a money angle too, and it is the one your sales team cares about. SSO closes the front door; SCIM closes the side door. Enterprise security questionnaires routinely ask "does this vendor support automated provisioning and deprovisioning via SCIM?" A "no" can stall or kill a deal. This is the same pattern that makes adding enterprise SSO to multi-tenant SaaS a deal accelerator: the security team can approve you faster because lifecycle control is provable and auditable. If you want the broader framing, the pillar on enterprise identity management for SaaS covers where SCIM sits in the stack.

How Does SCIM 2.0 Actually Work?

SCIM 2.0 is a REST API with a fixed JSON schema. The IdP is the SCIM client; your app is the SCIM service provider. Everything hangs off a base URL the customer configures, for example https://api.yourapp.com/scim/v2.

The two endpoints that matter are /Users and /Groups. A User resource carries core attributes defined in RFC 7643: userName (the unique key, usually an email), name.givenName, name.familyName, emails, active, and externalId (the IdP's stable identifier for the user). Group resources carry a displayName and a members array. Every resource also has a schemas array and a meta object with resourceType and location.

Here is a trimmed User payload that an IdP POSTs to /Users:

{
  "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
  "userName": "ada@acme.com",
  "name": { "givenName": "Ada", "familyName": "Lovelace" },
  "emails": [{ "value": "ada@acme.com", "primary": true }],
  "externalId": "00u1a2b3c4",
  "active": true
}

Enter fullscreen mode Exit fullscreen mode

Your server creates the user and responds with 201 Created, echoing the resource plus an id you assign and a meta.location URL. Two protocol details trip people up. First, SCIM filtering: IdPs check whether a user already exists by calling GET /Users?filter=userName eq "ada@acme.com" before they POST, so you must parse the eq filter operator on userName at minimum. Second, PATCH semantics from RFC 7644 use an operations array (add, replace, remove) rather than a full resource body, which keeps updates small and is how deactivation is sent.

How Do Okta and Microsoft Entra ID Drive Provisioning?

Okta and Microsoft Entra ID are the two SCIM clients you will be tested against most, and they behave slightly differently, so you test against both. Both discover existing users with a userName filter, create with POST, and deactivate with PATCH, but their PATCH payloads and attribute mappings differ in the details.

Okta's provisioning agent sends replace operations and is strict about the meta.resourceType and the ListResponse envelope on GET. Microsoft Entra ID is pickier about schema URNs and expects a 200 OK (not 204) on a successful PATCH that returns the updated resource. Both will run a connection test against your base URL with the bearer token before they send any real traffic, and both expect SCIM list responses wrapped like this:

{
  "schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
  "totalResults": 1,
  "startIndex": 1,
  "itemsPerPage": 20,
  "Resources": [{ "id": "abc", "userName": "ada@acme.com" }]
}

Enter fullscreen mode Exit fullscreen mode

You can see the full set of supported providers on the SSOJet integrations page, and the protocol-level reference lives in the SSOJet docs. The practical takeaway: read both vendors' SCIM compliance docs before you write a single handler, because "SCIM 2.0 compliant" in a marketing page rarely means "passes Okta's and Entra's clients on the first try."

How Do You Implement a SCIM 2.0 Server Step by Step?

What follows is the implementation order I use. Build it in this sequence, because each step depends on the one before it, and test against a real IdP as early as step two.

Step 1: Expose a SCIM 2.0 Base URL and Bearer Token

Stand up a versioned base path such as /scim/v2 and protect it with a long-lived bearer token per tenant. SCIM auth in practice is a static Authorization: Bearer <token> header that the customer pastes into their IdP, so generate a high-entropy token per connection, store only a hash, and reject any request missing or mismatching it with 401 Unauthorized. Scope the token to a single tenant so one customer's IdP can never read or write another customer's users. This per-tenant isolation is the same concern you handle when adding enterprise SSO to a multi-tenant app.

Step 2: Implement /Users CRUD

Build GET, POST, PUT, and DELETE on /Users, plus GET /Users/{id}. The POST handler must treat userName as the unique key and return 409 Conflict if a user already exists, because IdPs rely on that status to fall back to a filtered GET. Support the existence-check filter GET /Users?filter=userName eq "value" and return the ListResponse envelope shown earlier, even when there are zero results (totalResults: 0, empty Resources). Map externalId to your internal user ID so updates and deletes resolve to the right record across the user's lifetime.

Step 3: Implement /Groups

Add /Groups with the same CRUD verbs so customers can drive role and team membership from their directory. A Group has a displayName and a members array, where each member references a User id. Membership changes usually arrive as PATCH operations (add/remove on the members path), not full PUT replacements, so do not assume the IdP sends the whole group each time. If your app maps groups to roles or permissions, this is where directory-driven authorization gets wired up, and the Directory Sync product page explains how that mapping typically looks in production.

Step 4: Support PATCH for Partial Updates

Implement RFC 7644 PATCH on both /Users/{id} and /Groups/{id}. A PATCH body is an operations array, and you must handle replace, add, and remove against attribute paths. Here is the canonical deactivation PATCH that both Okta and Entra send:

{
  "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
  "Operations": [
    { "op": "replace", "path": "active", "value": false }
  ]
}

Enter fullscreen mode Exit fullscreen mode

Return 200 OK with the updated resource, not 204, because Microsoft Entra ID expects the body back. Handle both "path": "active" and the pathless {"op":"replace","value":{"active":false}} form, since IdPs are inconsistent about which they emit.

Step 5: Handle Deprovisioning and Deactivation

Treat active: false as the real offboarding signal, and decide deliberately what it does in your app. Okta and Entra deactivate by PATCHing active to false; they rarely issue an HTTP DELETE, so do not make DELETE your only offboarding path or accounts will linger exactly the way the Wing Security data describes. When you receive active: false, immediately revoke sessions and refresh tokens, block new logins, and either soft-delete or suspend the record while preserving audit history. This is the step that satisfies SOC 2 access-control controls and is why security reviewers ask about SCIM in the first place; the SaaS identity management glossary defines the surrounding lifecycle terms if you need to align language with a customer's security team.

Step 6: Test Against Okta and Microsoft Entra ID SCIM Clients

Configure a free Okta developer org and an Entra ID tenant, point each at your base URL, and run the full lifecycle: assign a user, change an attribute, remove the assignment. Okta exposes a SCIM connection test that surfaces exactly which capability check failed (for example, "create users" or "import groups"), and Entra logs provisioning steps you can read line by line. Run both, because passing one client does not mean you pass the other, and most real bugs surface only when a live IdP sends slightly malformed or unexpected payloads.

Step 7: Handle Rate Limits and Pagination

Implement cursor-friendly pagination with the startIndex and count query parameters, and return totalResults honestly so an IdP can page through thousands of users. When an initial sync pushes a large directory, you will get bursts of traffic, so return 429 Too Many Requests with a Retry-After header rather than failing silently, since both Okta and Entra honor backoff. Cap count at a sane maximum (commonly 100 to 200) and document it, because an IdP that asks for 10,000 users in one page can otherwise exhaust your memory.

What Are the Most Common SCIM Implementation Mistakes?

The first mistake is treating DELETE as the deactivation path. The standards allow DELETE, but the major IdPs deactivate with a PATCH that sets active: false, so a DELETE-only server quietly fails to offboard anyone. The second is skipping the userName eq filter, which breaks the IdP's existence check and produces duplicate users on every sync. The third is returning bare arrays instead of the ListResponse envelope, which both Okta and Entra reject outright.

The deeper tradeoff is build versus buy. A compliant SCIM server is not hard to start, but it is annoying to finish and maintain: per-IdP quirks, schema extensions, pagination edge cases, and ongoing conformance as Okta and Entra change behavior. That maintenance tail is why teams under deal pressure often choose a provider that ships the endpoints for them. SSOJet's Directory Sync gives you SCIM 2.0 endpoints that Okta and Entra already pass against, so you wire provisioning events into your app without owning the protocol surface. It is the honest tradeoff: you give up some control to skip the part that takes the longest.

Frequently Asked Questions

What is the difference between SCIM and SSO?

SSO authenticates a user at login time, usually with SAML 2.0 or OIDC, and proves who someone is for a single session. SCIM provisions the account ahead of time and deactivates it afterward, managing the user's existence and attributes across their whole lifecycle. Enterprises usually want both: SSO so logins are centralized, and SCIM so accounts are created and revoked automatically. They are complementary standards, not alternatives.

Do I have to support HTTP DELETE for SCIM deprovisioning?

In practice, no. Okta and Microsoft Entra ID deactivate users by sending a PATCH that sets the active attribute to false, not by issuing an HTTP DELETE. You should implement DELETE for full RFC 7644 compliance, but your offboarding logic must trigger on active: false, because that is the signal real identity providers send.

How long does it take to build a SCIM 2.0 server?

A focused team can ship a working SCIM 2.0 server in roughly two to four weeks if they scope it to the core User and Group schema, bearer-token auth, the userName eq filter, PATCH, and pagination. The long tail is ongoing: per-IdP quirks, schema extensions, and staying conformant as Okta and Entra change behavior. Using a provider's prebuilt Directory Sync endpoints can compress the initial build to days.

What status codes should a SCIM endpoint return?

Return 201 Created on a successful POST to /Users, 200 OK on successful GET and PATCH (with the updated resource in the body), 409 Conflict when a user already exists by userName, 401 Unauthorized for a missing or bad bearer token, and 429 Too Many Requests with a Retry-After header when you throttle. Avoid returning 204 No Content on PATCH, because Microsoft Entra ID expects the resource body back.

Which RFCs define SCIM 2.0?

SCIM 2.0 is defined by two IETF standards: RFC 7643, which specifies the core schema for User and Group resources, and RFC 7644, which specifies the protocol, including endpoints, filtering, and PATCH semantics. A SCIM server that claims 2.0 compliance should implement both. Reading them before you start saves you from the most common interoperability bugs.

Final Thoughts

SCIM is not glamorous, but it is the difference between an enterprise security team approving you and asking you to come back next quarter. Build the seven steps in order, test against both Okta and Microsoft Entra ID, and treat active: false as the offboarding signal that actually matters. If you would rather ship the endpoints in days instead of weeks, that is the gap SSOJet's Directory Sync is built to fill.

If you're ready to add enterprise SSO and SCIM without rebuilding your auth, start using SSOJet and go live in days

Sources

Top comments (0)