Resolve the tenant from the authenticated SCIM connector, map users by the provider’s immutable ID, and commit a disable as one transaction that marks the user inactive, bumps an authorization epoch, removes local grants, revokes sessions and tokens, and cancels pending jobs. Check the epoch on every request and job, accept CAEP session-revoked events from your identity provider, revoke tokens you hold elsewhere with RFC 7009, and measure how long each enforcement point takes.
- A successful SCIM
PATCHproves the resource changed, not that sessions, tokens and queued jobs stopped working. - Choose the tenant from the connector credential, then map users by the immutable SCIM
id, never by email. - Commit
active=false, a new epoch, removed grants, revoked credentials and cancelled jobs in one transaction. - Workers check current authority when a job runs, not the authority captured when it was queued.
- Shared Signals and CAEP 1.0 (final, August 2025) carry session-revoked events between services as signed SETs.
- Re-enabling a user issues new credentials; it never revives old sessions, tokens or jobs.
| Setting | Value | Note | Source |
|---|---|---|---|
| Media type | application/scim+json | — | RFC 7644 section 3.1 |
| Partial updates | PATCH with urn:ietf:params:scim:api:messages:2.0:PatchOp, applied atomically | — | RFC 7644 section 3.5.2 |
| Concurrency | ETag and If-Match where ServiceProviderConfig reports etag support | Treat versions as opaque; never compare them numerically. | RFC 7644 section 3.14 |
| Revocation events | CAEP session-revoked and credential-change over SSF push delivery | Push uses RFC 8935; poll delivery (RFC 8936) also exists. | OpenID SSF 1.0 and CAEP 1.0 |
| Event token checks | Signature, typ of secevent+jwt, issuer, audience and a unique jti | SSF SETs carry the subject in a top-level sub_id claim and one event each. | OpenID SSF 1.0 section 4 |
- Keep a session, a personal token and a queued job alive, disable the user through SCIM, and confirm all three fail on their next use.
- Restart the application after the disable and repeat the checks: none of the old credentials may come back.
- Disable a user in one tenant while another tenant has the same email and
externalId, and confirm only the first changes. - Send a signed CAEP session-revoked event twice and confirm sessions are revoked once and the second delivery is acknowledged.
- Measure the time from the directory disable to denial at each enforcement point and alert when it exceeds your target.
- Applies to
- jose (CAEP receiver example) 6.x (tested with 6.2.12)
SCIM provisioning consumers and downstream local grants, sessions and queued actions.
- Part of
- Workflows: Fix a finding, Review a design
Tested fixes: the vulnerable code, the fix, a plausible fix that still fails, and the regression tests that tell them apart.
Learning goals and prerequisites
What you will learn
- Keep provisioning identifiers scoped to the authenticated tenant.
- Translate disable and membership changes into enforceable authority changes.
- Test retries, stale updates, re-enable and incomplete remote revocation.
Before you start
SaaS developers and enterprise identity engineers integrating provisioning with application authorization.
- Read a JSON HTTP request and a SQL transaction.
- Understand sessions, bearer credentials and group-derived permissions.
Refresh first: Authorization models: RBAC, ABAC, ReBAC and policy as code you can test
Go to the guided practiceSSO answers how a login assertion is accepted; provisioning manages the resource lifecycle. An employee leaving a directory must lose usable application authority even if they never attempt another login. A connector can correctly update a User resource while a cached session, API credential or queued export keeps working. Test those paths directly.
Draw the authority inventory before implementing the connector: browser sessions, refresh grants, personal tokens, resource grants, role caches, delegated jobs and service-owned credentials. Label which are user-bound and which are independently owned service identities. Do not destroy a shared service identity merely because its creator leaves; require an accountable ownership transfer instead.
SCIM id is assigned by the service provider; externalId is supplied by the provisioning client and scoped to its provisioning domain. The User active attribute describes administrative status; its concrete effect is defined by the provider. Users.groups is read-only; membership changes belong in the relevant Group resource’s members attribute.
For this application, a connector credential selects Acme before resource lookup. Map that connector and the stable external user identity to the provider’s immutable user ID. Email/userName changes must not transfer another person’s resources or select a different tenant. Two organizations may legitimately use the same email or externalId. Store and test their mappings independently.
Scope connector permissions to the enrolled tenant and required lifecycle operations. Protect and rotate its secret, bound directory queries and returned attributes, and record its actor identity for changes. Reject requests attempting to override the tenant in request content. Connector authentication should not reuse an ordinary end-user login session.
SCIM PATCH uses application/scim+json and the PatchOp schema. Apply its Operations in order and atomically for the target resource; a failed operation must not leave a partial resource update. Discover supported features through ServiceProviderConfig. When versioning is supported, use the resource ETag and If-Match to detect concurrent modification; treat versions as opaque validators, not sortable counters.
A User active=false replacement and a Group member removal are separate resource changes. SCIM does not make that pair one cross-resource transaction. Nor does a protocol success automatically revoke a product’s existing sessions or remote jobs. Document which application effects are synchronous, which are pending, and how their completion is observed.
The minimal request below illustrates a resource update only. Authentication credentials and real resource IDs come from the configured connector; no runnable server is supplied by this snippet.
PATCH /scim/v2/Users/u-acme-42 HTTP/1.1
Host: app.example
Content-Type: application/scim+json
If-Match: W/"user-version-17"
{"schemas":["urn:ietf:params:scim:api:messages:2.0:PatchOp"],"Operations":[{"op":"replace","path":"active","value":false}]}In the accompanying model, one local transaction changes active state, advances an authorization epoch, replaces local grants, revokes credentials and cancels pending user jobs. The event receipt commits with that transition. An interruption before commit leaves the previous complete state, so a retry can safely try again. A half-disabled account is not an acceptable success state.
At request time, compare credential epoch with the current user epoch and check the current grant needed for the action. At job execution, check again rather than trusting authority captured at enqueue time. Cancellation is useful cleanup, but the worker’s effect-time check is an independent defense if cancellation delivery is missed.
Re-enabling a user is a new explicit transition. It must not revive an old bearer credential, refresh grant or pending job. Restore only approved present-day grants, issue new credentials through the normal flow, and retain the prior revocation history. An effect completed before disable is historical fact; disable does not automatically reverse it.
The epoch only helps if every enforcement point reads it. Session middleware, API token validation, GraphQL resolvers, exports and background workers must all compare the credential’s epoch with the user’s current one, or call a shared function that does. The session guide at /domains/identity-crypto/authentication/session-lifecycle/ shows the same version check for browser sessions.
A local disable does not reach sessions held by other applications. The OpenID Shared Signals Framework and the Continuous Access Evaluation Profile (CAEP), both final specifications since August 2025, let an identity provider push signed Security Event Tokens (SETs) such as session-revoked and credential-change to every service that subscribed to a stream. Your application can be a receiver, ending its own sessions when the identity provider says so, and a transmitter, telling downstream services when it disables a user.
For OAuth tokens your application holds at other services, such as integration refresh tokens stored for the departing user, call each authorization server’s revocation endpoint (RFC 7009). If your application issues tokens, give resource servers a way to learn about revocation, such as short access-token lifetimes plus token introspection (RFC 7662), because a self-contained JWT stays valid until it expires; the JWT guide at /domains/identity-crypto/authentication/jwt-attacks/ covers access-token lifetimes.
The receiver below verifies each SET against the transmitter’s keys, refuses anything that is not explicitly typed as a security event, acts once per jti because deliveries are retried, and revokes sessions for the subject named in sub_id. It was tested with jose 6.2.12 on Node.js 26: a session-revoked event revokes once even when delivered twice, and a forged signature, a plain JWT type, another issuer and another audience are rejected before any revocation.
import {createRemoteJWKSet, jwtVerify} from 'jose';
const SESSION_REVOKED = 'https://schemas.openid.net/secevent/caep/event-type/session-revoked';
const CREDENTIAL_CHANGE = 'https://schemas.openid.net/secevent/caep/event-type/credential-change';
// The transmitter's issuer and JWKS come from its SSF configuration, set up when the stream was created.
const transmitterKeys = createRemoteJWKSet(new URL('https://idp.example/ssf/jwks'));
// POST /ssf/events (RFC 8935 push delivery, Content-Type: application/secevent+jwt).
// Answer 202 when this resolves; 400 when it throws.
export async function receiveSet(token, {seenJti, revokeSessions}) {
const {payload} = await jwtVerify(token, transmitterKeys, {
issuer: 'https://idp.example/',
audience: 'https://app.example/ssf',
typ: 'secevent+jwt', // SSF requires explicit typing, so an ID or access token is refused
algorithms: ['RS256', 'ES256'],
});
if (!(await seenJti.add(payload.jti))) return 'duplicate'; // deliveries are retried: act once per jti
const [[type, event]] = Object.entries(payload.events); // SSF: one event per SET
if (type === SESSION_REVOKED || type === CREDENTIAL_CHANGE) {
// sub_id names the subject, for example {format: 'iss_sub', iss, sub}; map it within the tenant.
await revokeSessions(payload.sub_id, event.event_timestamp);
}
return 'accepted';
}For repeated delivery, distinguish the same change from a conflicting reuse of an operation identifier. The model uses adapter-assigned event IDs plus monotonically ordered source versions; those are declared model assumptions, not SCIM-defined event fields. A real connector needs a supported source snapshot/version or reconciliation strategy before it can claim stale updates cannot reactivate users. RFC 9967 (May 2026) adds an optional SCIM profile for Security Event Tokens that a provider can use to signal changes asynchronously; check that your directory supports it before relying on it.
After an ambiguous network result, read current resource state and reconcile deliberately. Do not blindly resend a stale full-resource replacement that restores removed grants. Measure the time from authoritative disable to denial at each enforcement point, alert on stalled propagation, and retain enough identifiers to prove which tenant and identity were affected.
Keep the old session, token and queued job alive in the test environment before sending a lifecycle update. Try each after the change and after an application restart. Also test an unrelated tenant with a colliding email/identifier; broad accidental revocation is a defect too.
| Action | Required observation | Important limit |
|---|---|---|
| Disable Acme user | Old session/token denied; pending user job cannot execute | New login rejection alone is insufficient |
| Remove export membership | Read still works with a new authorized credential; export fails | Other policy grants may independently permit export |
| Retry the same accepted transition | No second epoch change or repeated side effect | A changed body under the same event ID conflicts |
| Re-enable later | New credentials and explicit grants only | Old credentials and jobs stay invalid |
| External revocation pending | Pending/failed target and elapsed time are visible | Local commit is not proof of remote completion |
The identity-lifecycle downloadable project uses Python 3.11 or later and SQLite. Run python3 -m unittest -v test_lifecycle.py. Repair exercise.py, then compare reference.py after recording the failing boundaries. All 18 reference tests execute real local transactions, credentials and grants, and job-effect records.
The model uses normalized in-process provisioning snapshots and opaque credential IDs. It does not implement SCIM HTTP parsing, actual signed token verification or an external revocation service. Those limitations identify the next integration tests; they do not turn the local transaction tests into protocol compliance evidence.
Guided practice
Work through an offboarding race and decide whether a successful resource update has actually ended the user’s authority.
Supplied scenario material
Application contract: disable invalidates all user credentials, removes local grants and cancels pending user jobs. Every request and worker checks current active/epoch/grant state.
Tenant Acme: connector C-A; SCIM id u-acme-42; externalId 42; email person@example.test.
Tenant Beta: connector C-B; SCIM id u-beta-42; externalId 42; same email; unaffected account.
Acme initial state: active=true, epoch=7, grants=[read,export].
Session S7 and personal token T7 both carry epoch=7; export job J7 is pending at epoch=7.
09:00:00 directory disables Acme user.
09:00:02 C-A sends the active=false PATCH shown above; application commits its full local disable transition, epoch=8, and returns success.
09:00:03 client retries after losing the response; it supplies an identical normalized adapter event E18.
09:00:04 a delayed adapter snapshot E17 arrives claiming active=true.
09:00:05 S7 calls /invoices; T7 calls /exports; worker attempts J7.
09:05:00 an approved new snapshot E19 enables user with grants=[read], epoch=9.
Assumption for this PAPER MODEL only: E17/E18/E19 have trusted ordered source versions 17/18/19; event IDs and ordering are not SCIM wire fields.
Remote analytics service revocation remains unacknowledged at 09:00:05.1. Inventory the local disable result
Write every state change required at 09:00:02, and name the tenant-bound identity key.
Expected result and reasoning
Resolve C-A to Acme and immutable user
u-acme-42using its enrolled mapping. Commitactive=false, epoch 8, empty local grants, revoked S7/T7 and cancelled J7 together with the adapter receipt. Do not look up a user globally by email orexternalId; Beta’su-beta-42remains untouched.2. Handle duplicate and stale inputs
Evaluate E18 again and then E17 using only the explicit model assumption.
Expected result and reasoning
Identical E18 is a duplicate and must not advance the epoch again. A changed body under E18 would conflict. E17 is stale under the supplied trusted ordering and cannot restore active status or grants. Do not infer equivalent ordering by numerically parsing an arbitrary SCIM
ETag.3. Evaluate old credentials and queued work
Decide the outcomes of all three 09:00:05 attempts, including the case where a cancellation message was lost.
Expected result and reasoning
All deny. S7 and T7 are revoked and their epoch 7 differs from current epoch 8; the user is inactive. J7 cannot execute even if its pending flag survives because the worker checks current active state, epoch and export grant at effect time.
4. Distinguish local completion from remote completion
State what evidence the 09:00:02 success proves and whether offboarding is fully complete by 09:00:05.
Expected result and reasoning
It proves the agreed local resource/application transition committed. It does not prove analytics revocation. Offboarding remains incomplete for that target; report the unacknowledged target and elapsed time from 09:00:00, retry/reconcile under the service contract and escalate if its revocation objective is exceeded.
5. Re-enable without reviving old authority
At 09:05:00 determine whether T7 and J7 return to service, and what a new session may do.
Expected result and reasoning
T7 and J7 stay invalid. A newly issued session at epoch 9 may read because E19 explicitly grants read, but cannot export without another current grant. Historical completed effects remain auditable; re-enable does not repeat them.
The ordered event timeline is a declared application model, not a standardized SCIM event stream. Run the project for local transaction evidence, then test a real authenticated SCIM connector and each remote enforcement point.
Save a draft on this device to resume later. Automatic saving begins after you choose Save. Drafts are never sent by this application; use synthetic data.
No draft saved on this device yet.
Run a local project
Download and unzip a starter with failing tests and a step-by-step README, edit the named exercise file, and run the tests until they pass. The reference solution is a separate download, so you can attempt the repair before you see it.
Prove deprovisioning reaches sessions and queued work
Repair a durable SQLite identity lifecycle model. Disable current sessions/tokens, remove group-derived authority, cancel pending jobs, and handle duplicate, stale and interrupted changes without crossing tenant boundaries.
In the extracted folder, run:
python3 -m unittest -v test_lifecycle.py18 regression tests · The starter intentionally fails some cases.
Opens in a dev container (VS Code Dev Containers or GitHub Codespaces) with a pinned image.
Part of the learning path Identity through the whole lifecycle
Actual local transactions and authorization state. Normalized provisioning events, opaque credentials and local job effects; not a SCIM HTTP/PATCH server or external revocation service.
Sources & further reading
The standards, specifications and documentation behind the guidance above. Each is the evidence for a claim on this page; consult it for detail beyond the defaults and versions listed here.
- RFC 7643 — SCIM Core Schema, identifiers, active and groups. September 2015; updated by RFC 9865 and RFC 9967.
- RFC 7644 — SCIM PATCH, versioning and multi-tenancy. September 2015; updated by RFC 9865 and RFC 9967.
- RFC 7642 — provisioning and deprovisioning concepts. September 2015.
- SQLite — transaction behavior used by the runnable model
- OpenID Shared Signals Framework 1.0. Final, 29 August 2025.
- OpenID Continuous Access Evaluation Profile (CAEP) 1.0. Final, 29 August 2025.
- RFC 7009: OAuth 2.0 Token Revocation. August 2013.
- RFC 8935: Push-based Security Event Token delivery. November 2020.
- RFC 9967: SCIM profile for Security Event Tokens. May 2026.
Reference information
SCIM 2.0 RFC 7643/7644; the SQLite project models normalized events and is not a SCIM HTTP server.
- SDLC phase
- Development
- Role
- Developers
- ASVS 5.0.0 requirements
- v5.0.0-7.4.2 · v5.0.0-8.3.2 · v5.0.0-8.4.1