Skip to content

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

  1. On the SCIM tab, click Create token.
  2. The amber box reveals the plaintext token.
  3. Copy the value into your downstream application's SCIM bearer-token field (vendor-specific location below).
  4. 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

  1. On the SCIM tab, click Import existing token.
  2. Paste the bearer value from the downstream app into the token field. Surrounding whitespace is trimmed; tokens with internal spaces are rejected.
  3. 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_rotated audit 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.
  • done with an amber Skipped badge -- the resource was already absent at the receiver (typically a 404 on DELETE). The queue row drained without an actual push; the Error column carries the already_absent marker. 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 a SECRET_KEY rotation 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 only user and group are 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 status done and 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 POST of a User or Group to an SP, the worker captures the id from the response body and stores it. The scim_remote_id_mapped audit event records the mapping.
  • When a mapping is used -- every subsequent PUT / PATCH / DELETE against the same WeftID resource for the same SP uses the stored id. Group payloads also use stored ids for member value and $ref so the receiver can resolve members.
  • When a mapping is cleared -- if the receiver returns 404 for a PUT, PATCH, or DELETE against the stored id, the worker clears the mapping and reclassifies the outcome as retryable. The next pass POSTs the resource and re-captures a fresh id. A scim_remote_id_invalidated audit 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 on externalId continue 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:User URN. No vendor extension URN is needed.
  • Slack strips $ref on Group member entries; WeftID drops it before push (no admin action required).
  • Slack returns 429 with a Retry-After header 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:enterprise scope; 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 with externalId.
  • Group members removes 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 403 for rate-limit responses (with x-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 replace operations with empty value arrays; the quirk module drops them client-side.
  • displayName is strict: Atlassian fails the request with a 400 invalidValue if it is missing or whitespace-only. WeftID trims the value and falls back to userName when the canonical displayName is empty.
  • On 404 for a known-provisioned user, the quirk module treats the deprovision as already complete and marks the push done instead of failed.

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 externalId to the SAML NameID: a mismatch silently breaks login even when SCIM provisioning succeeds. Ensure the IdP attribute mapping for NameID lines up with the externalId WeftID sends (the user UUID by default).
  • GitLab returns 502 Bad Gateway from its CloudFront layer during deploys; classified as retryable.
  • 403 with a License does not allow SCIM provisioning body 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"

  1. Open the SP's SCIM tab and read the Sync activity panel.
  2. Inspect the most recent failed rows. The error column gives a truncated reason. Common causes:
  3. 401 Unauthorized -- the bearer token is invalid (expired, revoked, or not yet pasted into the downstream app).
  4. 404 Not Found on 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.
  5. 400 invalidValue -- the downstream app rejected a field. For Atlassian, this is usually a missing displayName.
  6. 429 Too Many Requests -- transient; the worker backs off.
  7. 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.
  8. 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"

  1. 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.
  2. Cross-reference with the SP's Sync activity panel: the resulting SCIM DELETE or active: false push is logged there.
  3. 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:

  1. The new token's plaintext was shown once at rotation. If you captured it, paste it into the downstream app now.
  2. 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"

  1. Verify Enable outbound SCIM is checked.
  2. Verify the SP has at least one bearer token in the active list (not all revoked).
  3. Verify the Target URL is reachable from WeftID's worker network. The worker logs DNS failures to the standard worker log.
  4. 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.
  5. 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.