Skip to content

필사 모드: SCIM 2.0 Deep Dive — The Standard for Automated User Provisioning

English
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

Introduction — SSO Is Only Half the Answer

When people talk about enterprise IAM, most of the discussion gravitates toward SSO (Single Sign-On): SAML versus OIDC, which IdP to use, and so on. But anyone who has actually rolled out SSO in production quickly runs into the next question.

"Login works — but who creates the accounts? And who deletes them when someone leaves?"

This is the problem of **provisioning**, and as of 2026 the de facto standard answer is **SCIM 2.0 (System for Cross-domain Identity Management)**. In an era where Zero Trust preaches "identity-first," account lifecycle automation is no longer optional — it is a security prerequisite. With non-human identities such as AI agents entering the picture, manual account management has become unsustainable both operationally and from a security standpoint.

This post walks through the SCIM 2.0 spec structure, concrete HTTP examples, the state of support in major IdPs (Okta, Microsoft Entra ID, Keycloak), and the traps you are likely to fall into when implementing a SCIM server yourself.

Why Provisioning Matters as Much as SSO — The JML Lifecycle

The account lifecycle is commonly described with the **JML (Joiner / Mover / Leaver)** model.

| Stage | Event | Required actions | Risk if it fails |

| --- | --- | --- | --- |

| Joiner | New hire, onboarding | Create accounts, assign groups/roles | Onboarding delays, lost productivity |

| Mover | Department transfer, role change | Re-adjust permissions, change groups | Privilege creep |

| Leaver | Resignation, contract end | Deactivate accounts, revoke sessions/tokens | Ex-employee access, data exfiltration |

SSO only covers the "moment of authentication." JML covers the "lifetime of an account." With SSO but no provisioning, the following happens.

- New hires waste days asking administrators for accounts in every SaaS app.

- Employees who change departments carry their old permissions with them — the single most common audit finding.

- A departed employee has their IdP account blocked, but JIT-created local SaaS accounts and API tokens stay alive. This is a recurring scenario in real breaches.

Leaver handling in particular is directly tied to security incidents. Blocking SSO only prevents "new logins"; already-issued sessions, refresh tokens, and app-local accounts must be revoked separately. The standardized channel for automating that revocation is SCIM deprovisioning.

SCIM 2.0 Spec Structure — A Standard Made of Three RFCs

SCIM 2.0 was standardized by the IETF in 2015 across three RFCs.

| RFC | Title | Role |

| --- | --- | --- |

