Docstash
SCIM 2.0

SCIM 2.0 Overview

What is SCIM?

SCIM 2.0 (System for Cross-domain Identity Management, RFC 7643/7644) is a REST API standard for managing users and groups in a directory. It is the dominant protocol for Identity Provider (IdP) to Service Provider (SP) identity provisioning — think Okta/Auth0/Azure AD syncing users to SaaS apps.

The core value: a standard wire format so any SCIM-compatible client can manage identities without custom integrations.

Why SCIM 2.0 in a Provisioning Flow?

Before SCIM, provisioning was a custom integration nightmare:

HR System (Workday) → Custom connector → Salesforce      ← Unique per vendor
HR System (Workday) → Custom connector → ServiceNow       ← Unique per vendor
HR System (Workday) → Custom connector → AWS IAM          ← Unique per vendor
... dozens more

Every vendor had its own user schema, its own API shape, its own attribute names. Identity teams wrote and maintained N×M integrations (N apps × M identity sources). A new SaaS app meant a 6-week integration project.

SCIM's Value Proposition

SCIM collapses this to a single, standard interface per target system:

HR System (any) → SCIM 2.0 → Target System (any SCIM-compliant app)

              Universal adapter: SailPoint, Okta, Azure AD
ProblemBefore SCIMWith SCIM
Schema mappingCustom per vendorStandard — userName, displayName, emails are universal
API shapeCustom per vendorREST + JSON — same patterns everywhere
AuthCustom per vendorBearer token (OAuth2) — same everywhere
Delta syncCustom polling logiclastModified timestamp + active flag
OffboardingCustom processSoft deactivate (active: false) — standard
Conflict detectionCustom logic409 Conflict with standard error body

Real Apps That Speak SCIM

These are just a selection — most modern SaaS platforms support SCIM out of the box:

AppSCIM Use Case
SalesforceProvision users + assign permission sets via SCIM
WorkdayActs as SCIM provider to downstream apps
SlackProvision members, manage workspaces, sync userName → Slack handle
ZoomAuto-provision users, sync display name, deprovision on offboard
GitHubOrganization membership, team assignment (via SCIM 2.0 beta)
Microsoft 365 / Azure ADFull user/group sync across the M365 ecosystem
ServiceNowITSM user provisioning, group-based role assignment
SplunkUser provisioning for analytics platform roles
DatadogTeam provisioning and RBAC sync
AWS IAM Identity CenterFederation + SCIM-based user/group sync to AWS accounts
OktaActs as SCIM provider AND SCIM client (双向 SCIM)
Azure AD (Entra)SCIM 2.0 provisioning to thousands of SaaS apps

The Key Insight: Schema Normalization at the IdP

The IdP (SailPoint, Okta, etc.) owns the semantic transformation:

Workday says:  employeeType = "FTE", costCenter = "CC-12345", managerId = "WD-00001"
SailPoint transforms → SCIM standard attributes:
  → userName = "jane.smith"
  → department = "Engineering"        (from HR cost center mapping)
  → title = "Senior Engineer"        (from HR job title)
  → emails[0].value = "jane@corp.com"

This means the target system only needs to speak SCIM — it doesn't need to understand Workday, SAP, or any specific HCM. The IdP handles all the complexity of mapping from whatever the HR system calls things into standard SCIM attribute names.

SCIM Is the Common Denominator

Target apps only need to implement ONE standard interface
rather than N different vendor-specific connectors.

For the IAM Protocol Engine, being a SCIM provider means:

  • Any SCIM-compatible IdP (SailPoint, Okta, Azure AD, JumpCloud, Gluu, etc.) can provision users/groups into it
  • No per-vendor custom code needed on the target side
  • Federation becomes "speak SCIM" — not "build a custom connector for every IdP"

Joiner / Mover / Leaver Lifecycle

Joiner   → POST   /scim/v2/Users        # New employee joins
Mover    → PUT    /scim/v2/Users/{id}    # Employee changes role/department
Leaver   → DELETE /scim/v2/Users/{id}    # Employee departs

Group changes via PATCH /scim/v2/Groups/{id}

Endpoints

