Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Implement automated user provisioning and deprovisioning using SCIM 2.0 protocol with Okta as the identity provider.
.claude/skills/implementing-scim-provisioning-with-okta/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | — | — |
| case-22 | ✗→✗ | = Same ✗ | — | — |
| case-17 | ✗→✗ | = Same ✗ | — | — |
| case-18 | ✗→✗ | = Same ✗ | — | — |
| case-05 | ✗→✗ | = Same ✗ | — | — |
SCIM (System for Cross-domain Identity Management) is an open standard protocol (RFC 7644) that automates the exchange of user identity information between identity providers like Okta and service providers. This skill covers building a SCIM 2.0-compliant API endpoint and integrating it with Okta for automated user lifecycle management including provisioning, deprovisioning, profile updates, and group management.
SCIM defines a standard schema for representing users and groups via JSON, with a RESTful API for CRUD operations:
| Operation | HTTP Method | Endpoint | Description | |-----------|-------------|----------|-------------| | Create User | POST | /scim/v2/Users | Provisions a new user account | | Read User | GET | /scim/v2/Users/{id} | Retrieves user details | | Update User | PUT/PATCH | /scim/v2/Users/{id} | Modifies user attributes | | Delete User | DELETE | /scim/v2/Users/{id} | Removes user account | | List Users | GET | /scim/v2/Users | Lists users with filtering | | Create Group | POST | /scim/v2/Groups | Creates a group | | Manage Group | PATCH | /scim/v2/Groups/{id} | Add/remove group members |
Okta (IdP) ──SCIM 2.0 over HTTPS──> SCIM Server ──> Application Database
│ │
├── User Assignment ├── Create/Update User
├── User Unassignment ├── Deactivate User
├── Profile Push ├── Sync Attributes
└── Group Push └── Manage Groups/scim/v2/ServiceProviderConfig): Advertises SCIM capabilities/scim/v2/ResourceTypes): Describes supported resource types/scim/v2/Schemas): Publishes the SCIM schema definitions/scim/v2/Users): User lifecycle operations/scim/v2/Groups): Group management operationsCreate a Flask-based SCIM server that implements the core endpoints. The server must handle:
eq filter on userName (required by Okta)startIndex, itemsPerPage, and totalResultspythonfrom flask import Flask, request, jsonify import uuid from datetime import datetime app = Flask(__name__) # Bearer token for Okta authentication SCIM_BEARER_TOKEN = "your-secure-token-here" def require_auth(f): def wrapper(*args, **kwargs): auth = request.headers.get("Authorization", "") if not auth.startswith("Bearer ") or auth[7:] != SCIM_BEARER_TOKEN: return jsonify({"detail": "Unauthorized"}), 401 return f(*args, **kwargs) wrapper.__name__ = f.__name__ return wrapper @app.route("/scim/v2/Users", methods=["POST"]) @require_auth def create_user(): data = request.json user_id = str(uuid.uuid4()) user = { "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], "id": user_id, "userName": data.get("userName"), "name": data.get("name", {}), "emails": data.get("emails", []), "active": True, "meta": { "resourceType": "User", "created": datetime.utcnow().isoformat() + "Z", "lastModified": datetime.utcnow().isoformat() + "Z", "location": f"/scim/v2/Users/{user_id}" } } # Persist user to database return jsonify(user), 201 @app.route("/scim/v2/Users", methods=["GET"]) @require_auth def list_users(): filter_param = request.args.get("filter", "") start_index = int(request.args.get("startIndex", 1)) count = int(request.args.get("count", 100)) # Parse filter: userName eq "john@example.com" # Query database with filter return jsonify({ "schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], "totalResults": 0, "startIndex": start_index, "itemsPerPage": count, "Resources": [] })
https://your-app.com/scim/v2userNameMap Okta user profile attributes to your SCIM schema:
| Okta Attribute | SCIM Attribute | Direction | |---------------|----------------|-----------| | login | userName | Okta -> App | | firstName | name.givenName | Okta -> App | | lastName | name.familyName | Okta -> App | | email | emailstype eq "work"].value | Okta -> App | | department | urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department | Okta -> App |
SCIM specifies standard error response format:
json{ "schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"], "detail": "User already exists", "status": "409", "scimType": "uniqueness" }
Common error codes: 400 (Bad Request), 401 (Unauthorized), 404 (Not Found), 409 (Conflict), 500 (Internal Server Error).
Okta provides an automated SCIM test suite (via Runscope/BlazeMeter) that validates your SCIM implementation against all required operations:
userName eq "..." filter works correctlystartIndex, count) handled properlyactive: false (not hard delete)add, replace, remove ops| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +5 percentage points is the difference between those two pass rates over the 22 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.