SCIM Provisioning Implementation Guide
A practical guide to implementing SCIM-based automated user provisioning and deprovisioning, covering the SCIM protocol, lifecycle management, vendor integration, and troubleshooting common issues.
Manual user provisioning is a liability. When an employee joins the organization, IT manually creates accounts in 10 different applications. When they transfer departments, maybe half the access changes are made. When they leave, their accounts linger for weeks or months because nobody remembers every system they had access to. Each gap is a security risk and a compliance violation.
SCIM (System for Cross-domain Identity Management) solves this by providing a standardized protocol for automating user provisioning and deprovisioning between an Identity Provider and Service Providers. When HR creates an employee record, SCIM automatically creates accounts in downstream applications. When the employee is terminated, SCIM disables or deletes those accounts immediately.
This guide walks you through implementing SCIM-based provisioning, from protocol fundamentals to production deployment.
What You Will Learn
- How the SCIM 2.0 protocol works (endpoints, schemas, operations)
- Setting up SCIM provisioning from your IdP to SaaS applications
- Building a SCIM server for custom applications
- Managing the complete user lifecycle (join, move, leave)
- Handling SCIM edge cases, conflicts, and failures
Prerequisites
- Identity Provider with SCIM support — Okta, Azure AD, OneLogin, Ping Identity, or another IdP that acts as a SCIM client.
- Target applications with SCIM support — SaaS applications (Slack, Salesforce, GitHub, Zoom) that expose SCIM server endpoints.
- User directory — An authoritative source for user attributes, typically HR or Active Directory.
- Network connectivity — The IdP must reach the SCIM endpoints of each application (typically over HTTPS on port 443).
- Application admin access — You need admin credentials for each target application to configure SCIM integration.
Architecture Overview
SCIM provisioning involves two parties:
- SCIM Client (IdP): Initiates provisioning requests. Your IdP maintains the authoritative user list and pushes changes to applications.
- SCIM Server (Application): Receives provisioning requests and creates, updates, or deletes user accounts accordingly.
SCIM protocol basics:
SCIM 2.0 (RFC 7643, 7644) defines a REST API with standard endpoints:
| Endpoint | Method | Description |
|----------|--------|-------------|
| /Users | GET | List/search users |
| /Users | POST | Create a user |
| /Users/{id} | GET | Get a specific user |
| /Users/{id} | PUT | Replace a user |
| /Users/{id} | PATCH | Update specific attributes |
| /Users/{id} | DELETE | Delete a user |
| /Groups | GET | List/search groups |
| /Groups | POST | Create a group |
| /Groups/{id} | PATCH | Update group membership |
SCIM User Schema:
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"userName": "jsmith@example.com",
"name": {
"givenName": "Jane",
"familyName": "Smith"
},
"emails": [
{
"value": "jsmith@example.com",
"type": "work",
"primary": true
}
],
"active": true,
"groups": [],
"title": "Software Engineer",
"department": "Engineering",
"externalId": "EMP001234"
}
Provisioning flow:
- HR system creates a new employee record.
- HR data syncs to the IdP (via SCIM, HRIS connector, or manual entry).
- IdP evaluates provisioning rules and determines which applications the employee needs access to.
- IdP sends SCIM POST requests to each application to create user accounts.
- IdP sends SCIM PATCH requests to add the user to appropriate groups.
- When the employee's attributes change (title, department), the IdP sends SCIM PUT/PATCH requests to update the accounts.
- When the employee is terminated, the IdP sends SCIM PATCH (to deactivate) or DELETE requests.
Step-by-Step Implementation
Step 1: Configure SCIM on Target Applications
Each SaaS application has its own SCIM setup process. The general pattern is:
- Navigate to the application's admin console.
- Find the SCIM/provisioning settings.
- Generate a SCIM API token (bearer token for authentication).
- Note the SCIM base URL (e.g.,
https://api.slack.com/scim/v2). - Configure which attributes the application accepts.
Example: Configuring Slack SCIM:
SCIM Base URL: https://api.slack.com/scim/v2
Authentication: Bearer token (generated in Slack admin)
Supported operations: Create, Update, Deactivate (no hard delete)
Required attributes: userName, name.givenName, name.familyName, emails
Optional attributes: title, department, profileUrl
Step 2: Configure SCIM on the IdP
In your IdP, create an application integration with SCIM provisioning:
Okta example:
- Go to Applications > Add Application.
- Select the target application (e.g., Slack).
- Under the Provisioning tab, enable "API Integration."
- Enter the SCIM base URL and API token.
- Test the connection.
- Enable provisioning features:
- Create Users
- Update User Attributes
- Deactivate Users
- Configure attribute mappings.
Attribute mapping configuration:
attribute_mappings:
# IdP attribute -> SCIM attribute
- source: "user.login"
target: "userName"
apply_on: "create"
- source: "user.firstName"
target: "name.givenName"
apply_on: "create, update"
- source: "user.lastName"
target: "name.familyName"
apply_on: "create, update"
- source: "user.email"
target: "emails[type eq 'work'].value"
apply_on: "create, update"
- source: "user.title"
target: "title"
apply_on: "create, update"
- source: "user.department"
target: "department"
apply_on: "create, update"
- source: "user.status"
target: "active"
mapping:
"ACTIVE": true
"DEPROVISIONED": false
apply_on: "create, update"
Step 3: Configure Group Provisioning
Group provisioning allows you to manage application-level groups from the IdP:
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
"displayName": "Engineering Team",
"members": [
{ "value": "user-id-1", "display": "Jane Smith" },
{ "value": "user-id-2", "display": "John Doe" }
]
}
Adding a user to a group (PATCH):
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "add",
"path": "members",
"value": [
{ "value": "new-user-id" }
]
}
]
}
Configure the IdP to push group memberships to the application. This enables group-based access control in the target application without manual group management.
Step 4: Build a SCIM Server for Custom Applications
For internal or custom applications that do not have built-in SCIM support, build a SCIM server:
from flask import Flask, request, jsonify
import uuid
app = Flask(__name__)
# In-memory user store (replace with database in production)
users = {}
@app.route('/scim/v2/Users', methods=['POST'])
def create_user():
"""Create a new user via SCIM."""
data = request.json
# Validate required fields
if not data.get('userName'):
return scim_error("userName is required", 400)
# Check for duplicate
for uid, user in users.items():
if user['userName'] == data['userName']:
return scim_error("User already exists", 409)
user_id = str(uuid.uuid4())
user = {
'id': user_id,
'externalId': data.get('externalId'),
'userName': data['userName'],
'name': data.get('name', {}),
'emails': data.get('emails', []),
'active': data.get('active', True),
'title': data.get('title'),
'department': data.get('department'),
'meta': {
'resourceType': 'User',
'created': datetime.utcnow().isoformat() + 'Z',
'lastModified': datetime.utcnow().isoformat() + 'Z',
'location': f'/scim/v2/Users/{user_id}'
}
}
users[user_id] = user
# Provision the user in the actual application
provision_user_in_app(user)
return jsonify({
'schemas': ['urn:ietf:params:scim:schemas:core:2.0:User'],
**user
}), 201
@app.route('/scim/v2/Users/<user_id>', methods=['PATCH'])
def patch_user(user_id):
"""Update user attributes via SCIM PATCH."""
if user_id not in users:
return scim_error("User not found", 404)
data = request.json
user = users[user_id]
for operation in data.get('Operations', []):
op = operation['op'].lower()
path = operation.get('path')
value = operation.get('value')
if op == 'replace':
if path == 'active':
user['active'] = value
if not value:
deactivate_user_in_app(user)
else:
reactivate_user_in_app(user)
elif path:
set_nested_value(user, path, value)
elif op == 'add':
if path:
add_nested_value(user, path, value)
user['meta']['lastModified'] = datetime.utcnow().isoformat() + 'Z'
update_user_in_app(user)
return jsonify({
'schemas': ['urn:ietf:params:scim:schemas:core:2.0:User'],
**user
})
@app.route('/scim/v2/Users', methods=['GET'])
def list_users():
"""List or search users via SCIM."""
filter_param = request.args.get('filter')
start_index = int(request.args.get('startIndex', 1))
count = int(request.args.get('count', 100))
result_users = list(users.values())
# Basic filter support
if filter_param:
result_users = apply_scim_filter(result_users, filter_param)
total = len(result_users)
paginated = result_users[start_index - 1:start_index - 1 + count]
return jsonify({
'schemas': ['urn:ietf:params:scim:api:messages:2.0:ListResponse'],
'totalResults': total,
'startIndex': start_index,
'itemsPerPage': len(paginated),
'Resources': [{
'schemas': ['urn:ietf:params:scim:schemas:core:2.0:User'],
**u
} for u in paginated]
})
def scim_error(detail, status):
return jsonify({
'schemas': ['urn:ietf:params:scim:api:messages:2.0:Error'],
'detail': detail,
'status': status
}), status
Step 5: Implement Lifecycle Management
The complete user lifecycle has three phases:
Joiner (New hire):
joiner_workflow:
trigger: "HR creates employee record"
steps:
1. IdP creates user account from HR data
2. Role assignment engine determines application access
3. SCIM provisions user in each application
4. Group memberships are pushed via SCIM
5. Welcome email sent with access details
sla: "< 1 hour from HR entry to full access"
Mover (Transfer/promotion):
mover_workflow:
trigger: "HR updates department, title, or location"
steps:
1. IdP receives updated attributes
2. Role assignment engine re-evaluates roles
3. Roles no longer matching are removed
4. New matching roles are assigned
5. SCIM updates attributes in downstream applications
6. SCIM removes user from old groups, adds to new groups
7. Old application access is deprovisioned
8. New application access is provisioned
sla: "< 4 hours from HR update to access change"
Leaver (Termination):
leaver_workflow:
trigger: "HR marks employee as terminated"
steps:
1. IdP disables user account
2. All active sessions are revoked
3. SCIM sends PATCH {active: false} to all provisioned applications
4. Mailbox is converted to shared (or delegated to manager)
5. Audit log of all access is preserved
6. After 30 days, SCIM sends DELETE to remove accounts
sla: "< 15 minutes from termination to full access revocation"
Step 6: Set Up Monitoring and Reconciliation
SCIM provisioning can fail silently. Monitor and reconcile:
# Reconciliation job: runs daily
def reconcile_provisioning(app_name):
"""Compare IdP users with application users to detect drift."""
idp_users = idp_client.list_assigned_users(app_name)
app_users = scim_client.list_users(app_name)
idp_usernames = {u['userName'] for u in idp_users}
app_usernames = {u['userName'] for u in app_users}
# Users in IdP but not in app (provisioning failure)
missing_in_app = idp_usernames - app_usernames
for username in missing_in_app:
alert(f"User {username} assigned in IdP but missing in {app_name}")
# Auto-remediate: re-trigger provisioning
scim_client.create_user(app_name, get_user_data(username))
# Users in app but not in IdP (orphan accounts)
orphans = app_usernames - idp_usernames
for username in orphans:
alert(f"Orphan account {username} found in {app_name}")
# Flag for review (do not auto-delete without verification)
# Active/inactive mismatch
for user in idp_users:
app_user = find_in_list(app_users, user['userName'])
if app_user and user['active'] != app_user['active']:
alert(f"Status mismatch for {user['userName']} in {app_name}: "
f"IdP={user['active']}, App={app_user['active']}")
Configuration Best Practices
- Use externalId for matching. Set the
externalIdfield to the IdP's user identifier. This prevents duplicate creation when usernames change. - Map only necessary attributes. Do not push every attribute to every application. Map only what the application needs and accepts.
- Deactivate before delete. When a user leaves, first set
active: falseto disable the account. Delete the account only after a retention period (30-90 days) to allow for audit and potential re-hire. - Test with a pilot group. Enable SCIM provisioning for a small group first. Verify that accounts are created correctly before enabling for all users.
- Handle rate limits. Many SaaS SCIM endpoints have rate limits. Implement retry logic with exponential backoff in your provisioning pipeline.
- Log every SCIM operation. Record every CREATE, UPDATE, DELETE, and failed operation. This audit log is essential for troubleshooting and compliance.
Testing and Validation
- Create user test: Assign a test user to the application in the IdP. Verify that a SCIM POST is sent and the user appears in the application within minutes.
- Update user test: Change the test user's title in the IdP. Verify that a SCIM PATCH is sent and the title updates in the application.
- Deactivate user test: Unassign the test user from the application. Verify that a SCIM PATCH sets
active: falseand the user can no longer log in to the application. - Group membership test: Add the test user to a group in the IdP. Verify that the group membership is pushed to the application.
- Bulk provisioning test: Assign 100 users simultaneously. Verify that all are provisioned without errors or rate-limiting issues.
- Reconciliation test: Manually create an orphan account in the application. Run reconciliation and verify it is detected.
Common Pitfalls and Troubleshooting
| Problem | Cause | Solution | |---------|-------|----------| | Duplicate users created | externalId not set or not used for matching | Configure externalId mapping and ensure the app uses it for uniqueness | | Attributes not updating | Attribute mapping set to "create only" | Change mapping to "create and update" | | Deprovisioning does not work | App does not support SCIM deactivation | Use the app's native API as a fallback; contact vendor for SCIM support | | Group push fails | App does not support SCIM Group endpoint | Push group information as a user attribute instead | | Rate limiting during bulk operations | Too many SCIM requests in short window | Implement exponential backoff; batch operations where possible | | Schema mismatch | App expects custom attributes not in core SCIM schema | Use SCIM schema extensions (urn:ietf:params:scim:schemas:extension:enterprise:2.0:User) |
Security Considerations
- Protect SCIM tokens. The bearer token used for SCIM authentication grants full provisioning access. Store it in a secrets vault, rotate it periodically, and restrict its scope to provisioning operations only.
- TLS is mandatory. SCIM operations create and modify user accounts. All SCIM traffic must be encrypted with TLS 1.2+. Never use HTTP for SCIM endpoints.
- Audit all provisioning events. Every user creation, modification, and deletion must be logged with the initiator, timestamp, and changed attributes. These logs are essential for compliance and incident investigation.
- Rate-limit the SCIM endpoint. If you are building a SCIM server, implement rate limiting to prevent abuse. A compromised SCIM token could be used to mass-create accounts or mass-delete users.
- Validate SCIM requests. Do not blindly trust incoming SCIM requests. Validate the authentication token, check that the request conforms to the SCIM schema, and reject malformed requests.
- Timely deprovisioning is critical. The window between employee termination and account deactivation is a security gap. Minimize this window by triggering deprovisioning in real-time from the HR system, not on a batch schedule.
Conclusion
SCIM provisioning transforms user lifecycle management from a manual, error-prone process into an automated, auditable system. When an employee joins, they have access on day one. When they transfer, their access adjusts within hours. When they leave, their access is revoked within minutes.
The implementation requires careful attribute mapping, thorough testing, and ongoing monitoring through reconciliation. But the investment pays off immediately: fewer help-desk tickets, faster onboarding, cleaner offboarding, and a defensible compliance posture. Every application you connect to SCIM is one fewer manual provisioning task and one fewer orphan account risk.
FAQs
Q: Do all SaaS applications support SCIM? A: No, but adoption is growing rapidly. Major SaaS applications (Salesforce, Slack, Zoom, GitHub, Atlassian, ServiceNow) all support SCIM. For applications that do not, use the application's native API or manual provisioning as a fallback.
Q: What is the difference between SCIM 1.1 and SCIM 2.0? A: SCIM 2.0 (RFC 7643, 7644) is the current standard. It uses JSON exclusively (SCIM 1.1 supported XML), has a cleaner API design, and better filtering support. Always use SCIM 2.0 for new implementations.
Q: Can SCIM handle custom attributes?
A: Yes. SCIM supports schema extensions. Define custom attributes under a custom schema URI (e.g., urn:ietf:params:scim:schemas:extension:yourcompany:2.0:User) and map them in the IdP.
Q: How quickly should deprovisioning happen? A: For security-sensitive environments, target real-time deprovisioning (within minutes of HR termination). For standard environments, within 1 hour is acceptable. Batch deprovisioning (once daily) is a security risk.
Q: What about applications behind a firewall? A: If the SCIM endpoint is behind a firewall, deploy an on-premises provisioning agent (Okta On-Prem Agent, Azure AD Connect) that can reach the internal endpoint and relay SCIM operations from the cloud IdP.
Q: How do I handle re-hires?
A: When an employee is re-hired, the IdP should detect the existing (deactivated) account and reactivate it via SCIM PATCH (active: true) rather than creating a duplicate. Use externalId (employee ID) for matching.
Share this article