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

FieldDefaultDescription
basePath/scim/v2Path prefix the SCIM endpoints are served under. Must start with / and must not overlap /mockserver.
idStrategyUUIDHow server-assigned ids are generated: UUID or AUTO_INCREMENT.
initialUsers[]Users to seed the store with.
initialGroups[]Groups to seed the store with.
enforceFiltertrueWhen true, the filter query parameter is applied; when false, a filtered list returns 501 Not Implemented.
enforcePatchtrueWhen false, PATCH returns 501 Not Implemented.
requireBearerTokenfalseWhen true, resource endpoints require an Authorization: Bearer … header (discovery endpoints stay open).
expectedBearerTokennullThe exact token required; null accepts any non-empty bearer token.

Generated Endpoints

Method & PathBehaviour
GET {basePath}/UsersList users (supports ?filter, ?sortBy, ?sortOrder, ?startIndex, ?count) — returns a ListResponse.
POST {basePath}/UsersCreate 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}/ServiceProviderConfigDiscovery — advertises supported features (patch/filter reflect the config; sort is true; bulk/etag are false; OAuth bearer-token auth scheme).
GET {basePath}/ResourceTypesDiscovery — a ListResponse of the User and Group resource types.
GET {basePath}/SchemasDiscovery — 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".