Skip to content

SCIM deprovisioning

Your IdP pushes active: false; lumnik archives the user; the very next request is refused, even with a still-valid token. The departure half of the identity loop.

Edition

The whole /scim/v2/* surface and its token endpoints ship in lumnik Pro, the image the deploy scripts pull. They are absent from lumnik Open. Editions

sequenceDiagram
  participant I as your IdP
  participant H as hub
  participant U as user
  I->>H: PATCH /scim/v2/Users/{id} · active:false · Bearer lmscim_…
  H->>H: archive the user (archived_at)
  H-->>I: 200
  U->>H: any request, token still valid
  H-->>U: 403 {"error":"user is deactivated"}

Why

  • Arrival is automatic: a user who logs in through your IdP is created at first login (BYO IdP). Departure is not: the IdP keeps issuing tokens until the account is disabled there, and the hub would keep honouring them.
  • SCIM closes the loop. Arrival and departure address the same row: JIT keys it (idp_issuer, idp_subject), SCIM reaches it through (issuer, externalId).
  • JIT is passive: it creates, never reactivates. Once archived, only SCIM (or an admin) brings a user back.
  • The integrator's departure is a different checklist: Offboarding.

The auth model

  • SCIM requests carry no user JWT. Each tenant mints opaque bearer tokens (lmscim_ + 32 random bytes, base64url: 256 bits); your IdP presents one on every call. The surface is not rate-limited; the entropy is the defence.
  • Minting needs lm_admin, for your own tenant only. The plaintext is returned once; only its SHA-256 hash is stored, never logged, never shown again.
  • A token is scoped to one tenant and one issuer: it cannot see local users, another IdP's users, or another tenant's. Such lookups answer a SCIM 404, not a leak.
  • Missing, unknown, revoked, or a user JWT instead of a SCIM token: 401, same body in every case by design. The application log tells them apart: SCIM auth refused: GET /scim/v2/… reason=missing or reason=unknown-or-revoked.
$ curl -s https://<hub>/scim/v2/Users -H "Authorization: Bearer <a user JWT>"
{"schemas":["urn:ietf:params:scim:api:messages:2.0:Error"],"detail":"invalid SCIM token","status":"401"}

Wiring your IdP

  1. Mint a token, as an lm_admin of the tenant. issuer must be the exact iss your IdP puts in its JWTs, byte for byte, or SCIM manages a parallel set of users logins never hit:

    curl -X POST "https://<hub>/api/tenants/<tid>/scim-tokens" \
      -H "Authorization: Bearer <your lm_admin JWT>" \
      -H "Content-Type: application/json" \
      -d '{"issuer": "https://idp.example.com/realms/acme", "label": "okta-prod"}'
    # → 201 {"id":1,"issuer":"…","label":"okta-prod","token":"lmscim_…"}   ← save the token NOW
    
  2. Configure the IdP's SCIM app: base URL https://<hub>/scim/v2, authentication Bearer token with the lmscim_… value, content type application/scim+json (PATCH also accepts application/json).

  3. Map attributes: externalId = the JWT subject claim (sub by default); it is the join key. userName, displayName/name and the primary emails entry are synced as profile.
  4. Verify: most IdPs test GET /scim/v2/ServiceProviderConfig, then a dedup lookup GET /scim/v2/Users?filter=userName eq "jdoe" before creating.

The end-to-end loop, provision → login 200 → active:false → login 403, is exercised by the Helm smoke test on every oidc-external rehearsal.

The /scim/v2/Users contract

Verb Path Does
POST /Users find-or-create by (issuer, externalId), then apply active + profile. 201. Authoritative: a re-POST revives a soft-deleted user and may re-activate an archived one. externalId required
GET /Users/{id} one user by lumnik id. 404 if absent, deleted, or keyed to another issuer
GET /Users?filter=… only externalId eq "x" and userName eq "x"; 0 or 1 result. Any other filter answers an empty list. Soft-deleted excluded, deactivated included
PUT /Users/{id} full replace: active, display name, primary email
PATCH /Users/{id} the deprovisioning operation: replace of top-level active only. A non-boolean active, or another op on it, is 400; a malformed value is never coerced into a deactivation. Other paths are silently ignored
DELETE /Users/{id} soft-delete, 204. The row stays, keeping the key occupied so a later login resolves to it and is refused

Deactivate vs erase

  • active: false archives. The row and its history stay; archived_at is stamped. Denial is immediate for both token shapes, user_id-claim or external identity. Only SCIM, being authoritative, pushes the user back with active: true.
  • DELETE soft-deletes. Reversible by a re-POST of the same (issuer, externalId).
  • Neither is a GDPR erasure. The row remains until an operator removes it: GDPR & personal data.
  • A SCIM write leaves no audit row. Deprovisioning, the event an access reviewer most wants, is not recorded. The audit trail

Not supported

  • Groups: no /scim/v2/Groups. Roles are managed in the IdP realm, not pushed over SCIM.
  • Bulk, sorting, etags, changePassword: declared unsupported in ServiceProviderConfig.
  • Pagination: the only list forms are the two eq filters above.
  • The full PATCH path grammar: emails[type eq "work"].value is ignored; use PUT.
  • /Schemas and /ResourceTypes: IdPs that require schema discovery need their mappings configured by hand.
  • Unfiltered GET /Users answers an empty list, not the tenant's population.

Token lifecycle

All three need lm_admin and your own tenant id in the path:

# Mint — plaintext in the response ONCE, never retrievable again
POST   /api/tenants/{tid}/scim-tokens          {"issuer": "<idp iss>", "label": "okta-prod"}

# List active tokens — id, issuer, label, createdAt; never the hash, never the plaintext
GET    /api/tenants/{tid}/scim-tokens

# Revoke — effective immediately, 204
DELETE /api/tenants/{tid}/scim-tokens/{id}

Rotation is mint-then-revoke: several tokens can be active at once (label tells them apart), so switching the IdP over needs no provisioning outage.

Troubleshooting

Symptom Likely cause
Plain 404 on every /scim/v2/* call, not the SCIM-shaped body above lumnik Open: the SCIM surface is not in that build
401 invalid SCIM token Missing Authorization: Bearer, a token never minted for this hub, a revoked one, or a user JWT. Identical response in every case
403 when minting, listing or revoking Your JWT lacks lm_admin, or the {tenantId} in the path is not your own tenant
404 on GET/PUT/PATCH/DELETE /Users/{id} No such user, soft-deleted, or the user's issuer differs from the token's: the most common wiring mistake. Compare the token's issuer with the exact iss of your IdP's JWTs (scheme, host, port, path)
Dedup search finds nothing, then POST returns an existing user Normal: filters exclude soft-deleted rows, POST is find-or-create
Deactivated user can still call the API A request already executing finishes; every request after it is denied. If later calls still succeed, confirm the PATCH landed (GET /Users/{id} → "active": false) and the issuer matches

See also