Client (Admin UI, SCIM-compatible IdP)

    ├── Users
    │   ├── POST   /scim/v2/Users         Create user   (joiner)
    │   ├── GET    /scim/v2/Users         List users    (with filter, pagination)
    │   ├── GET    /scim/v2/Users/{uuid}  Get user
    │   ├── PUT    /scim/v2/Users/{uuid}  Replace user  (mover)
    │   └── DELETE /scim/v2/Users/{uuid}  Delete user   (leaver)

    ├── Groups
    │   ├── POST   /scim/v2/Groups         Create group
    │   ├── GET    /scim/v2/Groups         List groups
    │   ├── GET    /scim/v2/Groups/{uuid}  Get group
    │   ├── PATCH  /scim/v2/Groups/{uuid}  Modify members (add/remove)
    │   └── DELETE /scim/v2/Groups/{uuid}  Delete group

    └── Both resources support:
        • ?filter=userName eq "..." or displayName eq "..."
        • ?startIndex=1&count=10  (pagination, 1-based)
        • Location header on create (RFC 7644 §5.2)
        • Bearer token auth on all requests

Resource Types

User (RFC 7643 §5.1)

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "userName": "john.doe",
  "displayName": "John Doe",
  "name": { "givenName": "John", "familyName": "Doe" },
  "emails": [{ "value": "john@example.com", "primary": true }],
  "active": true,
  "groups": [],
  "meta": {
    "resourceType": "User",
    "created": "2026-04-14T10:00:00Z",
    "lastModified": "2026-04-14T10:00:00Z",
    "location": "http://localhost:8080/scim/v2/Users/550e8400-e29b-41d4-a716-446655440000"
  }
}

Group (RFC 7643 §5.2)

{
  "id": "661f9500-f39c-52e5-b827-557866551111",
  "displayName": "Engineering",
  "members": [
    { "value": "550e8400-e29b-41d4-a716-446655440000", "type": "User" }
  ],
  "meta": {
    "resourceType": "Group",
    "created": "2026-04-14T10:05:00Z",
    "lastModified": "2026-04-14T10:05:00Z",
    "location": "http://localhost:8080/scim/v2/Groups/661f9500-f39c-52e5-b827-557866551111"
  }
}

Authentication

All SCIM endpoints require a Bearer token in the Authorization header. SCIM itself does not define an authentication mechanism — the hosting environment handles it. This implementation validates the token against the token table (same as OAuth 2.0 resource protection).

Authorization: Bearer <access_token>

RFCs Implemented

RFCTitleCoverage
RFC 7643SCIM Core SchemaUser and Group resource types, attribute definitions
RFC 7644SCIM ProtocolREST API, filtering, PATCH operations, error responses

Key Design Decisions

Bearer token auth on all endpoints. SCIM has no native auth mechanism — authentication is delegated to the hosting environment. Tokens are validated via the same TokenRepository used by OAuth 2.0.

Entities in auth-core, not scim module. ScimUser and ScimGroup JPA entities live in auth-core because they represent the canonical identity store. The scim module provides only the SCIM protocol layer (controller + DTOs).

Comma-separated members. Group membership is stored as a comma-separated string of UUIDs on the ScimGroup entity — consistent with the project's "no JSON columns for simple arrays" principle.

Filter parsing is simple. Only single-condition userName eq "..." and displayName eq "..." are implemented. Full SCIM filter grammar (RFC 7644 §3) is not in scope.

PATCH is idempotent for add. Adding the same user twice to a group is a no-op due to Set deduplication before persist.

Error Responses

SCIM uses HTTP status codes with a consistent error body (RFC 7644 §4.4):

{
  "schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],
  "status": 400,
  "detail": "userName is required"
}
StatusMeaning
201Resource created (POST)
200Success (GET, PUT, PATCH)
204No Content (DELETE)
400Bad Request — validation failed
401Unauthorized — invalid/missing token
404Not Found
409Conflict — unique constraint violation

Real-World Flow: SailPoint → SCIM → Target System

In enterprise identity governance, a SCIM请求 flows like this:

