Zoomed Image

SCIM Provisioning

Configuration Guide
Single Sign On

SCIM Provisioning

SCIM (System for Cross-domain Identity Management) is an industry-standard REST/JSON protocol for pushing user and group changes from an identity provider into a downstream application. xAssets implements SCIM 2.0 so identity providers such as Microsoft Entra ID, Okta or OneLogin can provision users and groups directly into xAssets without the daily directory pull.

The SCIM endpoint is a parallel surface to the standard REST API: it lives at /scim/v2/, uses bearer-token authentication, and emits application/scim+json responses conforming to RFC 7643 and RFC 7644.

Prerequisites

  • xAssets 7.3 or later (the SCIM endpoint and SCIMVirtualGroup table land in the 7.3 upgrade scripts).
  • Administrator access to xAssets to mint a SCIM token.
  • An identity provider that supports SCIM 2.0 push provisioning. Microsoft Entra ID is the primary integration target; Okta and OneLogin also work.
  • HTTPS access to your xAssets instance from the identity provider's network.

How SCIM Fits Alongside SSO Auto-Provisioning

xAssets already auto-provisions users on first SSO logon: any Entra user in a permitted group gets a Custodian + UserOptions record created the moment they sign in (see Maintaining User Identities and Entra Auto-Permissions). For pure joiners-via-login that mechanism is enough. SCIM solves the cases SSO auto-provision cannot:

Lifecycle event SSO auto-provision SCIM Why SCIM matters
Joiner who logs in Creates Custodian + UserOptions on first login Creates Custodian (and UserOptions if SCIMCreateMode = Both) Both work; SSO path is the simpler one
Joiner who never logs in (executive who only owns assets, contractor, asset-pool user) Never creates them Creates the Custodian record from the directory push SCIM is the only path
Leaver removed from Entra No event reaches xAssets — the user keeps their Custodian status, asset assignments and (if it exists) login Sets Custodian.CustodianStatusID = "Left" and UserOptions.DisabledFlag = 1 within ~40 minutes The primary driver. Without SCIM, leaver de-provisioning depends on someone running a script.
Group membership change between sessions Re-evaluated on next login only Pushed within the next provisioning cycle Important when group changes drive permissions
Asset reassignment on offboarding Cannot trigger — no event Triggers via the standard Custodian-status workflow Operational not theoretical
SOC 2 / ISO 27001 access-removal evidence No audit trail tied to Entra SCIM writes are attributed to the SCIM token's user, with timestamps and operations Hard requirement on enterprise procurement

The clean division is: SSO auto-provision owns UserOptions creation on first login; SCIM owns the Custodian lifecycle and de-provisioning. When a SCIM-provisioned user later logs in via SSO, the SSO auto-provision path finds the existing Custodian (matched by email/UPN) and creates the UserOptions row linked to it.

Approach Cadence Use when
SSO auto-provision (existing) At login Users will sign in and need a login account
Graph / LDAP pull (existing) Daily or every 2 hours Full directory inventory, customers with strict outbound-only network policies
SCIM push (this page) ~40 minutes (Entra default), under a minute on demand Timely de-provisioning of leavers, pre-provisioning Custodians for non-login users, compliance-driven access removal

Most customers will run all three paths concurrently. They do not conflict.

Architecture at a Glance

Entra ID -- bearer token, REST --> https://your-xassets.example.com/scim/v2/*
                                               |
                                               v
                                   SCIMRouter (BusClassNET04)
                                               |
                                   +-----------+-----------+
                                   |                       |
                            UserOptions / Custodian   UserGroup / SCIMVirtualGroup

The endpoint dispatches every URL under /scim/v2/ through a single SCIM-aware request handler. SCIM-scoped tokens cannot be used against the standard /api/api.ashx surface, and ordinary API tokens cannot be used against /scim/v2/. This separation is enforced server-side.

Generating a SCIM Token

