Outbound SCIM Connector¶
Built-in connector for provisioning and managing users + group memberships in any SCIM 2.0-compliant identity provider (Okta, Microsoft Entra ID / Azure AD, OneLogin, Keycloak, JumpCloud, PingOne, Auth0, etc.). Floh acts as the SCIM client; the IdP exposes the SCIM REST endpoints.
Pair this with the inbound SCIM endpoint when an IdP is the system of record for users coming into Floh, and use the outbound connector when Floh needs to push lifecycle events into a downstream IdP as part of an entitlement workflow.
When to Use¶
- Provision a new hire into an IdP at the end of an onboarding workflow.
- Grant or revoke an IdP group as the side effect of a Floh role grant / revoke (
reconcilesWithonaddGroupMember/removeGroupMember). - Deactivate a user in the IdP when their employment ends (soft-delete).
- Sync user accounts from a SCIM IdP into Floh's resource catalog (cursor-paginated
listUsers).
Prerequisites¶
- The target IdP must expose a SCIM 2.0 API. Most SaaS IdPs do; check the vendor's "SCIM provisioning" docs for the base URL.
- An API credential that the IdP accepts on its SCIM endpoint:
- A long-lived bearer token (Okta, OneLogin, JumpCloud, Auth0).
- Basic auth (rare; some self-hosted Keycloak deployments).
- OAuth2 client credentials (Microsoft Entra ID, PingOne, some Keycloak setups).
- Network egress from the Floh server to the IdP's SCIM host. The connector enforces SSRF protections — see Security & Networking below.
Connection Configuration¶
Create a connector instance via the Connectors API or UI with type scim.
| Field | Type | Required | Secret | Description |
|---|---|---|---|---|
baseUrl |
string | Yes | No | SCIM v2 base URL (e.g. https://idp.example.com/scim/v2). Trailing slash is normalized. |
authType |
select | Yes | No | bearer, basic, or oauth2_client_credentials. Defaults to bearer. |
bearerToken |
string | When authType=bearer |
Yes | Static bearer token issued by the IdP. |
username |
string | When authType=basic |
No | Basic auth username. Must not contain : (RFC 7617 §2). |
password |
string | When authType=basic |
Yes | Basic auth password. |
oauth2TokenUrl |
string | When authType=oauth2_client_credentials |
No | OAuth2 token endpoint URL. |
oauth2ClientId |
string | When authType=oauth2_client_credentials |
No | OAuth2 client id. |
oauth2ClientSecret |
string | When authType=oauth2_client_credentials |
Yes | OAuth2 client secret. |
oauth2Scope |
string | No | No | Space-separated OAuth2 scopes requested at token issuance. |
userResourcePath |
string | No | No | Path appended to baseUrl for users. Defaults to /Users. |
groupResourcePath |
string | No | No | Path appended to baseUrl for groups. Defaults to /Groups. |
Example: Okta (bearer token)¶
{
"baseUrl": "https://example.okta.com/scim/v2",
"authType": "bearer",
"bearerToken": "00aBcDeFgHiJkLmNoPqRsTuVwXyZ1234567890ABCDEF"
}
Example: Microsoft Entra ID (OAuth2 client credentials)¶
{
"baseUrl": "https://graph.microsoft.com/rp/scim",
"authType": "oauth2_client_credentials",
"oauth2TokenUrl": "https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token",
"oauth2ClientId": "<app-client-id>",
"oauth2ClientSecret": "<app-client-secret>",
"oauth2Scope": "https://graph.microsoft.com/.default"
}
Per the Microsoft Entra SCIM API reference, Entra exposes its SCIM endpoints under https://graph.microsoft.com/rp/scim. When integrating with a downstream SaaS app instead of Entra itself, use that app's documented SCIM base URL and bearer / OAuth2 credentials.
OAuth2 access tokens are cached on the connector instance for the lifetime of the workflow step and refreshed automatically when within 60 seconds of expiry.
Commands¶
All commands send and receive application/scim+json per RFC 7644 §3.1.
test¶
Verifies connectivity by issuing a GET /Users?count=1&startIndex=1.
Parameters: none.
Output: ok (boolean), baseUrl (string), authType (string), message (string).
User Lifecycle¶
createUser¶
Create a SCIM user. On 409 uniqueness (or any 409 when linkExistingOnConflict=true, the default), the connector falls back to GET /Users?filter=userName eq "..." and returns the existing record so workflow retries stay idempotent.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
userName |
string | Yes | - | The SCIM userName (typically the user's primary email). |
email |
string | No | - | Primary work email. Emitted as emails[0] with type=work. |
givenName |
string | No | - | Given name (name.givenName). |
familyName |
string | No | - | Family name (name.familyName). |
displayName |
string | No | - | Display name. |
externalId |
string | No | - | SCIM externalId (use Floh's user id for traceability). |
department |
string | No | - | Enterprise extension department. |
title |
string | No | - | Enterprise extension title. |
active |
boolean | No | true |
Initial active flag. |
linkExistingOnConflict |
boolean | No | true |
If true, fall back to lookup-by-userName on 409 instead of failing. |
Output: created (boolean), userId (string), userName (string), linkedExisting (boolean).
Reconciles with: checkUserActive — the entitlements engine can use this command in role-grant flows.
getUser¶
Fetch a user by id or by userName (one is required).
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | Either | SCIM resource id. |
userName |
string | Either | SCIM userName to look up (filter). |
Output: user (raw SCIM resource), userId, active.
updateUser¶
Update mutable user attributes. Defaults to a SCIM PATCH (RFC 7644 §3.5.2) with one replace op per supplied field. Set useReplace=true to issue a PUT with the full resource representation instead (PUT requires userName).
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
id |
string | Yes | - | SCIM resource id. |
userName |
string | Conditional | - | Required when useReplace=true. |
email |
string | No | - | Replace primary email. |
givenName |
string | No | - | Replace given name. |
familyName |
string | No | - | Replace family name. |
displayName |
string | No | - | Replace display name. |
externalId |
string | No | - | Replace externalId. |
department |
string | No | - | Replace enterprise extension department. |
title |
string | No | - | Replace enterprise extension title. |
active |
boolean | No | - | Toggle active flag. |
useReplace |
boolean | No | false |
Issue a PUT with the full resource instead of PATCH. |
A PATCH with no mutable fields fails fast with updateUser PATCH requires at least one mutable field so accidental no-ops are caught at the workflow boundary.
Output: updated (boolean), userId.
deactivateUser¶
Soft-delete via PATCH { op: "replace", path: "active", value: false }.
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | SCIM resource id. |
Output: deactivated (boolean), userId.
Reconciles with: checkUserActive.
checkUserActive¶
Read the user's active flag. A 404 is treated as exists=false (idempotent reconciliation).
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | SCIM resource id. |
Output: isActive (boolean), exists (boolean), userId.
listUsers¶
Cursor-paginated user list. SCIM startIndex / itemsPerPage are mapped onto the connector sync cursor surface so the resource sync engine can drive incremental syncs.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
cursor |
string | No | - | Opaque cursor (decimal startIndex from the previous page). |
pageSize |
number | No | 100 | Max records per page (1 – 1 000). |
filter |
string | No | - | Optional SCIM filter (e.g. active eq true). |
resourceType |
string | No | - | Sync engine resource discriminator (set automatically when sync-driven). |
Output: resources (array of { externalId, displayName, email, attributes }), nextCursor (string when more pages remain), totalEstimate (number when the IdP returns totalResults).
This command is syncCapable: true — wire it up via the Resource Sync UI to keep Floh's user catalog aligned with the upstream IdP.
Group Membership¶
addGroupMember¶
Add a user to a group via PATCH { op: "add", path: "members", value: [{ value: <memberId> }] }. Per RFC 7644 §3.5.2, SCIM servers MUST treat re-adding an existing member as a no-op, so this command is safe to retry.
| Parameter | Type | Required | Description |
|---|---|---|---|
groupId |
string | Yes | SCIM group id. |
memberId |
string | Yes | SCIM user id. |
Output: added (boolean), groupId, memberId.
Reconciles with: checkGroupMembership.
removeGroupMember¶
Remove a user from a group via the SCIM filter form: PATCH { op: "remove", path: 'members[value eq "<memberId>"]' }. A 404 from the server is treated as already-removed for idempotency.
| Parameter | Type | Required | Description |
|---|---|---|---|
groupId |
string | Yes | SCIM group id. |
memberId |
string | Yes | SCIM user id. |
Output: removed (boolean), groupId, memberId.
Reconciles with: checkGroupMembership.
checkGroupMembership¶
Read the group resource (?attributes=members) and scan the members array for memberId. Works against IdPs that don't expose a per-member endpoint. A 404 on the group is treated as isMember=false.
| Parameter | Type | Required | Description |
|---|---|---|---|
groupId |
string | Yes | SCIM group id. |
memberId |
string | Yes | SCIM user id. |
Output: isMember (boolean), groupId, memberId.
Reconciliation Mappings¶
The following commands set reconcilesWith, so they integrate with the entitlement reconciliation surface:
| Action command | Reconciler | Use in role mappings |
|---|---|---|
createUser |
checkUserActive |
Provision identity → confirm activation. |
deactivateUser |
checkUserActive |
Deactivate identity → confirm active=false. |
addGroupMember |
checkGroupMembership |
Grant role → confirm group membership. |
removeGroupMember |
checkGroupMembership |
Revoke role → confirm membership removed. |
When you wire a Floh role to a SCIM group via link_config, the engine will automatically pick the matching reconciler so a manual reconcile/repair pass converges on the IdP's truth.
Security & Networking¶
The outbound SCIM client uses the same shared HTTP machinery as every other built-in connector:
- SSRF protection — every outbound URL (SCIM base URL and OAuth2 token URL) is run through
validateHttpConnectorUrl. Loopback, link-local, RFC 1918, carrier-grade NAT, cloud-metadata, and IPv6 ULA / link-local hosts are blocked. - DNS pinning — DNS-resolved hostnames are pinned to the resolved address for the lifetime of the request, preventing DNS-rebinding attacks between validation and connect.
- No automatic redirects — 3xx responses surface as terminal errors. A validated public URL cannot be silently bounced to an internal target via
Location:. - Sanitized logging — request URLs are sanitized via
sanitizeUrl()before they hit the connector logger; bearer tokens, basic-auth credentials, and OAuth2 client secrets are never logged. - Bounded retries —
429and503responses trigger up to 3 attempts total with capped exponential backoff (250 ms → 5 000 ms ceiling). TheRetry-Afterheader is honored when present (seconds or HTTP-date forms). All other non-2xx responses surface immediately asScimOutboundErrorwithstatusCode,scimType, andresponseBodyavailable toerrorMap.
Errors¶
All non-2xx SCIM responses become a ScimOutboundError, which the connector's errorMap translates into:
Diagnostics returned to the workflow runner include:
| Key | Description |
|---|---|
statusCode |
HTTP status of the SCIM response. |
scimType |
The SCIM scimType from the response body when present (e.g. uniqueness, mutability, invalidValue). |
responseBody |
Parsed JSON response body (no secrets — Floh logs are sanitized separately). |