┌──────────────┐         SCIM v2 (RFC 7644)          ┌──────────────────┐
│   SailPoint   │ ────── Provisioning Agent ─────────→│  IAM Protocol    │
│   (IdP/IGA)   │                                      │  Engine          │
│              │ ←──── Access Token (OAuth2) ──────────│                  │
└──────────────┘                                      └────────┬─────────┘

                                                               │ PostgreSQL
                                                               │ writes to

                                                    ┌──────────────────┐
                                                    │  Target System   │
                                                    │  (Workday, SF,   │
                                                    │   ServiceNow)    │
                                                    └──────────────────┘

Step-by-Step: New Hire Provisioning

Step 1 — IT initiates hire in Workday (or any HCM system). SailPoint picks up the event via connector.

Step 2 — SailPoint evaluates access policies

Rule: "All Engineering employees get GitHub org membership + AWS read-only"
  → Applies to: user.department == "Engineering"

Step 3 — SailPoint sends SCIM create request to IAM Protocol Engine

POST /scim/v2/Users
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...
Content-Type: application/json

{
  "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
  "userName": "jane.smith",
  "displayName": "Jane Smith",
  "name": {
    "givenName": "Jane",
    "familyName": "Smith"
  },
  "emails": [{ "value": "jane.smith@example.com", "primary": true }],
  "active": true,
  "externalId": "WD-2026-00442"    ← Workday's unique ID (SailPoint tracks this)
}

Step 4 — IAM Protocol Engine persists to PostgreSQL

@PostMapping("/Users")
public Object createUser(@RequestBody ScimUserDto dto,
                         @RequestHeader("Authorization") String auth) {
    // validate Bearer token
    // check userName uniqueness → 409 if conflict
    // persist to scim_user table
    // return 201 + Location header
}

Step 5 — IAM Protocol Engine responds to SailPoint

HTTP/1.1 201 Created
Location: http://localhost:8080/scim/v2/Users/7c9e6679-7425-40de-944b-e07fc1f90ae7
Content-Type: application/json

{
  "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "userName": "jane.smith",
  "displayName": "Jane Smith",
  "meta": {
    "resourceType": "User",
    "created": "2026-04-14T10:00:00Z",
    "lastModified": "2026-04-14T10:00:00Z",
    "location": "http://localhost:8080/scim/v2/Users/7c9e6679-7425-40de-944b-e07fc1f90ae7"
  }
}

Step 6 — SailPoint maps the SCIM id to its internal provisioning record

SailPoint stores externalId = WD-2026-00442 (Workday ID) and id = 7c9e6679... (IAM Protocol Engine ID). On future updates/deletions, SailPoint uses the IAM Protocol Engine id as the SCIM id.

Group Provisioning: SailPoint → IAM Protocol Engine → Target Groups

Step 7 — SailPoint assigns group membership

SailPoint computes group membership based on HR attribute (department, manager, costCenter). For the "Engineering" group:

PATCH /scim/v2/Groups/661f9500-f39c-52e5-b827-557866551111
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...
Content-Type: application/json

[
  { "op": "add", "members": [{ "value": "7c9e6679-7425-40de-944b-e07fc1f90ae7" }] }
]

Step 8 — IAM Protocol Engine updates group membership

Group membership stored as comma-separated UUIDs in PostgreSQL: "7c9e6679-7425..., other-uuid-here"

Offboarding: SailPoint → Deactivate User

When Jane leaves (HR system fires event → SailPoint detects it):

PUT /scim/v2/Users/7c9e6679-7425-40de-944b-e07fc1f90ae7
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...

{
  "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
  "userName": "jane.smith",
  "active": false          ← Soft deactivate (not DELETE)
}

The user record is retained (not hard-deleted) for audit purposes. SailPoint also pushes the deactivation to all target systems (Workday, AWS, GitHub, etc.) through their respective provisioning agents.

Why This Architecture Works

ConcernHow SailPoint handles it
Credential syncSailPoint generates/rotates passwords for target apps via its credential vault
Role-to-group mappingPolicy engine evaluates on every identity event, not just at provisioning
Conflict resolutionIf userName already exists → 409 → SailPoint retries as update
Audit trailSailPoint logs every provisioning decision; IAM Protocol Engine logs every SCIM call
Delta syncSCIM PATCH avoids full replacement; only changed attributes sent

See Also

On this page