Until the admin UI for token management lands, SCIM tokens are minted by calling the SpecialGenerateSCIMToken SaveSpecial command from any administrator session. The simplest way is a short AMSX script run from the admin console:

print(SaveSpecial("", "SpecialGenerateSCIMToken", {"365"}))

The argument is the token expiry in days. The result has the form special=<GUID> -- copy the GUID portion and paste it into the identity provider's secret token field. Display it once; it is not retrievable later.

Equivalent direct call from xacli:

xacli savespecial SpecialGenerateSCIMToken 365

To revoke a token:

SaveSpecial("", "SpecialRevokeSCIMToken", {"the-token-guid"})

Revocation deletes both the encrypted token file and its underlying APIKey, so the bearer immediately stops working.

Note: SCIM tokens carry a scim-only scope flag. The same token cannot be used against /api/api.ashx -- a leaked SCIM token has no value beyond the SCIM surface.

URL Map

All routes are mounted under /scim/v2/. Path segments are case-insensitive per the SCIM specification.

Method Path Purpose
GET /scim/v2/ServiceProviderConfig Capability declaration document
GET /scim/v2/ResourceTypes Lists User and Group resource types
GET /scim/v2/Schemas Lists supported schema URNs
GET /scim/v2/Users List users (filter, startIndex, count)
POST /scim/v2/Users Create a user
GET /scim/v2/Users/{id} Retrieve a user by id
PUT /scim/v2/Users/{id} Replace a user
PATCH /scim/v2/Users/{id} Partially update a user (Entra's main verb)
DELETE /scim/v2/Users/{id} Soft-delete a user (sets status to "Left", does not hard-delete)
GET /scim/v2/Groups List groups
POST /scim/v2/Groups Create a group
GET /scim/v2/Groups/{id} Retrieve a group with members
PUT /scim/v2/Groups/{id} Replace group including membership
PATCH /scim/v2/Groups/{id} Add or remove members
DELETE /scim/v2/Groups/{id} Remove group; members preserved

Resource ids are integers for users and a mix of UserGroupCode (permission groups) or VG-<n> (informational groups) for groups -- see "Two-Tier Group Model" below.

For dev or multi-tenant deployments with several databases on the same host, append ?database=<name> to any request. In single-tenant deployments the host header alone is sufficient.

Testing With curl

Set the environment variables once and reuse:

export SCIM_BASE="https://your-xassets.example.com/scim/v2"
export SCIM_TOKEN="paste-the-guid-from-the-mint-step"

Capability discovery

curl -sS "$SCIM_BASE/ServiceProviderConfig" \
  -H "Authorization: Bearer $SCIM_TOKEN" \
  -H "Accept: application/scim+json"

A successful response declares which SCIM features are supported:

{
  "schemas": ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
  "patch":  { "supported": true },
  "bulk":   { "supported": false, "maxOperations": 0, "maxPayloadSize": 0 },
  "filter": { "supported": true,  "maxResults": 200 },
  "changePassword": { "supported": false },
  "sort":   { "supported": false },
  "etag":   { "supported": false },
  "authenticationSchemes": [
    { "type": "oauthbearertoken", "name": "Bearer Token", "description": "Bearer token issued by xAssets admin" }
  ]
}

Create a user

curl -sS -X POST "$SCIM_BASE/Users" \
  -H "Authorization: Bearer $SCIM_TOKEN" \
  -H "Content-Type: application/scim+json" \
  -d '{
    "schemas":    ["urn:ietf:params:scim:schemas:core:2.0:User",
                   "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"],
    "externalId": "abc-123-entra-objectid",
    "userName":   "alice@example.com",
    "name":       { "givenName": "Alice", "familyName": "Adams" },
    "active":     true,
    "emails":     [{ "type": "work", "value": "alice@example.com", "primary": true }],
    "title":      "Senior Engineer",
    "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {
      "employeeNumber": "E4821",
      "department":     "Engineering"
    }
  }'

Response is 201 Created with the full SCIM User resource and a Location header pointing at /scim/v2/Users/<id>. The id is an integer that maps to Custodian.CustodianID server-side.

