Mock a SCIM Provider
Mock a SCIM 2.0 Provider
MockServer can stand in for a SCIM 2.0 identity provider, so you can test System for Cross-domain Identity Management (SCIM) clients — user provisioning, deprovisioning, group membership sync — without a real IdP. A single control-plane call generates a fully functional provider that serves CRUD over Users and Groups plus the SCIM discovery documents, with the data held in memory.
Resources are stored and returned in SCIM shapes: every resource carries schemas, an id, and a meta object; lists are wrapped in a SCIM ListResponse envelope; errors use the SCIM Error envelope; and all responses use the application/scim+json media type.
Create a SCIM Provider
A single control-plane PUT /mockserver/scim creates the provider. Send an empty body to create one with the default base path /scim/v2, or supply a configuration body to customise the base path, seed initial data, choose the id strategy, or require a bearer token. The example below seeds one user and one group and requires a bearer token:
Send an empty body (or omit the configuration entirely) to create a provider with all defaults; the configuration body below customises it.
// default provider at /scim/v2
mockServerClient.mockScimProvider();
// or with configuration
mockServerClient.mockScimProvider(
new ScimProviderConfiguration()
.setBasePath("/scim/v2")
.setIdStrategy(CrudExpectationsDefinition.IdStrategy.UUID)
.setEnforceFilter(true)
.setEnforcePatch(true)
.setRequireBearerToken(true)
.setExpectedBearerToken("my-test-token")
);
The typed helper exposes the scalar options directly. To seed initialUsers / initialGroups, pass a List<ObjectNode> of SCIM resource nodes to setInitialUsers(...) / setInitialGroups(...), or send the raw JSON configuration body shown in the REST API tab.
const config = {
"basePath": "/scim/v2",
"idStrategy": "UUID",
"requireBearerToken": true,
"expectedBearerToken": "my-test-token",
"enforceFilter": true,
"enforcePatch": true,
"initialUsers": [
{ "id": "u1", "userName": "bjensen@example.com", "active": true }
],
"initialGroups": [
{ "id": "g1", "displayName": "Admins", "members": [] }
]
};
fetch("http://localhost:1080/mockserver/scim", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(config)
}).then(function () {
console.log("SCIM provider created");
});
import requests
config = {
"basePath": "/scim/v2",
"idStrategy": "UUID",
"requireBearerToken": True,
"expectedBearerToken": "my-test-token",
"enforceFilter": True,
"enforcePatch": True,
"initialUsers": [
{"id": "u1", "userName": "bjensen@example.com", "active": True}
],
"initialGroups": [
{"id": "g1", "displayName": "Admins", "members": []}
]
}
requests.put("http://localhost:1080/mockserver/scim", json=config)
require 'net/http'
require 'json'
require 'uri'
config = {
"basePath" => "/scim/v2",
"idStrategy" => "UUID",
"requireBearerToken" => true,
"expectedBearerToken" => "my-test-token",
"enforceFilter" => true,
"enforcePatch" => true,
"initialUsers" => [
{ "id" => "u1", "userName" => "bjensen@example.com", "active" => true }
],
"initialGroups" => [
{ "id" => "g1", "displayName" => "Admins", "members" => [] }
]
}
uri = URI('http://localhost:1080/mockserver/scim')
request = Net::HTTP::Put.new(uri, 'Content-Type' => 'application/json')
request.body = config.to_json
Net::HTTP.start(uri.hostname, uri.port) { |http| http.request(request) }
package main
import (
"bytes"
"net/http"
)
func main() {
body := []byte(`{
"basePath": "/scim/v2",
"idStrategy": "UUID",
"requireBearerToken": true,
"expectedBearerToken": "my-test-token",
"enforceFilter": true,
"enforcePatch": true,
"initialUsers": [
{ "id": "u1", "userName": "bjensen@example.com", "active": true }
],
"initialGroups": [
{ "id": "g1", "displayName": "Admins", "members": [] }
]
}`)
req, _ := http.NewRequest("PUT",
"http://localhost:1080/mockserver/scim",
bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
http.DefaultClient.Do(req)
}
using System.Net.Http;
using System.Text;
using var httpClient = new HttpClient();
var json = @"{
""basePath"": ""/scim/v2"",
""idStrategy"": ""UUID"",
""requireBearerToken"": true,
""expectedBearerToken"": ""my-test-token"",
""enforceFilter"": true,
""enforcePatch"": true,
""initialUsers"": [
{ ""id"": ""u1"", ""userName"": ""bjensen@example.com"", ""active"": true }
],
""initialGroups"": [
{ ""id"": ""g1"", ""displayName"": ""Admins"", ""members"": [] }
]
}";
await httpClient.PutAsync(
"http://localhost:1080/mockserver/scim",
new StringContent(json, Encoding.UTF8, "application/json")
);
use reqwest::blocking::Client;
let client = Client::new();
let body = r#"{
"basePath": "/scim/v2",
"idStrategy": "UUID",
"requireBearerToken": true,
"expectedBearerToken": "my-test-token",
"enforceFilter": true,
"enforcePatch": true,
"initialUsers": [
{ "id": "u1", "userName": "bjensen@example.com", "active": true }
],
"initialGroups": [
{ "id": "g1", "displayName": "Admins", "members": [] }
]
}"#;
client.put("http://localhost:1080/mockserver/scim")
.header("Content-Type", "application/json")
.body(body.to_string())
.send()
.unwrap();
$json = <<<'JSON'
{
"basePath": "/scim/v2",
"idStrategy": "UUID",
"requireBearerToken": true,
"expectedBearerToken": "my-test-token",
"enforceFilter": true,
"enforcePatch": true,
"initialUsers": [
{ "id": "u1", "userName": "bjensen@example.com", "active": true }
],
"initialGroups": [
{ "id": "g1", "displayName": "Admins", "members": [] }
]
}
JSON;
$ch = curl_init('http://localhost:1080/mockserver/scim');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_POSTFIELDS => $json,
CURLOPT_HTTPHEADER => ['Content-Type: application/json; charset=utf-8'],
CURLOPT_RETURNTRANSFER => true,
]);
curl_exec($ch);
curl_close($ch);
Send an empty PUT for an all-defaults provider:
curl -v -X PUT "http://localhost:1080/mockserver/scim"
Or supply a configuration body:
curl -v -X PUT "http://localhost:1080/mockserver/scim" -d '{
"basePath": "/scim/v2",
"idStrategy": "UUID",
"requireBearerToken": true,
"expectedBearerToken": "my-test-token",
"enforceFilter": true,
"enforcePatch": true,
"initialUsers": [
{ "id": "u1", "userName": "bjensen@example.com", "active": true }
],
"initialGroups": [
{ "id": "g1", "displayName": "Admins", "members": [] }
]
}'
Configuration Options
| Field | Default | Description |
|---|---|---|
| basePath | /scim/v2 | Path prefix the SCIM endpoints are served under. Must start with / and must not overlap /mockserver. |
| idStrategy | UUID | How server-assigned ids are generated: UUID or AUTO_INCREMENT. |
| initialUsers | [] | Users to seed the store with. |
| initialGroups | [] | Groups to seed the store with. |
| enforceFilter | true | When true, the filter query parameter is applied; when false, a filtered list returns 501 Not Implemented. |
| enforcePatch | true | When false, PATCH returns 501 Not Implemented. |
| requireBearerToken | false | When true, resource endpoints require an Authorization: Bearer … header (discovery endpoints stay open). |
| expectedBearerToken | null | The exact token required; null accepts any non-empty bearer token. |
Generated Endpoints
| Method & Path | Behaviour |
|---|---|
| GET {basePath}/Users | List users (supports ?filter, ?sortBy, ?sortOrder, ?startIndex, ?count) — returns a ListResponse. |
| POST {basePath}/Users | Create a user (userName required) — 201 with a Location header. |
| GET {basePath}/Users/{id} | Get a user — 200 or 404 error envelope. |
| PUT {basePath}/Users/{id} | Replace a user (preserves id and meta.created). |
| PATCH {basePath}/Users/{id} | Apply a SCIM PatchOp. |
| DELETE {basePath}/Users/{id} | Delete a user — 204 or 404. |
| The same six endpoints exist for {basePath}/Groups (displayName required on create). | |
| GET {basePath}/ServiceProviderConfig | Discovery — advertises supported features (patch/filter reflect the config; sort is true; bulk/etag are false; OAuth bearer-token auth scheme). |
| GET {basePath}/ResourceTypes | Discovery — a ListResponse of the User and Group resource types. |
| GET {basePath}/Schemas | Discovery — a ListResponse of the core User and Group schema definitions. |
ListResponse Envelope
{
"schemas": [ "urn:ietf:params:scim:api:messages:2.0:ListResponse" ],
"totalResults": 2,
"startIndex": 1,
"itemsPerPage": 2,
"Resources": [ /* shaped User or Group resources */ ]
}
Error Envelope
{
"schemas": [ "urn:ietf:params:scim:api:messages:2.0:Error" ],
"scimType": "invalidValue",
"detail": "userName is required",
"status": "400"
}
Supported Filters
A single attr op "value" comparison over a top-level attribute is supported: eq (equals), co (contains), sw (starts with), and the presence operator pr. For example ?filter=userName eq "bjensen@example.com". A filter referencing an unknown attribute matches nothing. Compound expressions (and/or/not), value-paths, and the ordered operators (gt/ge/lt/le) are not yet supported.
Sorting
List responses can be ordered with the sortBy and sortOrder query parameters (SCIM RFC 7644). sortBy is an attribute name, including a nested dotted path such as name.familyName; sortOrder is ascending (the default) or descending. For example ?sortBy=userName&sortOrder=descending. Comparison is case-insensitive, and resources whose sort attribute has no value are always ordered last (regardless of sort order). Sorting is applied after any filter and before startIndex/count pagination. A malformed sortBy path, or a sortOrder other than ascending/descending, returns a 400 error envelope.
Supported PATCH Operations
A SCIM PatchOp body (urn:ietf:params:scim:api:messages:2.0:PatchOp) with an Operations array is supported over a top-level attribute (emails) or a one-level sub-attribute (name.familyName):
- replace with a path — set the attribute.
- replace without a path — shallow-merge the value object.
- add with a path — set a scalar, or append to an array attribute (e.g. emails, members).
- add without a path — shallow-merge the value object.
- remove with a path — delete the attribute or sub-attribute.
A malformed PatchOp returns a 400 error envelope with scimType: "invalidSyntax".