Skip to content

Identity: SCIM Client

The SCIM Client test checks your SCIM 2.0 service provider: the /scim/v2 API your application exposes so identity providers can provision users and groups into it. Helena plays the SCIM client, the role Okta, Microsoft Entra ID, OneLogin or JumpCloud play in production. It sends the requests those connectors send and checks that your API answers as RFC 7643 and RFC 7644 require.

Users and groups a run creates are deleted again afterwards, even when a step fails.

Sidebar: Identity → SCIM Client

Before you begin

  • You need the SCIM base URL, for example https://app.example.com/scim/v2. It must be public HTTPS.
  • You need a bearer token, or OAuth client credentials that can obtain one, with permission to create, update and delete users (and groups, for the group lifecycle).
  • Lifecycle operations create and delete real records in the target. Use a test tenant or directory where you can.

Operations

Each test performs one operation. Create several tests to cover several operations.

Operation operation What a run does
Discovery discovery Reads /ServiceProviderConfig, /ResourceTypes and /Schemas.
User lifecycle user_lifecycle Creates a user, reads it, filters for it, PATCHes it, replaces it with PUT, deactivates it and deletes it.
Group lifecycle group_lifecycle Creates a user and a group containing it, reads and filters the group, removes the member and adds it back, then deletes both.
Custom request custom_request Sends one request you define (method, path, JSON body) and checks the response.

User lifecycle in detail

  1. GET /ServiceProviderConfig, to learn which features the target supports (information only).
  2. POST /Users with the User template. Expects 201, a Location header and meta.location.
  3. GET /Schemas, to learn which attributes are read-only (see Attribute mutability).
  4. GET /Users/{id}.
  5. GET /Users?filter=userName eq "…". Expects exactly the created user.
  6. PATCH /Users/{id} with the PATCH operations. Expects 200 with the resource, or 204, in which case Helena re-reads the user.
  7. PUT /Users/{id} with the user template plus PUT changes.
  8. PATCH /Users/{id} with active: false (deactivation, as IdPs do it).
  9. DELETE /Users/{id}. Expects 204. Then GET /Users/{id}, which must return a SCIM 404 error.

If /ServiceProviderConfig says patch.supported or filter.supported is false, those steps are skipped rather than failed. You can also turn steps off individually.

Group lifecycle in detail

  1. POST /Users (the member), then POST /Groups with the Group template and that member.
  2. GET /Groups/{id}, then GET /Groups?filter=displayName eq "…".
  3. PATCH remove members[value eq "{user-id}"]. Then checks the member is gone.
  4. PATCH add the member back. Then checks it is present.
  5. DELETE /Groups/{id}, which expects 204 and then 404. Then DELETE /Users/{member}.

The members[value eq "…"] remove path is the form IdPs use, and the one that most often breaks servers.

Field reference

Target

Field Key Required Default Supported values / notes
SCIM base URL base_url ✅ — The root that /Users, /Groups and /ServiceProviderConfig sit under. Public HTTPS, up to 2048 characters.
Authentication auth_method ✅ bearer Bearer token (bearer): a static token, as configured in most IdP connectors. OAuth client credentials (oauth_client_credentials): Helena first requests a token from a token endpoint (RFC 6749 §4.4), then uses it as the bearer token.
Bearer token secret bearer_token ✅ (bearer) — Up to 8192 characters. Encrypted, never shown again.
Token endpoint token_endpoint ✅ (OAuth) — Public HTTPS URL.
Client ID client_id ✅ (OAuth) —
Client secret secret client_secret ✅ (OAuth) — Encrypted, never shown again.
Client authentication client_auth_method client_secret_basic client_secret_basic or client_secret_post.
scope, audience scope, audience — Optional token request parameters.

Only the secret the chosen Authentication needs is kept. If you switch methods, the other secret is discarded.

Operation

