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¶
GET /ServiceProviderConfig, to learn which features the target supports (information only).POST /Userswith the User template. Expects201, aLocationheader andmeta.location.GET /Schemas, to learn which attributes are read-only (see Attribute mutability).GET /Users/{id}.GET /Users?filter=userName eq "…". Expects exactly the created user.PATCH /Users/{id}with the PATCH operations. Expects200with the resource, or204, in which case Helena re-reads the user.PUT /Users/{id}with the user template plus PUT changes.PATCH /Users/{id}withactive: false(deactivation, as IdPs do it).DELETE /Users/{id}. Expects204. ThenGET /Users/{id}, which must return a SCIM404error.
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¶
POST /Users(the member), thenPOST /Groupswith the Group template and that member.GET /Groups/{id}, thenGET /Groups?filter=displayName eq "…".PATCHremovemembers[value eq "{user-id}"]. Then checks the member is gone.PATCHadd the member back. Then checks it is present.DELETE /Groups/{id}, which expects204and then404. ThenDELETE /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) anddisplayNameare echoed back.- Filter results contain exactly the created resource.
- PATCH and PUT values are applied, except for
readOnlyandimmutableattributes. activeisfalseafter deactivation.- Group membership is correct after each PATCH.
- A deleted resource returns
404. totalResultsis an integer, andResourcesis present whentotalResults > 0.
With Specification checks on:
- Responses are JSON.
schemascontains the core schema URN, andmeta.resourceTypeis set.Locationandmeta.locationare set on create.- List responses include the
ListResponseschema. - Errors follow RFC 7644 §3.12: the
Errorschema,statusequal to the HTTP status, and a validscimType. /ServiceProviderConfighas its schema.
Notes are informational and never fail a run. They cover:
application/jsoninstead ofapplication/scim+jsonLocationdiffering frommeta.location- a numeric error
status - an unknown
scimType - discovery lists returned as bare arrays
- a group create response without
members - a missing
Groupresource type - PATCH paths that couldn't be verified
- skipped
readOnlyorimmutablechanges
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}. |