SCIM 2.0 Provisioning
SyAuth ships a SCIM 2.0 server so enterprise identity providers — Okta, Azure AD / Entra ID, OneLogin, JumpCloud, and any other SCIM-capable IdP — can push their directory of users and groups into a SyAuth workspace and keep it in sync automatically.
When an HR system onboards a new hire and adds them to an IdP group, SyAuth gets a SCIM User POST and a Group PATCH within seconds. When someone leaves, SyAuth gets a DELETE (which deactivates the user) or a PATCH flipping active=false. No manual CSV imports, no forgotten off-boarding.
End-to-end setup (5 minutes)
1. Mint a SCIM token
- Open SCIM Provisioning in the SyAuth dashboard (
/security/scim). - Label it (
"Okta production"), optionally set an expiry, click Create token. - Copy the plaintext token — it is only shown once.
2. Point your IdP at SyAuth
Configure your IdP's SCIM provisioning with:
| Field | Value |
|---|---|
| Base URL | https://api.syauth.com/scim/v2 |
| Authorization | Bearer <your token> |
| Token (Azure AD) | the plaintext token |
| Supported operations | Create, Update (PATCH + PUT), Deactivate |
That's it. The IdP will discover capabilities via /ServiceProviderConfig, /ResourceTypes, and /Schemas.
3. Provision
Assign a user or group to the SyAuth app in your IdP. Within seconds you'll see:
- A new
ExtsUserrow in the workspace withemail_verified=true(SCIM-provisioned users skip the email verification step — the IdP vouches for the address). - An
ExtsGrouprow if the IdP syncs groups, withmemberspopulated. scim_user_create/scim_group_createentries in the Audit Log withactor_type=api_tokenso you can trace everything back to the token that pushed it.
Supported endpoints
All endpoints live under /scim/v2/ and require a Bearer SCIM token. Responses use Content-Type: application/scim+json.
Users
| Method & path | Purpose |
|---|---|
GET /Users | List (with pagination + RFC 7644 filter) |
POST /Users | Create |
GET /Users/{id} | Read |
PUT /Users/{id} | Replace |
PATCH /Users/{id} | Partial update (add / replace / remove) |
DELETE /Users/{id} | Deactivate (soft) — is_active=false, all group memberships removed |
Groups
| Method & path | Purpose |
|---|---|
GET /Groups | List |
POST /Groups | Create |
GET /Groups/{id} | Read |
PUT /Groups/{id} | Replace (including membership) |
PATCH /Groups/{id} | Add / remove members, rename |
DELETE /Groups/{id} | Delete |
Discovery
| Path | Returns |
|---|---|
/ServiceProviderConfig | Feature support matrix — patch ✓, filter ✓, bulk ✗, sort ✗, etag ✗ |
/ResourceTypes | User and Group |
/Schemas | Core User + Group schema definitions |
Supported filter grammar
A minimal but safe subset of RFC 7644 §3.4.2.2:
filter := term ( OR term )*
term := factor ( AND factor )*
factor := "(" filter ")" | "not(" filter ")" | attr op value | attr "pr"
op := eq | ne | co | sw | ew | gt | ge | lt | le
Everything Okta and Azure AD send in practice works:
userName eq "[email protected]"
externalId eq "00u1a..."
active eq true
displayName sw "Engineering"
emails.value co "@acme.com" and active eq true
Unknown attributes return HTTP 400 invalidFilter rather than a silent empty set.
SCIM ↔ SyAuth attribute mapping
User
| SCIM attribute | SyAuth field | Notes |
|---|---|---|
userName | ExtsUser.email | Lowercased on write; unique per workspace via the existing (email, oauth_client) constraint |
name.givenName | first_name | Truncated to 30 chars |
name.familyName | last_name | Truncated to 30 chars |
emails[primary].value | ExtsUser.email | Overrides userName if primary flagged |
active | is_active | false deactivates without deleting |
externalId | ExtsUser.id (UUID) | We echo the internal UUID so IdPs can reconcile |
Unknown attributes (title, preferredLanguage, phoneNumbers, addresses, enterprise:2.0:User.department…) are accepted silently so IdPs don't retry forever, but they are not stored today.
Group
| SCIM attribute | SyAuth field | Notes |
|---|---|---|
displayName | ExtsGroup.name | Unique per workspace |
members[*].value | ExtsUserGroup rows | Only users that already exist in the workspace are added |
Example: create a user
curl -X POST "https://api.syauth.com/scim/v2/Users" \
-H "Authorization: Bearer scim_…" \
-H "Content-Type: application/scim+json" \
-d '{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"userName": "[email protected]",
"name": { "givenName": "Alice", "familyName": "Example" },
"emails": [ { "value": "[email protected]", "primary": true, "type": "work" } ],
"active": true
}'
Response 201 Created with Location: /scim/v2/Users/{id}:
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id": "…",
"externalId": "…",
"userName": "[email protected]",
"name": { "givenName": "Alice", "familyName": "Example", "formatted": "Alice Example" },
"emails": [ { "value": "[email protected]", "primary": true, "type": "work" } ],
"active": true,
"meta": { "resourceType": "User", "location": "https://api.syauth.com/scim/v2/Users/…" }
}
Example: PATCH to deactivate
curl -X PATCH "https://api.syauth.com/scim/v2/Users/{id}" \
-H "Authorization: Bearer scim_…" \
-H "Content-Type: application/scim+json" \
-d '{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [ { "op": "replace", "path": "active", "value": false } ]
}'
Example: filter with pagination
curl -G "https://api.syauth.com/scim/v2/Users" \
-H "Authorization: Bearer scim_…" \
--data-urlencode 'filter=active eq true and emails.value co "@acme.com"' \
--data-urlencode 'startIndex=1' \
--data-urlencode 'count=50'
IdP-specific notes
Okta
- Set SCIM connector base URL to
https://api.syauth.com/scim/v2. - Unique identifier field =
userName. - Supported provisioning actions: Create, Update, Deactivate — leave Sync Password unchecked (SyAuth does not accept passwords via SCIM).
- Group Push works out of the box.
Azure AD / Entra ID
- Set Tenant URL to
https://api.syauth.com/scim/v2. - Set Secret Token to your plaintext SCIM token.
- Aadoptscim compatibility flag is not required — SyAuth speaks stock SCIM 2.0.
- Default attribute mappings work; disable the
phoneNumbersmapping if you don't want a noisy audit log.
JumpCloud / OneLogin
- Use the generic SCIM 2.0 connector with the same base URL + bearer token.
Audit events
Every SCIM write is logged to the workspace Audit Log with actor_type=api_token:
| Event | Severity | When |
|---|---|---|
scim_token_create | warn | New SCIM token minted in the dashboard |
scim_token_revoke | warn | Token revoked |
scim_user_create | info | IdP provisioned a new user |
scim_user_update | info | PUT or PATCH changed a user |
scim_user_delete | warn | IdP deactivated a user |
scim_group_create | info | New group |
scim_group_update | info | Membership or rename |
scim_group_delete | warn | Group deleted |
Security notes
- Tokens are workspace-scoped. A compromised token can only act inside the workspace it was issued for — it cannot escalate to a different tenant.
- Stored as SHA-256. Plaintext is generated server-side with a
scim_prefix and 40 bytes ofsecrets.token_urlsafeentropy. Only the hash persists; the plaintext is shown once in the create response. - Revocation is instant.
DELETE /developer/scim/tokens/{id}/flipsis_active=falseand setsrevoked_at; the next SCIM call rejects with 401. - Expiry is optional but recommended for machine-to-machine integrations; rotate yearly at minimum.
- Audit trail is immutable. Every write goes into the append-only audit log with the token id recorded as the actor, so you can always answer "who pushed this change?"
- No password provisioning. SyAuth ignores
passwordin inbound SCIM payloads. Authentication happens through your IdP flow or SyAuth's native flows.
Limits and what's explicitly not supported
These are by design and documented to set IdP expectations:
- Bulk operations — not supported.
bulk.supported=falseinServiceProviderConfig. Most IdPs fall back to sequential writes automatically. - Sorting — not supported. Clients should paginate with
startIndex+count. - ETag / versioning — not supported.
- Password sync — deliberately rejected. See above.
- Enterprise schema — parsed but not persisted. Planned.
- Custom schemas — not supported.
Max page size is 200 resources per response.