Field Key Applies to Default Supported values / notes
Operation operation All user_lifecycle discovery, user_lifecycle, group_lifecycle, custom_request
User template (JSON) user_template Lifecycles See below A JSON object (up to 10,000 characters) that must contain userName. The core User schema is added to schemas if it is missing.
Group template (JSON) group_template Group lifecycle See below A JSON object that must contain displayName. Helena adds the member.
PATCH operations (JSON) patch_operations User lifecycle [{"op":"replace","path":"displayName","value":"Helena Patched {{random}}"}] The Operations array of the PatchOp request (RFC 7644 §3.5.2).
PUT changes (JSON) put_changes User lifecycle {"displayName":"Helena Replaced {{random}}"} A JSON object deep-merged into the user template for the PUT step. Use nested objects for sub-attributes. It must not contain id, schemas or meta.
Filter by userName / displayName include_filter Lifecycles On
PATCH user / PATCH membership include_patch Lifecycles On
PUT (replace) user include_put User lifecycle On
Deactivate user (active = false) include_deactivate User lifecycle On
Method custom_method Custom request GET GET, POST, PUT, PATCH, DELETE
Path custom_path Custom request — Required. Relative to the base URL, starting with /. It may include a query string, e.g. /Users?startIndex=1&count=10. No spaces or #.
Body (JSON) custom_body Custom request — Sent as application/scim+json for POST, PUT and PATCH only.
Clean up cleanup All On Delete users and groups the run created but did not delete itself, for example after a failed step.

Default User template:

{
  "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
  "externalId": "helena-{{run_uuid}}",
  "userName": "helena-{{random}}@example.com",
  "name": {"givenName": "Helena", "familyName": "User {{random}}"},
  "displayName": "Helena User {{random}}",
  "emails": [{"value": "helena-{{random}}@example.com", "type": "work", "primary": true}],
  "active": true
}

Default Group template:

{
  "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
  "externalId": "helena-{{run_uuid}}",
  "displayName": "Helena Group {{random}}"
}

Placeholders

Placeholders are replaced in string values of the templates, PATCH operations, PUT changes and custom body. They are replaced after the JSON is parsed, so a value can never break the JSON.

Placeholder Replaced with
{{random}} 8 random lowercase characters, the same everywhere in one run. This keeps userName unique across runs.
{{run_uuid}} The run's UUID, as shown in the Test Log
{{timestamp}} The current Unix time in seconds

Expected result

Field Key Default Supported values / notes
The target should expected_outcome success Accept the requests (success) or Refuse the first request (rejection). See Expected rejection.
HTTP status of the first request expected_status — 100–599. Blank: the status the RFC requires (e.g. 201 on create), any 2xx for custom requests, or any 4xx for a refusal.
scimType expected_scim_type — Refusal only. Exact match (RFC 7644 §3.12), e.g. uniqueness.
Specification checks strict_spec_checks On See What gets checked.

Expected rejection

With Refuse the first request, only the operation's first request is sent. In a lifecycle, that is POST /Users. The request must be refused with a 4xx (or HTTP status of the first request) and a well-formed SCIM error. If the target unexpectedly creates the resource, the run fails and cleanup deletes the resource.

To test an error halfway through a lifecycle, such as a duplicate user, use a Custom request.

Attribute mutability

Before modifying a user, the user lifecycle reads /Schemas to see how each attribute it changes is declared (RFC 7643 §7):

Declared mutability What Helena checks
readWrite, writeOnly, or not declared The new value must appear in the PATCH or PUT result.
readOnly Not checked. The RFC says supplied values "SHALL be ignored", so a note records what was sent and what came back.
immutable Not checked, because the value can't change once set. A note is recorded.

A readOnly parent makes its sub-attributes readOnly. Extension attributes such as urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department are looked up in their extension's schema. If /Schemas can't be read, every change is checked and a note says so.

When the PUT step fails

