tacticmedia/scim-server¶
SCIM 2.0 server library for PHP (RFC 7643, RFC 7644). ScimServer::handle() takes a Symfony HttpFoundation
Request and returns a SCIM Response. You supply one repository for each resource type.
The library does these tasks:
- It routes and answers the SCIM endpoints: resources, list and search, PATCH, Bulk and discovery.
- It validates request bodies against the schemas and applies PATCH operations.
- It checks uniqueness, applies
attributes,excludedAttributesandreturned, and evaluates ETags. - It sets
meta.resourceType,meta.locationandmeta.version, and fills absent$refvalues. - It contains the RFC 7643 User, Group and Enterprise User schemas and an in-memory repository.
The library does not do these tasks:
- Authentication and authorization. Authenticate each request before you call
handle(). - Storage. Implement
ResourceRepositoryfor your storage. - Rate limiting, logging and the HTTP routing of your application.
For a Symfony application, use tacticmedia/scim-server-bundle.
Installation¶
Requirement: PHP 8.5 or later.
The library needs no registration.
Quick start¶
use Psr\Clock\ClockInterface;
use Symfony\Component\HttpFoundation\Request;
use TacticMedia\ScimServer\Repository\InMemoryResourceRepository;
use TacticMedia\ScimServer\Schema\CoreResourceTypes;
use TacticMedia\ScimServer\Server\RegisteredResourceType;
use TacticMedia\ScimServer\Server\ScimServer;
$clock = new class implements ClockInterface {
public function now(): DateTimeImmutable
{
return new DateTimeImmutable();
}
};
$users = CoreResourceTypes::user();
$groups = CoreResourceTypes::group();
$server = new ScimServer([
new RegisteredResourceType($users, new InMemoryResourceRepository($users, $clock)),
new RegisteredResourceType($groups, new InMemoryResourceRepository($groups, $clock)),
]);
$request = Request::create(
'https://example.com/scim/v2/Users',
'POST',
server: ['CONTENT_TYPE' => 'application/scim+json'],
content: '{"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], "userName": "bjensen"}',
);
$response = $server->handle($request, '/scim/v2');
echo $response->getStatusCode(), "\n", $response->getContent(), "\n";
201
{"schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],"id":"<UUID v7>","meta":{"resourceType":"User","created":"<now>","lastModified":"<now>","version":"W/\"1\"","location":"https://example.com/scim/v2/Users/<UUID v7>"},"userName":"bjensen"}
InMemoryResourceRepository keeps the documents in a PHP array. Use it for tests and prototypes.
The second argument of handle() is the path prefix of the SCIM API, or "" at the root. The server removes it
from $request->getPathInfo(). The base URL of meta.location is the scheme, the host, $request->getBaseUrl()
and the prefix.
In a front controller without HttpKernel, call prepare(). It removes the body of a HEAD response:
$request = Request::createFromGlobals();
$server->handle($request, '/scim/v2')->prepare($request)->send();
handle() turns each ScimException into a SCIM error response. Other exceptions propagate.
PSR-15¶
Psr15\ScimRequestHandler is a PSR-15 request handler. It converts the PSR-7 request with
symfony/psr-http-message-bridge, calls ScimServer::handle() and prepare(), and converts the response back. A
HEAD response thus has no body in every host. The handler needs packages that the library only suggests:
The constructor takes the server, the path prefix of the SCIM API, and a bridge PsrHttpFactory with the four
PSR-17 factories of your PSR-7 implementation. Slim 4 with nyholm/psr7 and nyholm/psr7-server:
use Nyholm\Psr7\Factory\Psr17Factory;
use Slim\Factory\AppFactory;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
use TacticMedia\ScimServer\Psr15\ScimRequestHandler;
$psr17 = new Psr17Factory();
$app = AppFactory::create($psr17);
$app->any('/scim/v2[/{path:.*}]', new ScimRequestHandler($server, '/scim/v2', new PsrHttpFactory($psr17, $psr17, $psr17, $psr17)));
$app->run();
$serveris theScimServerof the quick start.- The handler does not authenticate. Add an authentication middleware to the route.
- Exceptions other than
ScimExceptiongo to the error middleware of the host, which answers without a SCIM body. - The base URL of
meta.locationcomes from theHostheader and the URI scheme of the PSR-7 request. Behind a proxy, make the host give the public values.
Implement a repository¶
A repository stores the documents of one resource type. A document is an array<string, mixed> in schema
spelling. schemas, id, externalId, meta and the core attributes are at the top level. The attributes of an
extension are below the URN of the extension:
[
'schemas' => ['urn:ietf:params:scim:schemas:core:2.0:User', 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'],
'id' => '01a1ac91-c4ab-7b1b-b72f-45f08a161b38',
'meta' => ['created' => '2026-11-05T10:20:30Z', 'lastModified' => '2026-11-05T10:20:30Z', 'version' => 'W/"1"'],
'userName' => 'bjensen',
'emails' => [['value' => 'bjensen@example.com', 'type' => 'work']],
'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => ['manager' => ['value' => '01a1ac92-54be-77a4-8bd1-24e885e32c9f']],
]
The server does this work before it calls the repository:
- It normalizes the body: schema spelling,
schemas, value types, single values wrapped in lists, oneprimary. - It rejects a body without a required value. It drops
readOnlyvalues from POST and PUT bodies. - It answers 400
mutabilityfor a change of a storedimmutablevalue, and for a PATCH that changes areadOnlyvalue. - It applies PATCH operations and gives
replace()the complete result. A PATCH that changes nothing calls no write. - It checks the single-valued attributes with
uniquenessserverorglobal. It callslist()with aneqfilter andcount2, and answers 409uniquenesson a match. - It projects responses, evaluates
If-MatchandIf-None-Match, and setsmeta.resourceType,meta.locationandmeta.version. - It fills an absent
$reffromvalue. The target is the registered resource type thattypenames, or else the only resource type in thereferenceTypesof$ref.
The repository does this work:
| Method | Contract |
|---|---|
get($id, $attributes, $excludedAttributes) |
Return the document, or null for an unknown id. |
list(ListQuery $query) |
Apply filter, then sortBy and sortOrder, then startIndex (from 1) and count. Return a ListResult with the page and the number of all matches. |
create($document) |
Set id, meta.created, meta.lastModified and, if you use it, meta.version. The document has no id and no meta. Return the stored document. |
replace($id, $document, $version) |
Store $document as the complete new state. Update meta.lastModified and, if you use it, meta.version. Throw ScimException::notFound() for an unknown id. When $version is not null, write only while the stored meta.version equals it, else throw ScimException::preconditionFailed(). |
delete($id, $version) |
Throw ScimException::notFound() for an unknown id. When $version is not null, delete only while the stored meta.version equals it, else throw ScimException::preconditionFailed(). |
Rules:
meta.versionis optional. Without it, the server makes a weak ETag from a hash of the document.- Every path in a
ListQueryhas the schema URN and the schema spelling, for exampleurn:ietf:params:scim:schemas:core:2.0:User:externalId. A comparison on a complex attribute targets itsvaluesub-attribute:emails co "x"arrives asemails.value co "x". - The filter is a tree of
Comparison,Present,LogicalAnd,LogicalOr,NotandValuePathFilter. Apply RFC 7644 section 3.4.2.2. Compare strings in any case whencaseExactis false. The uniqueness check needs this.FilterEvaluator::matches()evaluates a filter against one document, asInMemoryResourceRepositorydoes. - Sort as RFC 7644 section 3.4.2.3 says. Root queries with
sortBymerge the sorted pages of all types. - The server checks uniqueness before the write, without a lock. Make the storage reject duplicates too, and throw
ScimException::uniqueness()fromcreate()orreplace(). passwordarrives as the client sent it. Hash it before you store it. PUT and PATCH give the value fromget()back toreplace(). Thus do not return the hash fromget(), and keep the stored hash whenreplace()gets nopassword.- User
groupsisreadOnly. The server never changes it. Compute it from the group memberships if you need it.
Projection hints: get() gets the attributes and excludedAttributes of a GET of one resource, and on the delta
path of a PATCH the hints that Member deltas lists. ListQuery has them for lists and searches,
except for root queries with sortBy. Each is a list of resolved AttributePath. You can skip what the response
omits. The server projects the response itself. If you skip attributes, set meta.version, because a hash of a
partial document is not a stable ETag.
If-Match: the server compares it with the document from get(). For an If-Match with an entity tag, it then
passes the meta.version of that document as $version to replace() or delete(). Without If-Match, and for
If-Match: *, $version is null. A repository that writes only while the stored meta.version still equals
$version makes If-Match atomic. A repository that ignores $version leaves a race between the check and the
write: two concurrent requests with the same If-Match can both pass.
Member deltas¶
By default a PATCH reads the complete resource, applies the operations and calls replace() with the complete
result. For a group with many members, each member add or remove thus reads and writes every member. Microsoft
Entra ID sends one member per PATCH to a non-gallery application. Implement Repository\DeltaRepository to change large multi-valued attributes
by delta instead:
use TacticMedia\ScimServer\Path\AttributePath;
use TacticMedia\ScimServer\Repository\DeltaRepository;
use TacticMedia\ScimServer\Repository\PatchResult;
use TacticMedia\ScimServer\Repository\ResourcePatch;
final class GroupRepository implements DeltaRepository
{
public function deltaAttributes(): array
{
return [new AttributePath('urn:ietf:params:scim:schemas:core:2.0:Group', 'members')];
}
public function patch(string $id, ResourcePatch $patch): PatchResult
{
// One transaction: see the contract below.
}
// get(), list(), create(), replace() and delete() as for every repository.
}
Each attribute of deltaAttributes() must be multi-valued, complex, readWrite and not required, with a value
sub-attribute and without a primary sub-attribute. Else each PATCH of the type throws \LogicException. Return
[] to turn deltas off.
The server uses the delta path when each operation on these attributes is one of these forms:
| Operation | Step that the repository gets |
|---|---|
add with path members |
Add of the values |
remove with path members and a value list (Entra) |
Remove of the values |
remove with path members[value eq "<id>"] (Entra, Okta) |
Remove of [{value: <id>}] |
remove with path members and no value |
Replace with [] |
replace with path members (Okta) |
Replace with the values |
Each value has a string value. Any other operation on members sends the whole request to the full path. The
server then:
- Calls
get($id, [], <deltaAttributes>). Leave out these attributes, and returnmeta.version. - Checks
If-MatchandIf-None-Matchagainst that document. - Applies the operations on the other attributes to that document, with all checks, and checks uniqueness.
- Calls
patch($id, $patch), unless nothing changed and there is no step. - Answers 204 with the ETag of the returned document. With
attributesorexcludedAttributesit answers 200 and callsget()again with the projection.
Contract of patch():
| Item | Rule |
|---|---|
| Transaction | Write $patch->document and all steps in one transaction. Nothing changes when patch() throws. |
$patch->document |
The complete, normalized new state without the delta attributes, as replace() gets it. Store it when it is not null. Keep the stored delta attribute values. |
$patch->steps |
list<ValuesStep>, in request order. Add inserts the values that are not present. Remove deletes the values that are present. Replace makes the values equal to $step->values. Identify a value by its value. A type limits it to resources of that type. A value can repeat. |
$patch->version |
Not null: throw ScimException::preconditionFailed('Failed to update. Resource has changed on the server.') when the stored meta.version is different. |
| Errors | ScimException::notFound() for an unknown id. ScimException::invalidValue() for a value of an Add or Replace step that names no resource. |
meta |
Change meta.version and meta.lastModified when the document or a value changed. Keep them when nothing changed. |
| Result | new PatchResult($document, $changes): the stored document without the delta attributes and with meta.version, and one ValuesChange for each step that added or removed a value, with those values. Leave out steps without effect, so $changes is [] when no value changed. |
Events: a PATCH on the delta path dispatches Event\ResourcePatched (type, before, after, changes, patch)
instead of ResourceUpdated. before and after have no delta attributes. changes is the PatchResult list
without a ValuesChange that has no values. before, after and patch have no value that is never returned, as
Events describes. The server dispatches it when the document changed or a value was added or removed. PUT
and PATCH on the full path still dispatch ResourceUpdated. A listener that must see every change listens to both
events.
Differences from the full path:
- The repository compares
valuewith its own id rules. The full path compares exact strings. - Without
If-Match, two concurrent member changes do not remove the member of the other request.
Custom schemas and resource types¶
use TacticMedia\ScimServer\Schema\AttributeDefinition;
use TacticMedia\ScimServer\Schema\AttributeType;
use TacticMedia\ScimServer\Schema\CoreSchemas;
use TacticMedia\ScimServer\Schema\ResourceType;
use TacticMedia\ScimServer\Schema\Schema;
use TacticMedia\ScimServer\Schema\SchemaExtension;
use TacticMedia\ScimServer\Schema\Uniqueness;
$acmeUser = new Schema('urn:example:scim:schemas:extension:acme:2.0:User', 'AcmeUser', 'Acme user', [
new AttributeDefinition('badgeNumber', required: true, uniqueness: Uniqueness::Server),
new AttributeDefinition('clearanceLevel', AttributeType::Integer),
]);
$users = new ResourceType('User', '/Users', CoreSchemas::user(), [
new SchemaExtension(CoreSchemas::enterpriseUser()),
new SchemaExtension($acmeUser, required: true),
], 'User Account');
$devices = new ResourceType('Device', '/Devices', new Schema('urn:example:scim:schemas:core:2.0:Device', 'Device', 'Device', [
new AttributeDefinition('serialNumber', required: true, caseExact: true, uniqueness: Uniqueness::Server),
new AttributeDefinition('owner', AttributeType::Complex, subAttributes: [
new AttributeDefinition('value', required: true, caseExact: true),
new AttributeDefinition('$ref', AttributeType::Reference, referenceTypes: ['User']),
]),
]));
AttributeDefinitiondefaults:string, single-valued, not required,caseExactfalse,readWrite,returneddefault,uniquenessnone. AwriteOnlyattribute is alwaysreturnednever.- Register each type with
new RegisteredResourceType($type, $repository)./ResourceTypesand/Schemasshow them./Schemasalso shows the ServiceProviderConfig, ResourceType and Schema schemas (RFC 7643 section 8.7.2). - Endpoint names match in any case. A fixed name such as
SchemasorBulkwins over a resource type endpoint. - The server fills
owner.$refabove, becausereferenceTypesnames one resource type.
Configuration¶
ScimServer takes a ServiceProviderConfig as its second argument:
use TacticMedia\ScimServer\Server\AuthenticationScheme;
use TacticMedia\ScimServer\Server\ServiceProviderConfig;
$config = new ServiceProviderConfig(
documentationUri: 'https://example.com/docs/scim',
maxResults: 200,
authenticationSchemes: [
new AuthenticationScheme('oauthbearertoken', 'OAuth Bearer Token', 'Send an access token in the Authorization header.', primary: true),
],
bulkMaxOperations: 100,
);
| Parameter | Default | Effect |
|---|---|---|
documentationUri |
null |
Shown in /ServiceProviderConfig when set. |
maxResults |
100 |
The largest page of a list or search, and the count when the client sends none. |
changePasswordSupported |
false |
Shown in /ServiceProviderConfig only. |
authenticationSchemes |
[] |
Shown in /ServiceProviderConfig only. The server does not authenticate. |
bulkSupported |
true |
false makes POST /Bulk answer 501. |
bulkMaxOperations |
1000 |
More operations in one request: 413. |
bulkMaxPayloadSizeBytes |
1048576 |
A larger Content-Length header or body: 413. |
/ServiceProviderConfig always shows patch, filter, sort and etag as supported.
Cursor pagination¶
The server supports RFC 9865 cursors when you give it a secret key. Without a key, it ignores cursor, and
/ServiceProviderConfig shows "pagination": {"cursor": false, ...}. The PHP extension sodium is required.
$server = new ScimServer(
$resourceTypes,
new ServiceProviderConfig(maxResults: 200, cursorTimeoutSeconds: 3600),
cursorKey: $cursorKey,
clock: $clock,
);
| Argument | Default | Effect |
|---|---|---|
cursorKey of ScimServer |
null |
The secret that encrypts the cursors, for example 32 random bytes from your secret store. Use the same key in each process of the API. A new key makes all open cursors invalid. |
clock of ScimServer |
null |
A PSR-20 clock for the cursor lifetime. null uses the system time. |
cursorTimeoutSeconds of ServiceProviderConfig |
null |
The lifetime of a cursor. null: cursors do not expire. |
Requests:
- Index pagination stays the default.
GET /Users?cursor&count=100, or"cursor": ""in a SearchRequest, asks for the first cursor page. For the next page, send thenextCursorof the response ascursor, with the same endpoint,filter,sortByandsortOrder.count,attributesandexcludedAttributescan change. - An empty
cursorwith astartIndexuses index pagination. A non-emptycursorignoresstartIndex. - A cursor response has
nextCursoron each page except the last, and nostartIndex.count=0returnstotalResultsand nonextCursor. The server does not sendpreviousCursor. - GET and POST
.searchaccept the cursors of each other.
| Error (400) | Cause |
|---|---|
invalidCursor |
The cursor is not from this server and key, or it belongs to another endpoint, filter, sortBy or sortOrder. The detail is always The cursor is not valid for this query. |
expiredCursor |
The cursor is older than cursorTimeoutSeconds. |
invalidCount |
count is not an integer. |
A cursor contains the offset, the position, a hash of the query and the time of issue, encrypted and authenticated with XSalsa20-Poly1305. Clients cannot read or change it, and the server keeps no state. Each page runs the query again, so your authorization and the current data apply to each page.
The position comes from the repository:
- A repository that implements only
ResourceRepository: the cursor holds an offset, and the server callslist(). A create or delete before the position moves the page window, as withstartIndex. - A
CursorPaginatedRepositorypages by position (keyset).listAfter(ListQuery $query, ?array $after)gets the query withstartIndex1 and$afternull for the first page. It returns aCursorPage($resources, $totalResults, $next).$nextis a list of JSON scalar values, for example the sort value and a unique id of the last resource, ornullon the last page.$totalResultscan benullwhen it is unknown; the response then omits it. ReturnnullfromlistAfter()to page this query by offset. ThrowScimException::invalidCursor()for a position that you cannot use.InMemoryResourceRepositoryimplements it. When a position would make the cursor longer than 4096 characters, for example with a long sort value, the cursor holds only the offset, and the rest of the traversal pages by offset. - Queries across resource types (
/and/.search) always use an offset, because the server merges their pages itself.
Events¶
Give a PSR-14 event dispatcher, for example symfony/event-dispatcher, as the third argument of ScimServer.
| Event | Properties | Dispatched after |
|---|---|---|
Event\ResourceCreated |
type, resource |
create() |
Event\ResourceUpdated |
type, before, after, patch (null for PUT) |
replace(), for PUT and for a PATCH that changes the resource |
Event\ResourceDeleted |
type, resource (the document before the delete) |
delete() |
resource, before and after are the repository documents without the values that are never returned, such as
password. They have no meta.resourceType and no meta.location. patch is the PATCH request without these
values: an operation whose path names such an attribute has the value null, a value object loses the keys of such
attributes, and a complex value loses such sub-attributes. The server dispatches no event when the repository call
throws. A ScimException from a listener becomes the error response, but the write is already done.
/Me¶
ScimServer takes a Server\MeResolver as its me argument. Without one, /Me answers 501.
use Symfony\Component\HttpFoundation\Request;
use TacticMedia\ScimServer\Server\MeResolver;
use TacticMedia\ScimServer\Server\MeSubject;
final class UserNameMeResolver implements MeResolver
{
public function resolve(Request $request): ?MeSubject
{
// Set by your authentication.
$userName = $request->attributes->get('user_name');
return \is_string($userName) ? new MeSubject('User', $userName, 'userName') : null;
}
}
$server = new ScimServer($resourceTypes, me: new UserNameMeResolver());
new MeSubject($resourceType, $value, $attribute = 'id')names the resource whose$attributeequals$value. Forid, the server makes no query. For another attribute, it callslist()with aneqfilter, so the attribute must be filterable. A resource type that is not registered, or an attribute that it cannot filter, throws\LogicException.nullor no match answers 404. More than one match answers 409. AScimExceptionfromresolve()becomes the response, for example 403.- GET, HEAD, PUT, PATCH and DELETE act on the resource as on
/Users/{id}. Each response after the lookup hasLocationset to the URL of the resource. POST answers 501. - A write through
/Mehas the rights of a/Users/{id}request: an end user can changeactive,userNameorroles, or delete the resource. Thus allow only GET and HEAD on/Mefor end users.
Endpoints and features¶
| Path | Methods |
|---|---|
/ |
GET, HEAD: query across all resource types |
/.search |
POST: search across all resource types |
/{endpoint} |
GET, HEAD, POST |
/{endpoint}/.search |
POST |
/{endpoint}/{id} |
GET, HEAD, PUT, PATCH, DELETE |
/ServiceProviderConfig |
GET, HEAD |
/ResourceTypes, /ResourceTypes/{name} |
GET, HEAD |
/Schemas, /Schemas/{urn} |
GET, HEAD |
/Bulk |
POST |
/Me |
GET, HEAD, PUT, PATCH, DELETE with a MeResolver, else 501. POST: 501. See /Me. |
An unknown path answers 404. A known path with another method answers 405 with an Allow header.
- Filters: the grammar of RFC 7644 Figure 1 with errata 4670, 4690, 7319 and 7322. Keywords and operators match in
any case. Value paths such as
emails[type eq "work" and value co "@example.com"]work. caseExactfalse compares with Unicode case folding.- Lists:
sortBy,sortOrder,startIndexandcount. An empty result answers 200 with"Resources": []. - Root queries: in a query across resource types, an attribute that a type does not define has no value for that type.
- PATCH:
add,removeandreplace, with or withoutpath. Paths can have a value filter and a sub-attribute, for exampleemails[type eq "work"].value. All operations of one request apply, or none. PATCH answers 204. WithattributesorexcludedAttributesin the query, it answers 200 with the resource. - Bulk:
bulkIdreferences indataat any depth, also to a later POST.failOnErrorsstops after the given number of failed operations. - Bulk resolves circular
bulkIdreferences (RFC 7644 section 3.7.1). It takes the first POST of the cycle, in request order, that has no such reference in a required attribute. It creates that resource without the references, then adds them with a PATCH. When each POST of the cycle has its reference in a required attribute, the first POST fails with 409, and the POSTs that reference it fail with 409 too. - ETags: each returned resource has
meta.version, and single-resource responses have anETagheader. GET, PUT, PATCH and DELETE of a resource evaluateIf-MatchandIf-None-Matchand can answer 304 or 412. - HEAD works wherever GET works.
- Discovery endpoints answer a
filterwith 403 and ignore the other query parameters. - Responses are
application/scim+json, orapplication/jsonwhenAcceptnamesapplication/jsonand notapplication/scim+json. Request bodies can beapplication/scim+json,application/jsonor have noContent-Type.
Interoperability¶
The monorepo tests replay request sequences of Microsoft Entra ID, Okta, Keycloak, PingOne, OneLogin and JumpCloud. The server accepts these quirks:
opin any case, and"True"or"False"as booleans.- PATCH without
pathwhose keys are paths, for examplename.givenNameor a URN-qualified name. - A PATCH
paththat is an extension URN. - A value filter path that matches no value. The server creates the value from the
eqterms of the filter. manageras an id string, an object or a list with one object.""andnullclear it.removewith avaluelist removes only those values.addof a value that is present changes nothing.- PUT built from GET output. The stored
passwordstays. members[].display. The server drops it, because it isreadOnly.- A base URL with a trailing slash (
//Users), and unknown query parameters.
The server does not accept unquoted filter values (userName eq bjensen). It answers 400 invalidFilter, because
RFC 7644 Figure 1 requires a JSON value.
PATCH replace of a single-valued complex attribute, for example name, merges the sub-attributes, as RFC 7644
section 3.5.2.3 requires. A client that expects a full replacement finds the omitted sub-attributes unchanged.
Limits and deviations from RFC 7643 and RFC 7644¶
manager.$refis optional, because Microsoft Entra ID sends only the manager id. Erratum 8359 makes it required.- Group
members.displayisreadOnly(erratum 6011). Groupmembers.valueiscaseExact, because ids are case exact. - Group
displayNamehasuniquenessserver, unlike RFC 7643 Figure 9 (none), because Microsoft Entra ID expects 409 for a duplicate group name. A duplicate name answers 409uniqueness. To accept duplicate names, register a Group schema whosedisplayNamehasUniqueness::None. - The server checks
uniquenessglobalonly in the repository of the same resource type. - A
filterorsortByon an attribute that is never returned, for examplepassword, answers 400. A match or an order could reveal the value. - A filter longer than 8192 bytes, or with more than 64 nested levels of
(and[, answers 400invalidFilter. schemasis not an attribute in filters.- Cursor pagination (RFC 9865) needs
cursorKey. The server does not sendpreviousCursor. An offset cursor moves when a resource before its position is created or deleted, asstartIndexdoes. - A root query with
sortByloadsstartIndex - 1 + countresources from each resource type. If-Matchuses the weak comparison of ETags.
Contributing¶
This repository is a read-only split of the tacticmedia/scim monorepo,
directory scim-server/. Send issues and pull requests to the monorepo.
License¶
MIT. See LICENSE.