List users with a filter

curl -sS "$SCIM_BASE/Users?filter=userName+eq+%22alice%40example.com%22&count=10" \
  -H "Authorization: Bearer $SCIM_TOKEN"

Filters supported in this release: eq, ne, co (contains), sw (starts with), pr (present), combined with and. or, parentheses and the broader filter grammar return 501 invalidFilter; identity providers fall back to full enumeration when they receive that response.

Patch a user (deactivate)

curl -sS -X PATCH "$SCIM_BASE/Users/4821" \
  -H "Authorization: Bearer $SCIM_TOKEN" \
  -H "Content-Type: application/scim+json" \
  -d '{
    "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
    "Operations": [
      { "op": "replace", "path": "active", "value": false }
    ]
  }'

Setting active = false sets Custodian.CustodianStatusID to "Left" and UserOptions.DisabledFlag = 1. The user record is preserved -- SCIM never hard-deletes.

Delete a user

curl -sS -X DELETE "$SCIM_BASE/Users/4821" \
  -H "Authorization: Bearer $SCIM_TOKEN"

Returns 204 No Content. Equivalent to PATCH active=false -- the row remains queryable, with status set to "Left".

Create a permission group with members

curl -sS -X POST "$SCIM_BASE/Groups" \
  -H "Authorization: Bearer $SCIM_TOKEN" \
  -H "Content-Type: application/scim+json" \
  -d '{
    "schemas":     ["urn:ietf:params:scim:schemas:core:2.0:Group"],
    "displayName": "xAssets SAM Managers",
    "externalId":  "entra-group-objectid-here",
    "members":     [{ "value": "4821" }, { "value": "4822" }]
  }'

Because the displayName starts with xAssets, this becomes a permission group -- a row in the UserGroup table. The returned id is the derived UserGroupCode.

Add a member via PATCH

curl -sS -X PATCH "$SCIM_BASE/Groups/SAMMGRS" \
  -H "Authorization: Bearer $SCIM_TOKEN" \
  -H "Content-Type: application/scim+json" \
  -d '{
    "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
    "Operations": [
      { "op": "add", "path": "members", "value": [{ "value": "4823" }] }
    ]
  }'

Testing With Postman

The standard xAssets API has a logon-then-call sequence that needs scripting to stitch session values together (see REST API Postman Guide -- non-trivial setup with xa-Authorization and Sec headers). SCIM is dramatically simpler: a single bearer token, no logon step, no scripts. The setup mirrors the existing guide's structure so you can reuse the same environment pattern.

Step 1 - Create a Postman Environment

  1. In the left sidebar, click Environments.
  2. Click + to create an environment named xAssets SCIM Production (or similar).
  3. Add the following variables:
Variable Initial Value
scim_base https://mycompany.mydomain.xassets.net/scim/v2
scim_token (paste the GUID from the SCIM token mint step)

The scim_token value should be marked as Secret in Postman so it does not appear in shared exports.

  1. Click Save and select the environment from the top-right dropdown.

Step 2 - Create a Collection

  1. In the left sidebar, click Collections.
  2. Click + to create a new collection named xAssets SCIM.
  3. Right-click the collection and choose Edit.

Step 3 - Configure Collection-Level Auth and Headers

This is the key shortcut: set authentication and content negotiation once at the collection level so every request inherits both.

  1. On the collection's Authorization tab:
    • Type: Bearer Token
    • Token: {{scim_token}}
  2. On the collection's Headers tab (if your Postman version puts headers under Variables + a header preset, the same fields apply):
Key Value
Accept application/scim+json
Content-Type application/scim+json
  1. Click Save.

Unlike the standard API there is no post-response script and no logon request -- the SCIM bearer is long-lived and self-contained.

Step 4 - Add Requests

Inside the collection, create requests that all leave Authorization and Headers set to Inherit auth from parent / Inherit headers from parent. The four most useful starter requests:

Name Method URL
ServiceProviderConfig GET {{scim_base}}/ServiceProviderConfig
Users - List GET {{scim_base}}/Users?count=10
Users - Create POST {{scim_base}}/Users
Users - Patch Active False PATCH {{scim_base}}/Users/{{user_id}}

For the Create body, paste the JSON shown earlier under "Create a user" into the request's Body tab (set type to raw, format to JSON).

For the Patch request, use the patch body shown earlier and put the user id either directly into the URL or in a user_id environment variable populated from a previous response (a one-line post-response script: pm.environment.set("user_id", pm.response.json().id);).

Step 5 - Send the First Request

Click Send on ServiceProviderConfig. You should get a 200 response with the capability document. If you get 401 the token is wrong or revoked; if 403, you pasted a non-SCIM token.

Running a Sequence

  1. Click your xAssets SCIM collection.
  2. Click Run collection.
  3. Order the requests: ServiceProviderConfig, then Users - Create, then Users - Patch Active False.
  4. Click Run xAssets SCIM.

This drives a complete provision-then-deactivate cycle in a few seconds, which is the same shape Entra performs every 40 minutes in production.

SSL Certificate Verification

If you are connecting to a development environment with a self-signed certificate, turn off SSL certificate verification in Postman's Settings (gear icon). This is rarely needed for production hosted instances.

Microsoft Entra ID Configuration

In the Microsoft Entra admin centre:

  1. Navigate to Enterprise applications, choose or create the xAssets application.
  2. Open Provisioning, set the provisioning mode to Automatic.
  3. Under Admin Credentials:
    • Tenant URL: https://your-xassets.example.com/scim/v2
    • Secret Token: the GUID from the SCIM token mint step
  4. Click Test Connection. Entra issues GET /scim/v2/ServiceProviderConfig and expects a 200 response.
  5. Under Mappings, leave the default User and Group attribute mappings unless you have a specific reason to customise. The defaults map cleanly to the xAssets SCIM implementation.
  6. Assign the users and groups you want provisioned to the enterprise application.
  7. Set the provisioning status to On. Entra runs an initial cycle within a few minutes, then continues every 40 minutes by default.

Two-Tier Group Model

xAssets stores SCIM groups in one of two tables depending on the displayName:

Trigger Stored in xAssets visibility
displayName starts with xAssets (configurable via the SCIMGroupPrefix SpecialOption) UserGroup table -- a full permission group Drives access to forms, queries and menus
Any other displayName SCIMVirtualGroup table -- an informational group Tracked on the Custodian record only; does not affect permissions

The membership for both tiers is stored as a CSV on Custodian.UserGroups. A user in two permission groups (ADMINS, SAMMGRS) and two informational groups (VG-47, VG-103) has UserGroups = "ADMINS,SAMMGRS,VG-47,VG-103". Their UserOptions.UserGroup (the alphabetically-first permission code) becomes ADMINS.

This design means a tenant with hundreds of Entra groups but only a handful of xAssets-relevant ones gets a curated UserGroup table -- the rest survive as round-trippable SCIM resources without polluting the permission catalogue.

User Mapping

A SCIM User resource maps onto two xAssets tables:

SCIM field xAssets target
id (integer) Custodian.CustodianID
externalId Custodian.ADSPath (prefixed entra:)
userName UserOptions.UserID if present, otherwise Custodian.UserID or Custodian.EMail
name.givenName, name.familyName Split from Custodian.CustodianName (best-effort -- xAssets stores a single name field)
displayName UserOptions.FullName or Custodian.CustodianName
emails[type=work].value UserOptions.Email and Custodian.EMail
phoneNumbers[type=work\|mobile].value Custodian.Phone / Custodian.Mobile
active UserOptions.DisabledFlag (inverted) and Custodian.CustodianStatusID
title Custodian.PostTitle
enterprise.employeeNumber Custodian.PostNumber
enterprise.department Custodian.DepartmentID (department auto-created on demand)
enterprise.manager.value Custodian.ManagerCustodianID (FK lookup)