| [RFC 7642](https://datatracker.ietf.org/doc/html/rfc7642) | Definitions, Overview, Concepts, and Requirements | Terminology, use cases, requirements |

| [RFC 7643](https://datatracker.ietf.org/doc/html/rfc7643) | Core Schema | User/Group resource schemas, extension model |

| [RFC 7644](https://datatracker.ietf.org/doc/html/rfc7644) | Protocol | REST API, filters, PATCH, bulk, pagination |

Here is the structure in a single picture.

+----------------------------+ +----------------------------+

| IdP / HR system | SCIM | SP (SaaS app) |

| (SCIM client) | ------> | (SCIM server) |

| | HTTPS | |

| - Okta | | - POST /Users |

| - Entra ID | | - GET /Users?filter= |

| - Keycloak + custom | | - PATCH /Users/id |

| - Workday and other HR | | - DELETE /Users/id |

+----------------------------+ +----------------------------+

RFC 7642: concepts RFC 7643: schema RFC 7644: protocol

The key point is the division of roles: typically the **IdP acts as the SCIM client** and the **SaaS app (SP) acts as the SCIM server**. When a vendor says "we support SCIM," what they mean is "we implemented a SCIM server."

User / Group Schemas and the Extension Model

The Core User schema

These are the representative attributes of the User resource defined by RFC 7643.

{

"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],

"id": "2819c223-7f76-453a-919d-413861904646",

"externalId": "emp-10042",

"userName": "yjkim@example.com",

"name": {

"familyName": "Kim",

"givenName": "Youngju"

},

"displayName": "Youngju Kim",

"emails": [

{

"value": "yjkim@example.com",

"type": "work",

"primary": true

}

],

"active": true,

"groups": [

{

"value": "e9e30dba-f08f-4109-8486-d5c6a331660a",

"display": "platform-team"

}

],

"meta": {

"resourceType": "User",

"created": "2026-06-12T09:00:00Z",

"lastModified": "2026-06-12T09:00:00Z",

"version": "W/\"3694e05e9dff590\"",

"location": "https://api.example.com/scim/v2/Users/2819c223-7f76-453a-919d-413861904646"

}

}

The attributes worth paying close attention to:

- **id** — An immutable identifier issued by the SCIM server (SP). The client never chooses it.

- **externalId** — The identifier on the client (IdP/HR) side. It is the keystone of cross-system mapping; use an immutable value such as an employee number.

- **userName** — Must be unique within the server. Email addresses are common here, but the trap is that emails can change.

- **active** — The heart of deprovisioning. The common pattern is soft-delete: set active to false instead of issuing DELETE.

- **meta.version** — The ETag value, used for concurrency control.

The Group schema

{

"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],

"id": "e9e30dba-f08f-4109-8486-d5c6a331660a",

"displayName": "platform-team",

"members": [

{

"value": "2819c223-7f76-453a-919d-413861904646",

"display": "yjkim@example.com",

"type": "User"

}

]

}

Group looks simple but is the most painful resource in practice. Some IdPs replace the entire members list of a several-thousand-member group with a single PUT, while others add/remove members one at a time via PATCH — your server implementation has to survive both.

Enterprise User extension and custom extensions

HR attributes (department, employee number, manager, and so on) are expressed via the Enterprise User extension schema.

{

"schemas": [

"urn:ietf:params:scim:schemas:core:2.0:User",

"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"

],

"userName": "yjkim@example.com",

"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {

"employeeNumber": "10042",

"department": "Platform Engineering",

"costCenter": "CC-3120",

"manager": {

"value": "26118915-6090-4610-87e4-49d8ca9f808d",

"displayName": "Jane Doe"

}

}

}

If you need your own attributes, you can define a custom extension schema under your own URN namespace, advertised by the server at the /Schemas endpoint. However, custom extensions are only usable in practice if the IdP attribute-mapping UI supports them, so staying within the Enterprise extension whenever possible is the safer compatibility bet.

The Protocol — REST Endpoints with HTTP Examples

The full list of endpoints defined by RFC 7644:

| Method | Path | Purpose |

| --- | --- | --- |

| POST | /Users | Create a user |

| GET | /Users | List + filter search |

| GET | /Users/id | Retrieve one user |

| PUT | /Users/id | Full replacement |

| PATCH | /Users/id | Partial modification |

| DELETE | /Users/id | Delete |

| GET | /ServiceProviderConfig | Advertise server capabilities |

| GET | /Schemas | Advertise schemas |

| GET | /ResourceTypes | Advertise resource types |

| POST | /Bulk | Bulk operations (optional) |

Creating a user — POST

POST /scim/v2/Users HTTP/1.1

Host: api.example.com

Authorization: Bearer eyJhbGciOiJSUzI1NiIs...

Content-Type: application/scim+json

{

"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],

"externalId": "emp-10042",

"userName": "yjkim@example.com",

"name": { "givenName": "Youngju", "familyName": "Kim" },

"emails": [{ "value": "yjkim@example.com", "type": "work", "primary": true }],

"active": true

}

On success the server returns 201 Created together with the full resource including the populated id and meta. If the userName already exists, the server must respond with 409 Conflict carrying a SCIM error body.

HTTP/1.1 409 Conflict

Content-Type: application/scim+json

{

"schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],

"scimType": "uniqueness",

"detail": "userName yjkim@example.com already exists",

"status": "409"

}

Filtered search — GET with filter queries

Before pushing, the IdP checks "does this user already exist?" using a filter. This is the most frequently invoked pattern.

GET /scim/v2/Users?filter=userName%20eq%20%22yjkim%40example.com%22 HTTP/1.1

Host: api.example.com

Authorization: Bearer eyJhbGciOiJSUzI1NiIs...

Decoded, the filter reads:

filter=userName eq "yjkim@example.com"

The RFC 7644 filter grammar supports the operators eq, ne, co (contains), sw (starts with), gt, ge, lt, le, pr (present), plus and/or/not combinators, parentheses, and complex attribute paths.

filter=emails[type eq "work" and value co "@example.com"]

filter=meta.lastModified gt "2026-06-01T00:00:00Z"

filter=userName sw "yj" and active eq true

In reality, though, the filters real IdPs send boil down to two: "userName eq ..." and "externalId eq ...". When implementing a server, the pragmatic strategy is to support those two patterns flawlessly first and expand the rest incrementally rather than chasing full grammar coverage.

Partial modification — PATCH

PATCH is the most complex part of SCIM. It carries a list of operations, each with one of three ops: add / remove / replace.

PATCH /scim/v2/Users/2819c223-7f76-453a-919d-413861904646 HTTP/1.1

Host: api.example.com

Authorization: Bearer eyJhbGciOiJSUzI1NiIs...

Content-Type: application/scim+json

{

"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],

"Operations": [

{ "op": "replace", "path": "active", "value": false },

{ "op": "replace", "path": "name.familyName", "value": "Lee" },

{

"op": "add",

"path": "emails",

"value": [{ "value": "yj.lee@example.com", "type": "work" }]

}

]

}

Group membership manipulation uses paths containing a value filter.

{

"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],

"Operations": [

{

"op": "remove",

"path": "members[value eq \"2819c223-7f76-453a-919d-413861904646\"]"

},

{

"op": "add",

"path": "members",

"value": [{ "value": "08b0fe34-0ec4-4857-b8e2-58dbccca2f48" }]

}

]

}

Full replacement — PUT

PUT replaces the entire resource. It is simple but dangerous: it can wipe out attributes the client does not know about (attributes the server manages internally). The server must therefore preserve readOnly/immutable attributes regardless of what arrives in the PUT body.

Push Model vs Pull Model

| Aspect | Push (IdP to SP) | Pull (SP from IdP/HR) |

| --- | --- | --- |

| Direction | IdP calls the SP SCIM API on change | SP polls the source periodically |

| Latency | Near real time (event driven) | Bound by polling interval (tens of minutes to hours) |

| Typical examples | Okta and Entra ID SaaS provisioning | Parts of Entra ID on-prem HR integration |

| SP implementation burden | Must implement a SCIM server | Must implement a SCIM client |

| Failure handling | Relies on IdP retry queues | Self-heals on next poll |

The industry mainstream is the **push model**. Both Okta and Entra ID detect change events in their directories and push them to the SP SCIM endpoint. An interesting nuance is how Entra ID behaves: it is not pure event push but accumulates changes within a synchronization cycle of roughly 40 minutes. Most "Entra provisioning is not applying immediately" support tickets come down to this cycle.

In the push model, the one thing the SP absolutely must consider is **idempotency**. The IdP retries the same request on network errors, so two POSTs with the same externalId must never produce duplicate accounts.

SCIM Support in Major IdPs (2026)

Okta

- **As a client**: supports SCIM push for thousands of OIN (Okta Integration Network) apps; custom apps can integrate via the SCIM 2.0 template.

- **As a server**: Okta itself exposes a SCIM server API, allowing external systems to manage Okta users via SCIM.

- Group Push is a separate feature, and group membership synchronization is PATCH based.

Microsoft Entra ID (formerly Azure AD)

- The provisioning feature of enterprise apps is built on SCIM 2.0. Both gallery apps and the "non-gallery app + SCIM endpoint" combination are supported.

- There is a sync cycle (about 40 minutes) with a distinction between initial and incremental sync, and On-demand provisioning lets you push a specific user immediately for debugging.

- The Entra SCIM client has historically been famous for subtle deviations from the spec (for example, past PATCH format issues). Entra compatibility testing is mandatory for any server implementation.

Keycloak

- **Even as of Keycloak 26.6.x (26.6.2 as of May 2026), SCIM is not a core feature.** If you assume "we use Keycloak, so SCIM just works," you are in for a surprise.

- You have three options: (1) turn Keycloak into a SCIM server via community extensions (scim-for-keycloak and others), (2) implement your own SCIM client driven by the Keycloak event listener SPI, (3) use the Workflows feature in Keycloak 26.6 to replace some in-realm lifecycle automation.

- [Workflows](https://www.keycloak.org/docs/latest/release_notes/index.html) in Keycloak 26.6 is useful for in-realm automation such as "automatically disable inactive users," but provisioning to external SPs still requires a separate implementation.

The skeleton of a push-style integration using the event listener SPI looks like this.

public class ScimPushEventListener implements EventListenerProvider {

private final ScimClient scimClient;

@Override

public void onEvent(AdminEvent event, boolean includeRepresentation) {

if (event.getResourceType() != ResourceType.USER) {

return;

}

switch (event.getOperationType()) {

case CREATE -> scimClient.createUser(toScimUser(event));

case UPDATE -> scimClient.patchUser(toScimPatch(event));

case DELETE -> scimClient.deactivateUser(extractUserId(event));

}

}

@Override

public void close() {

// no-op

}

}

In production you should pair this with an outbox table and a retry queue to guard against event loss.

Traps When Implementing a SCIM Server

Trap 1 — The complexity of PATCH semantics

PATCH accounts for eighty percent of SCIM implementation difficulty.

- **PATCH without a path**: when an op omits path, the value becomes a "partial resource" that must be merged attribute by attribute. This was a pattern Entra ID was fond of.

- **Value filter paths**: paths like members[value eq "..."] effectively require a mini query-language parser.

- **Multi-valued attribute semantics**: does add on emails append or replace, and what happens when two entries claim primary? You must handle these exactly per the spec (RFC 7643 section 2.4).

- **Case sensitivity**: SCIM attribute names are case-insensitive. "userName" and "username" must be treated identically. A surprising number of implementations miss this.

Rather than writing your own parser, use a proven library (for Java, the UnboundID SCIM 2 SDK and similar).

Trap 2 — ETags and concurrency

If the HR system and an IdP administrator modify the same user concurrently, the last write wins (lost update). SCIM defines ETag-based optimistic locking for this.

PUT /scim/v2/Users/2819c223-7f76-453a-919d-413861904646 HTTP/1.1

If-Match: W/"3694e05e9dff590"

Content-Type: application/scim+json

If the version differs, the server returns 412 Precondition Failed. In reality, though, most IdP clients never send If-Match, so the right call is to implement ETags as "supported but not enforced" and advertise that honestly in ServiceProviderConfig.

{

"schemas": ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],

"patch": { "supported": true },

"bulk": { "supported": false, "maxOperations": 0, "maxPayloadSize": 0 },

"filter": { "supported": true, "maxResults": 200 },

"etag": { "supported": true },

"changePassword": { "supported": false },

"sort": { "supported": false },

"authenticationSchemes": [

{

"type": "oauthbearertoken",

"name": "OAuth Bearer Token",

"description": "Authorization header with Bearer token"

}

]

}

Trap 3 — Pagination

SCIM default pagination is startIndex based, starting at 1.

GET /scim/v2/Users?startIndex=101&count=100 HTTP/1.1

{

"schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],

"totalResults": 5042,

"startIndex": 101,

"itemsPerPage": 100,

"Resources": []

}

The traps:

- startIndex starts at **1**, not 0 — a classic source of off-by-one bugs.

- Because it is offset based, data changing mid-pagination causes omissions or duplicates. This is the root cause of subtle inconsistencies during large syncs. Cursor-based pagination extensions have been discussed in drafts, but startIndex must still be supported for client compatibility.

- Computing an exact totalResults can make count queries expensive on large tables. Factor this into your DB index design.

Trap 4 — Error response format

SCIM clients branch on the scimType in the error body. If you do not return 401/403/404/409/412 in the exact SCIM error format, some IdPs decide the endpoint is "unhealthy" and quarantine provisioning. The Entra ID quarantine state is the canonical example.

Deprovisioning and Security

Leaver handling is the most security-critical flow. The recommended design:

[HR: termination processed]

|

v

[IdP: deactivate account] --> SCIM PATCH active=false --> [SP: soft-delete]

| |

v v

[Revoke all IdP sessions] [Revoke SP sessions/refresh tokens]

[Invalidate API keys, PATs]

[Hard-delete after retention period]

Core principles:

1. **Prefer active=false over DELETE** — audit trails and (legally required) data retention make immediate hard deletes a bad idea. Most IdPs default to "deactivate" as well.

2. **SCIM does not kill sessions** — active=false only blocks "new authentication." When the SP receives this signal, it must proactively revoke the user's active sessions and refresh tokens. The complementary standard filling this gap is the OpenID [Shared Signals Framework / CAEP](https://openid.net/wg/sharedsignals/), which major vendors are steadily adopting as of 2026.

3. **Do not forget non-human identities** — service accounts, bot tokens, and AI-agent delegations created by the departed employee often fall outside SCIM scope. Design a separate ownership-transfer process.

HR-driven Provisioning Architecture

The end state for mature organizations is to treat the HR system as the single source of truth.

+-----------+ +---------------------+ +------------------------+

| HR system | --> | IdP | --> | SaaS app 1 (SCIM server)|

| (Workday | | (Okta / Entra / | --> | SaaS app 2 (SCIM server)|

| etc.) | | Keycloak) | --> | Internal (SCIM server)|

+-----------+ +---------------------+ +------------------------+

join/move/leave group/role mapping account/role updates

events published rules (dept -> group session/token revocation

-> app role)

Design points:

- **Centralize attribute mapping rules**: manage mappings like "department code 3120 means group platform-team, and platform-team means the admin role in app X" in one place — the IdP.

- **Pre-stage start/end dates**: HR data contains future hire and termination dates. The ideal is to create accounts before the start date but keep them inactive, and auto-deactivate at midnight on the termination date.

- **Exception flows**: contractors, partners, and organizations absorbed through acquisitions often do not exist in the HR system; they need a separate registration path and expiry policy.

Testing Strategy

A test matrix to run through before shipping a SCIM server:

| Category | Test items |

| --- | --- |

| Create | Happy-path POST, duplicate userName 409, missing required attribute 400 |

| Read | userName/externalId filters, case insensitivity, URL encoding |

| Modify | PATCH add/remove/replace, PATCH without path, value filter paths |

| Replace | readOnly attributes preserved on PUT, handling of omitted attributes |

| Deactivate | active=false triggers session/token revocation |

| Pagination | startIndex starting at 1, boundary values, empty results |

| Concurrency | ETag mismatch 412, concurrent PATCH |

| Auth | Expired token 401, insufficient privileges 403 |

| Idempotency | Retried identical POST, re-applied identical PATCH |

On the tooling side:

- **Microsoft SCIM Validator**: automatically checks Entra compatibility.

- **Okta SCIM tests (Runscope collection based)**: mandatory pass items before OIN listing.

- **curl smoke tests**: a minimal verification example that fits nicely in CI.

#!/usr/bin/env bash

set -euo pipefail

BASE="https://api.example.com/scim/v2"

TOKEN="$SCIM_TEST_TOKEN"

1. Create

USER_ID=$(curl -sf -X POST "$BASE/Users" \

-H "Authorization: Bearer $TOKEN" \

-H "Content-Type: application/scim+json" \

-d @fixtures/user-create.json | jq -r '.id')

2. Filtered lookup

curl -sf "$BASE/Users?filter=userName%20eq%20%22scim-test%40example.com%22" \

-H "Authorization: Bearer $TOKEN" | jq -e '.totalResults == 1'

3. Deactivate

curl -sf -X PATCH "$BASE/Users/$USER_ID" \

-H "Authorization: Bearer $TOKEN" \

-H "Content-Type: application/scim+json" \

-d '{"schemas":["urn:ietf:params:scim:api:messages:2.0:PatchOp"],

"Operations":[{"op":"replace","path":"active","value":false}]}' \

| jq -e '.active == false'

4. Clean up

curl -sf -X DELETE "$BASE/Users/$USER_ID" -H "Authorization: Bearer $TOKEN"

echo "SCIM smoke test passed"

The most reliable approach is to keep real IdP tenants (an Okta developer org, an Entra test tenant) wired to your staging environment and run regression tests with traffic from real IdPs on every release. IdP-specific implementation quirks will never be fully captured by reading documentation alone.

Operational Best Practices

- **Authenticate with OAuth Bearer tokens, and make tokens rotatable**: operating on a single long-lived static token means one leak exposes the entire directory. Establish token rotation procedures and expiry monitoring.

- **Give the SCIM endpoint its own rate limits and audit logs**: record who (which IdP tenant) changed what and when. This is standard evidence for SOC 2 / ISO 27001 audits.

- **Detect provisioning drift**: run a periodic batch that reconciles the IdP user list against the SP user list. Assume push events will be lost — because they will.

- **Make schema changes backward compatible**: avoid breaking attribute mappings of customers already integrated; isolate attribute removals or semantic changes behind a new version path.

Closing Thoughts

SCIM 2.0 is not a glamorous technology. But if SSO is the "front door," SCIM is the "roster management" — and most security incidents originate in the roster, not the door. In the enterprise B2B market of 2026, SCIM support has long since joined SSO on the checklist for enterprise deals.

To summarize:

1. SSO and provisioning are separate problems; identity-first security only holds once the entire JML lifecycle is automated.

2. Understand SCIM 2.0 along its three axes: RFC 7642 (concepts), 7643 (schema), 7644 (protocol).

3. The hard parts of a server implementation are PATCH semantics, pagination, error formats, and idempotency. Tame them with proven SDKs and tests against real IdPs.

4. Deprovisioning does not end at active=false. Design through to session and token revocation, and look toward combining with event standards such as Shared Signals/CAEP in the long run.

In the next post, we will cover how to design per-customer SSO in a multi-tenant SaaS, and how to make SCIM onboarding self-service on top of it.

References

- [RFC 7642 — SCIM: Definitions, Overview, Concepts, and Requirements](https://datatracker.ietf.org/doc/html/rfc7642)

- [RFC 7643 — SCIM: Core Schema](https://datatracker.ietf.org/doc/html/rfc7643)

- [RFC 7644 — SCIM: Protocol](https://datatracker.ietf.org/doc/html/rfc7644)

- [RFC 6749 — The OAuth 2.0 Authorization Framework](https://datatracker.ietf.org/doc/html/rfc6749)

- [RFC 9700 — Best Current Practice for OAuth 2.0 Security](https://datatracker.ietf.org/doc/html/rfc9700)

- [Okta — SCIM protocol documentation](https://developer.okta.com/docs/reference/scim/)

- [Microsoft Entra ID — Develop a SCIM endpoint guide](https://learn.microsoft.com/entra/identity/app-provisioning/use-scim-to-provision-users-and-groups)

- [Keycloak Documentation](https://www.keycloak.org/documentation)

- [Keycloak release notes (26.6 Workflows and more)](https://www.keycloak.org/docs/latest/release_notes/index.html)

- [OpenID Shared Signals Framework / CAEP](https://openid.net/wg/sharedsignals/)

- [UnboundID SCIM 2 SDK for Java](https://github.com/pingidentity/scim2)

현재 단락 (1/343)

When people talk about enterprise IAM, most of the discussion gravitates toward SSO (Single Sign-On)...

작성 글자: 0원문 글자: 21,115작성 단락: 0/343