SCIM 2.0 Provisioning
GET /api/v1/scim/v2/Users — list (filter=userName|externalId eq "…")
POST /api/v1/scim/v2/Users — create (idempotent on externalId/userName)
GET /api/v1/scim/v2/Users/{id}
PUT /api/v1/scim/v2/Users/{id} — replace displayName/active
PATCH /api/v1/scim/v2/Users/{id} — RFC 7644 ops (active, displayName)
DELETE /api/v1/scim/v2/Users/{id} — soft-deactivate + revoke sessions
Rate limit: 60 requests per minute per IP (burst 30). Auth: Authorization: Bearer scim_… — a SCIM provisioning token, not a cookie or API key.
Token scope. A SCIM token may only act on accounts SCIM itself created, and only within its own provider:
- Every account provisioned over SCIM is marked SCIM-managed (
app_user.scim_managed) and records its owning provider (app_user.scim_provider_id, NULL when the creating token is deliberately unbound). An account that is not SCIM-managed is invisible to every SCIM token — this is what makes interactively created local admins unclaimable.
- A token bound to a provider sees only that provider's accounts — an unowned account included, or the binding would not be a boundary. A token deliberately created without a provider keeps the single-IdP convenience and may act on any SCIM-created account, including the ones it creates itself (origin and ownership are separate facts precisely so an unbound token can still deprovision its own accounts).
- Out-of-scope accounts answer
404, never 403, so a provisioning client cannot probe which usernames exist outside its scope. A create whose userName collides with an out-of-scope account gets 409.
- Re-binding an account that already carries a different
externalId is refused with 409: that is an identity change, not idempotency.
- Reactivation of a disabled account is audited as
scim_reactivate (it restores privileges). Errors use the SCIM error schema (application/scim+json). SCIM-created users get auth_source: oidc (SSO, no local password) and role operator; the IdP group mapping elevates them at first SSO login. Deactivation (PATCH active:false, PUT active:false, or DELETE) revokes the user's sessions so deprovisioning bites within the access-TTL, and audits scim_deprovision. A PUT that omits active (or displayName) leaves that attribute unchanged — a routine partial replace never silently deactivates the account or blanks its name. Deactivating the last active admin via any SCIM path is refused with 409 (mirrors the interactive user API), so an IdP lifecycle rule cannot lock everyone out.
Admin management of provisioning tokens (raw token shown once, revoke ≠ delete):
GET /api/v1/admin/scim-tokens
POST /api/v1/admin/scim-tokens — {description?, expires_at?} → {token, …}
DELETE /api/v1/admin/scim-tokens/{id} — revoke
Error responses:
400 Bad Request — Missing username or password
401 Unauthorized — Invalid credentials, inactive user, or SSO-provisioned account
403 Forbidden — Local login restricted to administrators (auth.local_login policy)
429 Too Many Requests — Rate limit exceeded or account locked