Suppose your server computes displayName from name and silently ignores a supplied displayName. If /Schemas declares displayName as readOnly, the step passes. If it doesn't, the run fails: the server is out of spec for dropping a readWrite attribute. Either declare the attribute correctly, or change PUT changes to an attribute the server does store, e.g. {"name": {"familyName": "Replaced {{random}}"}}.

PATCH values are verified for add and replace operations on simple paths (displayName, name.givenName, extension attributes) and on path-less operations with an object value. Value-filter paths are recorded as notes.

What gets checked

Always:

  • The HTTP status of each request.
  • A created or returned resource has an id.
  • userName (case-insensitive) and displayName are echoed back.
  • Filter results contain exactly the created resource.
  • PATCH and PUT values are applied, except for readOnly and immutable attributes.
  • active is false after deactivation.
  • Group membership is correct after each PATCH.
  • A deleted resource returns 404.
  • totalResults is an integer, and Resources is present when totalResults > 0.

With Specification checks on:

  • Responses are JSON.
  • schemas contains the core schema URN, and meta.resourceType is set.
  • Location and meta.location are set on create.
  • List responses include the ListResponse schema.
  • Errors follow RFC 7644 §3.12: the Error schema, status equal to the HTTP status, and a valid scimType.
  • /ServiceProviderConfig has its schema.

Notes are informational and never fail a run. They cover:

  • application/json instead of application/scim+json
  • Location differing from meta.location
  • a numeric error status
  • an unknown scimType
  • discovery lists returned as bare arrays
  • a group create response without members
  • a missing Group resource type
  • PATCH paths that couldn't be verified
  • skipped readOnly or immutable changes

Reading the results

Step Shows
Resolve credentials / Obtain access token The bearer token or client secret as a fingerprint. The token response is redacted.
One step per SCIM request (e.g. Create user, Filter user by userName, Patch user: re-read) The request (method, URL, query, body) and response (status, Content-Type, Location, ETag, WWW-Authenticate, body). Values such as password are redacted.
Cleanup: delete … One step per leftover resource. If Clean up is off, a single skipped step lists them.
Notes The informational findings described above.

A run ends with status error when a secret is missing, the token request is refused, a URL is blocked, a connection fails, or the run would send more than 40 requests.

Examples

Okta-style provisioning check, nightly

Field Value
SCIM base URL https://app.example.com/scim/v2
Authentication Bearer token: the token from your Okta app's provisioning settings
Operation User lifecycle
Cron expression (UTC) 0 2 * * *

If your application needs extra attributes, add them to the User template:

{
  "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User",
              "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"],
  "userName": "helena-{{random}}@example.com",
  "name": {"givenName": "Helena", "familyName": "User"},
  "emails": [{"value": "helena-{{random}}@example.com", "type": "work", "primary": true}],
  "active": true,
  "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {"department": "QA"}
}

Duplicate users are refused

Field Value
Operation Custom request: POST, path /Users
Body (JSON) A user whose userName already exists in the target
The target should Refuse the first request
HTTP status of the first request 409
scimType uniqueness

A revoked token is refused

Field Value
Operation Discovery or User lifecycle
Authentication Bearer token: the revoked token
The target should Refuse the first request
HTTP status of the first request 401

Paging through users

Field Value
Operation Custom request: GET, path /Users?startIndex=1&count=10
The target should Accept the requests

Troubleshooting

Symptom Likely cause
The PUT or PATCH step fails on displayName The server ignores the attribute without declaring it readOnly. See Attribute mutability.
409 on create A user with the same userName already exists. Make sure the template uses {{random}}.
Filter step finds 0 results The server doesn't support filter=userName eq "…", or indexes new users with a delay.
Many notes about application/json Harmless. The server should send application/scim+json, but IdPs accept both.
Leftover helena-… users in the directory Clean up was off, or the delete failed (recorded as a note). Delete them by their externalId, which is helena-{run-uuid}.