SCIM, deprovisioning, and the joiner-mover-leaver scope
If you run access from an identity provider, you will ask the lifecycle question: when someone joins, moves team, or leaves, what does downpipes do, and what is still yours to do in your directory. This page answers that, including the parts downpipes does not do, so you can plan the gap rather than discover it.
The short version: a leaver’s role can be removed by your provider’s SCIM connector through a minimal SCIM facade, or by an in-product ceremony. The SCIM facade also revokes the ways a non-Owner leaver signs in to the engine, and the in-product ceremony does not. The joiner and mover cases are covered by mapping your directory groups to roles. The SCIM surface downpipes exposes is deliberately narrow: it deprovisions a leaver and does nothing else. It provisions no one and reads no user records. The rest of this page sets out where that line sits and what stands in for the parts beyond it.
What the SCIM facade exposes, in one line
downpipes exposes a minimal SCIM 2.0 facade your identity provider can target for one job: offboarding a leaver, by DELETE /scim/v2/Users/{id} or PATCH /scim/v2/Users/{id} with active:false. Joiner and mover are handled by group-to-role mapping that follows your directory on the next sign-in. The facade also revokes the passkey, recovery codes and invites a non-Owner leaver holds in the engine. Removing the person in your directory and ending their Cloudflare Access session stay yours to do. SCIM provisioning, reads, and reactivation are out of scope by design.
Joiner and mover: group-to-role mapping follows your directory
You do not provision a joiner into downpipes one person at a time, and you do not re-grant a mover. Instead, map your identity-provider groups to downpipes roles once, and a person’s role follows the groups their verified sign-in carries.
A joiner who lands in a mapped group gets the mapped role on their first sign-in, with no second grant to maintain in downpipes. A mover whose group membership changes in your directory resolves to the new role on their next sign-in. The engine reads the groups of the latest verified sign-in on each request, not a copy in the session cookie. The mapping is setGroupRole in the scheduler Durable Object, gated on the access.policy capability, and a group can map to any role except owner. The mechanics, the per-provider claim extraction, and the limits are on group-to-role mapping.
This is the recommended way to run joiner and mover at any size, because your directory stays the single place you manage who is in which team, and downpipes follows it. The one role it cannot drive this way is owner, which stays an explicit, named, per-person grant.
Leaver over SCIM: the deprovision facade
If your identity provider runs a SCIM connector, point its deprovision at the downpipes SCIM facade and a leaver is offboarded automatically. The facade lives under /scim/v2 and is dispatched from the Worker entry before the console admin branch, so it has its own authentication and never touches the console auth model (handleScim in engine/src/admin/scim.ts, wired in engine/src/index.ts).
The facade is deprovision-only by design. It first revokes the person’s sign-in factors. It then drives the same audited offboarding that the member-removal ceremony drives: it removes the person’s in-app role and bumps the engine’s per-email session epoch and per-subject not-before instant, so any session they hold fails closed on the next request. It provisions no one, reads no user records, and supports no other write.
A SCIM deprovision revokes sign-in factors, except for an Owner
The facade calls two engine routes for each leaver (engine/src/admin/scim.ts). It first revokes the person’s sign-in factors: their passkey credentials, recovery codes and pending invites. It then calls /roles/delete, the same route the console ceremony calls.
For an Owner, the engine declines the revoke and leaves every sign-in factor in place. A connector can misfire, and recovery codes cannot be re-created. If the revoke fails for a non-Owner, the facade still removes the role and the factors stay. In these cases, use Revoke sign-in on the Roles and access tab, under Sign-in factors. The admin API route is POST /admin/signin-factors/revoke. See offboard a member for the three stores, the revoke and its guards.
The two requests that offboard a leaver
{id} in each path is the member’s email, URL-encoded. Configure your connector so the SCIM userName or externalId is the person’s email, the same email they sign in with.
| Request | Effect |
|---|---|
DELETE /scim/v2/Users/{id} | Offboards the member. Returns 204 No Content. A member with no role row also returns 204. The facade still revokes their sign-in factors and ends their sessions. |
PATCH /scim/v2/Users/{id} with a SCIM PatchOp setting active to false | The same offboarding as DELETE. Returns 204 No Content. Both the path-scoped form ({op:"replace",path:"active",value:false}) and the value-object form ({op:"replace",value:{active:false}}) are accepted, because different connectors emit each. |
One asymmetry is worth knowing before you rely on either path. A SCIM offboard is not held by dual control, even on an account that has Require Config Approval switched on: the handler bypasses that gate for this caller deliberately, because queuing a connector’s deprovision produces a change nobody can ever approve, and it returns 204 either way. Holding it would keep the departed member’s access open in the meantime, which is the exposure prompt deprovision exists to close. The bypass can only ever narrow access: this facade has no provisioning path, so there is nothing here that could grant. Every other caller falls through to the fully gated path, including the console’s own member-removal ceremony. The one refusal that survives in ordinary operation is the last-Owner guard, which comes back as a SCIM error carrying no member address.
The facade answers the two reads a connector makes as follows. GET /scim/v2/ServiceProviderConfig advertises what the facade supports: PATCH is supported, and bulk, filter, sort, changePassword, and ETag are not. GET /scim/v2/Users returns an empty ListResponse, because this facade does not enumerate your members and discloses no member data. Any other route or method returns a SCIM error.
What the facade does not do, and why
Everything beyond deprovision is out of scope, and the facade says so.
| SCIM operation | Supported? | Why |
|---|---|---|
DELETE /Users/{id} (deprovision) | Yes | Revokes the sign-in factors of a non-Owner, then drives the existing audited offboarding: role removal plus session termination. |
PATCH /Users/{id} with active:false (deactivate) | Yes | The same offboarding as DELETE. |
PATCH /Users/{id} with active:true (reactivate) | No | Rejected with a SCIM mutability error rather than silently accepted, so the provider is never told a reactivation succeeded that did not happen. Provisioning happens through a console role grant plus the person’s next sign-in, not here. |
POST or PUT /Users (create or replace) | No | This facade provisions nothing. A joiner gets a role through group-to-role mapping or an explicit grant. |
GET /Users/{id} (read a member) | No | The facade reads no user records and discloses no member data. |
| Bulk, filter, sort, changePassword, ETag | No | Advertised as unsupported in ServiceProviderConfig so a conformant connector plans around them. |
Authentication and fail-closed behaviour
The facade is gated by a dedicated bearer, SCIM_BEARER_TOKEN, separate from the owner break-glass ADMIN_TOKEN on purpose: your SCIM connector holds only a leaver-offboarding credential, never the owner break-glass.
When SCIM_BEARER_TOKEN is unset, the whole surface is off and every /scim/v2 request returns 503, so a deployment that has not opted in has no SCIM attack surface at all. When it is set, the presented Authorization: Bearer value is compared against it in constant time over the SHA-384 digest of each side, so neither the value nor its length leaks, and a per-IP rate limit, the same fail-closed brute-force gate the unauthenticated admin ceremony uses, fronts the check so a stolen-endpoint guess is throttled and a limiter outage denies rather than opens.
A removed in-app role is not a closed door, at the edge or inside the engine
A SCIM deprovision, like the member-removal ceremony, ends the person’s downpipes privilege and fails their engine sessions closed on the next request. Outward, neither path ends their Cloudflare Access session or removes them from your identity provider, because the engine holds no credential for either. Inward, the member-removal ceremony and a SCIM deprovision of an Owner leave their passkey credential, their recovery codes and any unredeemed invite in place. Each of these is a fresh way back in that the edge never sees, because these are the engine’s own stores and sit behind Access rather than in front of it. A leaver is closed when the directory removal, the Access session end and the sign-in-factor revoke have all been done. See offboard a member for the boundary in full.
Leaver in the console: the member-removal ceremony
If you do not wire a SCIM connector, or you want to offboard someone by hand, the member-removal ceremony on the Access and security screen does the same role removal: it removes the person’s in-app role immediately and audibly, and bumps the session epoch and not-before instant so their sessions fail closed on the next request. The facade and the ceremony reach the same audited offboarding by different front doors. The full flow, including the last-Owner guard and that removing an absent member is a safe no-op, is on offboard a member.
If a group-to-role mapping was the only thing granting the person a role, removing them from that group in your directory also drops their downpipes role on their next sign-in, because the role is re-resolved from the verified groups each time. That is the mapping working as a deprovision path for a group-granted role; an explicit per-email grant is removed by the SCIM facade or the member-removal ceremony instead.
Lifecycle coverage, in one table
| Lifecycle event | Covered? | How |
|---|---|---|
| Joiner | Yes, via the directory | Land them in a mapped group; the mapped role applies on their first sign-in (setGroupRole). |
| Mover | Yes, via the directory | Change their group in your directory; the new role resolves on their next sign-in (the engine reads the groups of the latest sign-in on each request). |
| Leaver, over SCIM | Yes, deprovision-only | DELETE /scim/v2/Users/{id} or PATCH with active:false drives the audited offboarding. |
| Leaver, in the console | Yes | The member-removal ceremony removes the in-app role and fails their sessions closed on the next request. |
| Leaver, in your directory and at the edge | No, by design | The engine holds no credential for your identity provider or Cloudflare Access, so removing the person there is yours to do. |
| Leaver, their ways of signing in to the engine | Yes, over SCIM except for an Owner | POST /admin/signin-factors/revoke clears the passkey credential, the recovery codes and any live invite for one email as one audited act. A SCIM deprovision runs it first for a non-Owner. After the console ceremony, or for an Owner, somebody runs it deliberately, on the Roles and access tab under Sign-in factors. |
| SCIM create, update, reactivate, or read | No, by design | The facade is deprovision-only. It provisions no one and reads no user records. |
The reason the SCIM surface is deprovision-only is the same no-custody constraint that runs through the product: a downpipes deployment holds your account’s own data and no credential that can write to your identity-provider directory. Group-to-role mapping reaches the joiner and mover outcome by reading the groups your provider already signs into the token, which needs no standing credential, and the SCIM facade reaches the leaver outcome with only a leaver-offboarding bearer, so neither needs the broad standing access that full SCIM provisioning would.
The scim-users and scim-groups names you may see in config backup are a different thing
The Cloudflare configuration backup includes surfaces named scim-users and scim-groups. Those read your account’s SCIM provisioning records from Cloudflare for backup, defined in cf-config-registry-expanded.ts. They are part of capturing your Cloudflare configuration, not the SCIM facade downpipes runs for your identity provider to deprovision into. Backing up those records and exposing the inbound deprovision endpoint are unrelated capabilities.
Wiring an Okta or Microsoft Entra ID connector
Okta and Microsoft Entra ID both push lifecycle events to an application over SCIM by pointing a connector at a base URL with a bearer token and enabling event classes such as Create, Update, and Deactivate or Deprovision.
Point the connector’s SCIM base URL at /scim/v2 on your deployment, issue it the dedicated SCIM_BEARER_TOKEN, and enable only the deactivate or deprovision event class. A deprovision event also revokes the passkey, recovery codes and any invite for a non-Owner address. Plan the revoke for an Owner as a manual step. The create, update, and reactivate classes have no effect here by design: a reactivate is refused with a SCIM mutability error, and a create or a read of one user returns a SCIM error, so enabling them only produces connector errors rather than silent half-success. Configure the joiner and mover side with group-to-role mapping instead, so a person’s role follows the groups your provider already sends in the sign-in token.
Related
- Group-to-role mapping covers the joiner and mover path, the per-provider group extraction, and the limits including that no group can confer owner.
- Offboard a member is the leaver ceremony in full, including the hard boundary that downpipes cannot deprovision your identity provider or end a Cloudflare Access session.
- Session management explains the session epoch and not-before instants that make a removal take effect on the next request.
- Supported providers lists the providers downpipes ships presets for and what each emits in the token.
Last updated .