Outbound SCIM Provisioning¶
WeftID can push user and group changes to downstream applications using SCIM 2.0. When a user is added to a group, has their attributes updated, or loses access, WeftID notifies the downstream application so it can create, update, or deactivate the corresponding account without manual intervention.
This guide covers what outbound SCIM does, how to enable it on a registered service provider, vendor-specific setup walkthroughs, and troubleshooting tips.
What outbound SCIM does¶
WeftID is a SCIM 2.0 client: it pushes resource changes to the SP's SCIM endpoint. WeftID is not a SCIM server -- inbound provisioning is a separate feature.
Outbound SCIM closes the gap that pure SAML cannot. A user removed from WeftID still retains access to downstream SaaS until SCIM tells those apps to deprovision. With SCIM enabled on an SP, every relevant user and group change in WeftID is replicated to the SP within seconds.
What triggers a push¶
The following changes enqueue work for the per-tenant SCIM worker:
- User creation, activation, deactivation -- pushed as a SCIM User create or update.
- User profile attribute change -- pushed as a SCIM User PUT.
- Group creation, rename, delete -- pushed as a SCIM Group create, update, or delete.
- Group membership change -- enqueues both the group itself and every affected user, individually (eager fan-out at trigger time). This keeps queue depth a meaningful "work remaining" metric and lets per-user retries fail in isolation.
- Group-to-SP grant change -- granting or revoking a group's access re-evaluates scope for every member: those who gain access are pushed, those who lose their last grant are deprovisioned.
A user is in scope for an SP if they belong to any group that has
been granted access to that SP (or any ancestor group, when the SP is
configured for effective membership). When a user falls out of scope,
the worker translates this into a SCIM DELETE or a SCIM PUT with
active: false, depending on the vendor's quirks.
Enabling SCIM on a registered SP¶
SCIM lives on its own tab on each SP's detail page. Open the SP, click SCIM, and fill in the configuration.
- Enable outbound SCIM -- master switch. The worker only picks up work for SPs with this checkbox set.
- Target URL -- the SCIM 2.0 base URL of the downstream
application. WeftID appends
/Users,/Groups, and resource IDs as needed. Vendor-specific base URLs are listed in the vendor sections below. Target URL is required before you can enable SCIM: the service rejects the save with a validation error if you check Enable outbound SCIM without first supplying a URL. - Application type -- the vendor preset. Selecting Slack, GitHub, Atlassian, or GitLab applies known compatibility transforms. Use Generic SCIM 2.0 for spec-correct providers (most other vendors).
- Group membership mode -- Effective (default) flattens subgroup membership: a user in a child group is reported as a member of every ancestor group granted access to the SP. Direct members only reports only the users explicitly listed in the group. Use Effective unless your downstream app cannot reconcile inheritance for you.
- Sync activity retention -- how long detailed per-row push
history is kept in
scim_sync_log. Choose 3, 6, 12, 24 months, or Forever. Older rows are deleted nightly. Admin audit events (token created, rotated, revoked; config changes) are kept indefinitely regardless of this setting.
Save the configuration before creating a bearer token. The API rejects credential operations when SCIM is disabled.
Pausing without deleting¶
You can uncheck Enable outbound SCIM while leaving the target URL, application type, and bearer tokens in place. Pausing this way:
- Stops the worker from picking up new pushes for this SP (pending rows that have not yet drained become "skipped" on the next pass).
- Leaves the credentials intact, so re-enabling is a one-click operation. No need to mint a new token or repaste it in the downstream app.
- Leaves the queue, sync-log retention, and audit history intact.
This is the right setting for a temporary maintenance window, a suspected misconfiguration you want to halt while investigating, or a paused integration that you intend to resume.
Credential lifecycle¶
Bearer tokens are how WeftID authenticates its outbound push.
WeftID supports two credential modes. Pick the one that matches how the downstream app handles SCIM bearer tokens.
Create token (WeftID generates the secret). WeftID mints a random bearer value, displays it once, and the admin pastes it into the downstream app's SCIM bearer-token field. This matches how Okta-style SCIM connectors work, and is what Slack, GitHub Enterprise, Atlassian Guard, GitLab, and most generic SCIM 2.0 apps accept.
Import existing token (the downstream app generates the secret). Some apps, notably Authentik and several self-hosted apps, mint the token on their side and expect the client to send it back verbatim. The admin copies the token out of the downstream app and pastes it into WeftID's import field.
Either way, the plaintext is stored only as a Fernet-encrypted ciphertext at rest; the worker decrypts it on each push.
Create a token¶
- On the SCIM tab, click Create token.
- The amber box reveals the plaintext token.
- Copy the value into your downstream application's SCIM bearer-token field (vendor-specific location below).
- Click Done to refresh the page. The token appears in the active list, identified by its first eight characters.
TODO: screenshot - amber plaintext token box right after creation
Import an existing token¶
- On the SCIM tab, click Import existing token.
- Paste the bearer value from the downstream app into the token field. Surrounding whitespace is trimmed; tokens with internal spaces are rejected.
- Click Save token. The page refreshes and the imported token appears in the active list, identified by its first eight characters.
Rotate does not apply to imported tokens: rotation always generates a fresh random secret, which the downstream app would reject. When the downstream app issues a new token, import it as a new credential and revoke the prior one after pushes succeed under the new value.
Imports emit a scim_token_imported audit event, distinct from
scim_token_created.
Rotate a token¶
Use Rotate when you suspect compromise or on a regular cadence. Rotation:
- Creates a fresh token (shown once in the amber box, same UX as Create).
- Schedules the prior token for revocation after a 24-hour overlap window. Both tokens are accepted during the overlap so in-flight pushes complete cleanly.
- Emits a
scim_token_rotatedaudit event.
Paste the new token into the downstream app during the overlap window. If you wait past 24 hours, the old token expires and any push using it fails until you update the downstream configuration.
TODO: screenshot - credential list showing one active token and one scheduled for revocation
Revoke a token¶
Use Revoke when you need to invalidate a token immediately (lost device, departing admin, suspected leak). Revocation is instantaneous: any in-flight push that has not yet authenticated will fail.
Revocation cannot be undone. Create a fresh token after revoking.
Sync activity panel¶
The Sync activity panel at the bottom of the SCIM tab summarizes queue depth and the most recent push attempts.
- Pending -- queue rows the worker has not yet picked up. Should stay near zero in steady state.
- Dead-lettered -- queue rows that exhausted their retry budget. Always investigate before clicking Retry dead-lettered: a dead-lettered row usually points to a real configuration problem (wrong target URL, revoked token, vendor schema mismatch).
- Sync log table -- per-row history of recent pushes. Each row shows the resource type (User or Group), resource ID, status, attempt number, started and completed timestamps, and the truncated error string.
Status meanings¶
pending-- enqueued, waiting for the worker.running-- worker is currently pushing this row.done-- push succeeded.donewith an amber Skipped badge -- the resource was already absent at the receiver (typically a 404 onDELETE). The queue row drained without an actual push; the Error column carries thealready_absentmarker. This is benign and common when deprovisioning a user the receiver never saw.failed-- push failed; the row is scheduled for retry with exponential backoff.dead_letter-- retry budget exhausted. No further attempts until the admin clicks Retry dead-lettered.
TODO: screenshot - sync activity panel showing a mix of done / failed / dead_letter rows
Common worker reason codes¶
The Error column shows a compact, machine-readable reason slug followed by extra context. The slugs are stable so admins (and grep) can spot patterns at a glance. The most common ones:
no_credential_source: outbound SCIM credential is not configured for this SP-- No active bearer token exists for this SP. Mint one on the SCIM tab and paste it into the downstream app.credential_decrypt_failed: credential <id> ciphertext could not be decrypted (check SECRET_KEY rotation)-- A bearer-token row exists but Fernet rejected its ciphertext. The usual cause is aSECRET_KEYrotation that did not re-encrypt existing rows. Mint a fresh token (the new one is encrypted with the current key) and revoke the broken row.scim_target_missing/scim_disabled_or_no_target-- the SP was deleted, SCIM was disabled on it, or the target URL was cleared between enqueue and drain. The queue row is discarded; the next config change re-enqueues if needed.unknown_resource_type: '<value>'-- a queue row carries a resource type the worker does not know (currently onlyuserandgroupare valid). Indicates a bad enqueue call upstream; file a bug.worker_exception: <ExceptionType>: <message>-- the worker raised an uncaught exception while building the payload or scoping the resource. Retryable, bounded by the attempts counter so a code-fix redeploy gets a clean second chance.permanent http=<code> <body excerpt>-- the downstream SP returned a non-retryable status (typically a 4xx other than 429). The row dead-letters immediately. Inspect the body excerpt for the SP's error message (401 Unauthorized,400 invalidValue, etc.).retryable http=<code> <body excerpt>-- a retryable failure (5xx, 429, or a vendor-specific retryable code). The row is scheduled with exponential backoff (1m / 5m / 30m / 2h / dead-letter).already_absent: ...-- the resource was already gone at the receiver when the worker tried to update or delete it. Surfaces with statusdoneand the amber Skipped badge. No retry; the queue row drains.remote_id_invalidated (HTTP 404; next attempt will POST)-- the receiver returned 404 for a resource WeftID thought it had previously created. The recorded id is cleared automatically and the next worker pass POSTs the resource again, recapturing a fresh id. The row is retryable; one cycle is normal.
Resource ID mapping¶
SCIM 2.0 says the receiver mints the canonical id for a resource
when the client POSTs it (RFC 7644 ยง3.3). After the first POST,
WeftID stores that id in the sp_scim_remote_ids mapping table so
later PUT, PATCH, and DELETE calls go against the receiver's
id rather than WeftID's internal UUID. This matters for any
spec-compliant receiver (most of them) where group members[].value
must reference the receiver's id, not the externalId.
- When a mapping is created -- on the first successful
POSTof a User or Group to an SP, the worker captures theidfrom the response body and stores it. Thescim_remote_id_mappedaudit event records the mapping. - When a mapping is used -- every subsequent
PUT/PATCH/DELETEagainst the same WeftID resource for the same SP uses the stored id. Group payloads also use stored ids for membervalueand$refso the receiver can resolve members. - When a mapping is cleared -- if the receiver returns 404 for a
PUT,PATCH, orDELETEagainst the stored id, the worker clears the mapping and reclassifies the outcome as retryable. The next passPOSTs the resource and re-captures a fresh id. Ascim_remote_id_invalidatedaudit event records the clear. - Backwards compatibility -- rows that pre-date the mapping
table (or any resource that has not yet been successfully
POSTed) fall back to using WeftID's UUID. Receivers that key onexternalIdcontinue to work unchanged; spec-compliant receivers re-mint and start mapping on the next push. - Group members without a mapping -- when a Group is pushed and
some of its members have not yet been
POSTed (no recorded id), those members are SKIPPED from the Group payload with a warning in the worker log. The next push (after the members are individually pushed) includes them. Emitting a WeftID UUID where the receiver expects its own id would silently drop the member at the receiver's resolver -- the skip is the safe alternative.
The mapping table is purely additive: removing it (or starting from an empty one after a migration) is safe. The worker self-heals via the fallback path on the next push.
Vendor walkthroughs¶
WeftID ships day-one quirk modules for four widely-used SaaS vendors. Any other SCIM 2.0 application can be configured using the Generic SCIM 2.0 preset.
Slack (Enterprise Grid)¶
Slack's SCIM API is documented at https://api.slack.com/scim.
- Application type:
Slack - Target URL:
https://api.slack.com/scim/v2 - Where to paste WeftID's bearer token:
- Sign in to your Slack Enterprise Grid org as an Org Owner.
- Navigate to Org admin > Settings > Authentication and enable SCIM provisioning. SAML SSO must already be configured between Slack and WeftID.
- Slack accepts a long-lived bearer that the SCIM client supplies. Paste the plaintext token from WeftID's amber box into Slack's SCIM-token field.
- Quirks the admin should know:
- Slack uses the spec-correct
urn:ietf:params:scim:schemas:core:2.0:UserURN. No vendor extension URN is needed. - Slack strips
$refon Group member entries; WeftID drops it before push (no admin action required). - Slack returns
429with aRetry-Afterheader during burst traffic. The worker honors it.
TODO: screenshot - Slack Org Owner SCIM provisioning settings page
GitHub Enterprise Cloud¶
GitHub's Enterprise SCIM API is documented at https://docs.github.com/en/enterprise-cloud@latest/rest/scim.
- Application type:
GitHub - Target URL:
https://api.github.com/scim/v2/enterprises/<your-enterprise-slug> - Where to paste WeftID's bearer token:
- Sign in to GitHub as an Enterprise Owner.
- Navigate to Your enterprises > Settings > Authentication security > SCIM.
- Configure WeftID's plaintext token as the SCIM bearer for
this enterprise. GitHub validates that the bearer carries the
scim:enterprisescope; ensure the SCIM provisioning flow is enabled before WeftID pushes its first request. - Quirks the admin should know:
- GitHub's SCIM is tied to SAML: a user's SAML NameID must
match the SCIM
externalId. If your IdP NameID format diverges from what GitHub expects, users will be SCIM-provisioned but unable to log in. The WeftID IdP setup guide notes how to align NameID withexternalId. - Group
membersremoves use the filter-path PATCH form (path: members[value eq "<id>"]) instead of a values list. The quirk module rewrites the spec-correct form into GitHub's form automatically. - GitHub uses
403for rate-limit responses (withx-ratelimit-remaining: 0); the worker classifies these as retryable rather than terminal.
TODO: screenshot - GitHub Enterprise Settings > Authentication security > SCIM
Atlassian (Atlassian Guard / Access)¶
Atlassian's provisioning documentation is at https://support.atlassian.com/provisioning-users/docs/about-scim-provisioning/.
- Application type:
Atlassian - Target URL:
https://api.atlassian.com/scim/directory/<directory-id>(the directory ID is shown when you set up user provisioning). - Where to paste WeftID's bearer token:
- Sign in to admin.atlassian.com as an Org Admin with Atlassian Guard (or Access).
- Open your organization, then Security > Identity providers and pick the IdP linked to this directory.
- Click Set up user provisioning. Atlassian displays the SCIM base URL and prompts for the SCIM bearer.
- Copy the plaintext token from WeftID's amber box and paste it into the Atlassian SCIM-bearer field.
- Quirks the admin should know:
- Atlassian rejects PATCH
replaceoperations with emptyvaluearrays; the quirk module drops them client-side. displayNameis strict: Atlassian fails the request with a400 invalidValueif it is missing or whitespace-only. WeftID trims the value and falls back touserNamewhen the canonicaldisplayNameis empty.- On
404for a known-provisioned user, the quirk module treats the deprovision as already complete and marks the pushdoneinstead offailed.
TODO: screenshot - admin.atlassian.com Security > Identity providers > Set up user provisioning
GitLab.com (Group SAML SCIM)¶
GitLab's SCIM setup is documented at https://docs.gitlab.com/ee/user/group/saml_sso/scim_setup.html.
- Application type:
GitLab - Target URL:
https://gitlab.com/api/scim/v2/groups/<your-group-path> - Where to paste WeftID's bearer token:
- Sign in to GitLab as the Owner of the top-level group.
- Navigate to Group > Settings > SAML SSO. You must have SAML SSO already configured.
- Scroll to SCIM Token. GitLab expects the SCIM client to present a bearer of the admin's choice; paste the plaintext token from WeftID's amber box.
- Set the Target URL to the SCIM base URL shown above the token field.
- Quirks the admin should know:
- GitLab couples
externalIdto the SAML NameID: a mismatch silently breaks login even when SCIM provisioning succeeds. Ensure the IdP attribute mapping for NameID lines up with theexternalIdWeftID sends (the user UUID by default). - GitLab returns
502 Bad Gatewayfrom its CloudFront layer during deploys; classified as retryable. 403with aLicense does not allow SCIM provisioningbody is classified as terminal (license-error); the row is dead-lettered immediately rather than retried.
TODO: screenshot - GitLab Group SAML SSO page with SCIM token section
Generic SCIM 2.0¶
Any spec-correct SCIM 2.0 application can be configured with
Generic SCIM 2.0 as the application type. The generic path
sends unmodified, spec-correct payloads and uses the spec's
urn:ietf:params:scim:schemas:core:2.0:User and
urn:ietf:params:scim:schemas:core:2.0:Group URNs.
Use Generic for vendors not in the list above (Zoom, Notion, Linear, PagerDuty, Datadog, Vercel, etc.). If a vendor's SCIM diverges from the spec in a way that breaks pushes, log a backlog item -- a new quirk module is the right fix, not a workaround in the generic path.
Troubleshooting¶
"Queue is growing, pushes are failing"¶
- Open the SP's SCIM tab and read the Sync activity panel.
- Inspect the most recent failed rows. The error column gives a truncated reason. Common causes:
401 Unauthorized-- the bearer token is invalid (expired, revoked, or not yet pasted into the downstream app).404 Not Foundon a User PUT -- the user was deleted in the downstream app. WeftID will create on the next attempt if the user is still in scope.400 invalidValue-- the downstream app rejected a field. For Atlassian, this is usually a missingdisplayName.429 Too Many Requests-- transient; the worker backs off.- If a row is
dead_letter, do not click Retry dead-lettered without first fixing the underlying cause. Re-attempting the same broken push wastes the backoff budget. - Once the root cause is fixed, Retry dead-lettered revives every dead row for the SP. The worker re-attempts them on its next pass.
"A user got deprovisioned and I didn't expect it"¶
- Open the user's profile and check the Audit log tab. The
trigger event (
group_membership_removed,user_inactivated,group_sp_grant_removed, ...) is logged with actor and timestamp. - Cross-reference with the SP's Sync activity panel: the
resulting SCIM DELETE or
active: falsepush is logged there. - If the trigger event was unexpected, fix the root cause in WeftID (re-add to group, reactivate user, restore grant). The change re-enqueues a SCIM push that re-provisions the user downstream.
"I rotated the token and now pushes fail"¶
The overlap window is 24 hours from rotation. If the new token has not been pasted into the downstream app within that window, the old token has expired and the downstream app rejects every push.
Fix:
- The new token's plaintext was shown once at rotation. If you captured it, paste it into the downstream app now.
- If you did not capture it, click Revoke on the active (new) credential, then Create token to start over.
"Old sync history disappeared"¶
The Sync activity retention setting on the SCIM tab controls how long detailed per-row history is kept. The default is 3 months. To keep history indefinitely, set retention to Forever.
Note that admin audit events (token created, rotated, revoked; config changes) are kept indefinitely regardless of this setting. The sync log only affects per-resource push history.
"I configured SCIM but nothing is being pushed"¶
- Verify Enable outbound SCIM is checked.
- Verify the SP has at least one bearer token in the active list (not all revoked).
- Verify the Target URL is reachable from WeftID's worker network. The worker logs DNS failures to the standard worker log.
- Verify users are in scope: a user with no granting group has no work to push. Add a group grant on the SP's Groups tab.
- Check that the background worker is running:
docker compose ps worker. SCIM pushes are processed by the same worker that handles other background jobs, with per-tenant slicing.