By default, SCIM POSTs create a Custodian row only — not a UserOptions login account. The existing SSO auto-provision path creates the UserOptions row on first login and links it to the Custodian by matching email/UPN. The two paths share responsibility cleanly:

  • SCIM owns the Custodian record (presence, status, ADSPath, departments, manager FK, group memberships).
  • SSO auto-provision owns the UserOptions record (login credential, FullName, Email).

A user who is SCIM-provisioned but never logs in stays as a Custodian-only record — correct for asset-owning identities that don't need an xAssets account. A user who later logs in via SSO automatically gets the matching UserOptions row, with the SCIM-managed status carried across (a SCIM-deactivated user is denied login by the standard DisabledFlag check that SCIM DELETE / active=false already set).

Setting SCIMCreateMode = Both

For deployments where SCIM is the primary identity feed and users must have a usable login account before their first sign-in (e.g. SCIM-only without SSO, or pre-provisioning admin accounts), set the SCIMCreateMode SpecialOption to Both. SCIM POSTs will then create the Custodian and a paired UserOptions row in one operation:

Field on UserOptions Value
UserID SCIM userName
FullName SCIM displayName (or givenName + familyName if displayName is absent)
Email SCIM work email
UserGroup USERS (default; the alphabetically-first permission group code overrides this on the next group-membership change)
CustodianID FK to the Custodian row created in the same POST
DisabledFlag mirrors SCIM active (false → 1, true → 0)
Password auto-generated and stored encrypted in CustomerDataPrivate (same path used by SSO first-login)

Behaviour is idempotent: re-POSTing the same externalId does not create a second UserOptions row. If a user was provisioned under Custodian mode and the option is later flipped to Both, the next POST or PATCH that touches that user fills in the missing UserOptions row.

To set the option:

INSERT INTO SpecialOption (SpecialOptionCode, SpecialOptionName, OptionValue, SpecialOptionClass)
VALUES ('SCIMCreateMode', 'SCIM Create Mode', 'Both', 'SCIM')

Or via the standard SpecialOption admin UI under Administration → Settings. Default is Custodian — leave it that way unless you know you need the UserOptions row to exist before first login.

Error Responses

Every error returns a standard SCIM error envelope:

{
  "schemas":  ["urn:ietf:params:scim:api:messages:2.0:Error"],
  "status":   "404",
  "detail":   "User not found",
  "scimType": "invalidValue"
}
Status scimType Meaning
401 invalidCredentials Missing, malformed or unrecognised bearer token
403 insufficientScope Token authenticated but lacks the scim-only scope
400 invalidValue Required field missing or value rejected (e.g. userName not supplied on POST)
400 invalidPath PATCH operation targets a path not in the supported whitelist
400 invalidSyntax Body is empty or not valid JSON
404 -- Unknown resource id or unknown URL
409 uniqueness Duplicate externalId or userName collision
413 -- Request body exceeds 1 MB
501 invalidFilter Filter uses an unsupported operator (e.g. or, parentheses)

Troubleshooting

Symptom Likely cause
Entra "Test Connection" returns 401 The Secret Token field contains an old or revoked GUID. Mint a new token and paste again.
Entra reports 403 on every request A non-SCIM API token was pasted into the SCIM secret field. Use SpecialGenerateSCIMToken, not SpecialGenerateAPIToken.
Users appear in xAssets without a department The Entra user has no department attribute, or the mapping has been removed in the Entra Provisioning Mappings UI.
Group membership changes don't appear Entra schedules group reconciles on its own cadence -- typically within 40 minutes. Trigger an on-demand cycle from the Provisioning page.
UserGroup table contains unexpected rows A group was created with the displayName prefix xAssets. Either rename the Entra group or change the SCIMGroupPrefix SpecialOption.
500 with "potentially dangerous Request.Path value" The client sent a colon (:) in the URL path. Resource ids no longer use colons -- update to the latest IdP connector or strip the prefix client-side.