SCIM users, groups, and schemas
A common shape for users
Harbor's identity system sends changes to more than one application's API. Without a shared format, each of those connections speaks its own dialect. One application calls a surname last_name and another calls it surname. One disables a user by setting status to 2, another by deleting the user, and a third with a field called enabled. Someone has to write a connector that translates Harbor's view of a person into each dialect, test it, and repair it whenever the application's API changes.
SCIM, the System for Cross-domain Identity Management introduced in Trust across systems, replaces that variety with one agreed shape for users and groups and one set of requests for changing them. It comes in two parts. RFC 7643 defines the schema: what a user or a group looks like. RFC 7644 defines the protocol: the HTTP requests that create, read, change, and remove them. When an application supports both, the provisioning service can talk to it the same way it talks to every other SCIM application, without a custom translation layer.
SCIM has its own names for the participants. The patient records system is the service provider: it holds the accounts and answers requests. Federation uses the same term for an application that relies on a sign-in, but in SCIM it means the application that stores the accounts. The provisioning service is the client. Each thing the service provider holds, such as one user or one group, is a resource, and a schema defines the attributes a kind of resource can have. Schemas are named with URNs, long names designed to be unique everywhere, such as urn:ietf:params:scim:schemas:core:2.0:User for the core user schema. SCIM messages are JSON, sent with the media type application/scim+json.
Reading a user
On the morning of Monday 2 November, a few hours after the identity system enabled Dana Okafor's patient records account, the provisioning service reads it back:
GET /scim/v2/Users/a3f1c9e2-58d4-4b7e-9c61-2e7d0b4f8a15 HTTP/1.1
Host: records.harborclinic.example
Authorization: Bearer demo-provisioning-token-7
Accept: application/scim+json
The records system returns the user:
HTTP/1.1 200 OK
Content-Type: application/scim+json
ETag: W/"2"
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User",
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
],
"id": "a3f1c9e2-58d4-4b7e-9c61-2e7d0b4f8a15",
"externalId": "E10482",
"userName": "dana.okafor",
"name": {
"formatted": "Dana Okafor",
"givenName": "Dana",
"familyName": "Okafor"
},
"displayName": "Dana Okafor",
"emails": [
{
"value": "[email protected]",
"type": "work",
"primary": true
}
],
"active": true,
"groups": [
{
"value": "e2a6b8d4-7c1f-4f39-a5e0-9b3d2c8f6a71",
"$ref": "https://records.harborclinic.example/scim/v2/Groups/e2a6b8d4-7c1f-4f39-a5e0-9b3d2c8f6a71",
"display": "Pediatrics nurses"
}
],
"meta": {
"resourceType": "User",
"created": "2026-10-26T09:14:05Z",
"lastModified": "2026-11-02T00:05:12Z",
"location": "https://records.harborclinic.example/scim/v2/Users/a3f1c9e2-58d4-4b7e-9c61-2e7d0b4f8a15",
"version": "W/\"2\""
}
}
All names, hosts, identifiers, and tokens in these examples are fictional.
The schemas list names every schema this user draws on. The second one adds employment details, and its attributes are left out of this copy until the section on extensions.
Three attributes identify the user, and each has a different job.
id belongs to the records system. The service provider assigns it when the user is created, and no client can set or change it. It is returned in every response, stays the same for the life of the account, and is never reused for anyone else. The user's address ends with it. This is the value the identity system keeps for addressing Dana's account later: every update goes to /Users/ followed by the id.
externalId belongs to the client. The identity system set it to E10482, Dana's HR employee number, when it created the account. The records system stores it and returns it without interpreting it. With it, the identity system can find the account by its own key if it ever loses the id, or is unsure whether a create succeeded.
userName is the account's name in the records system. Every user must have one, and no two users in the same service can share it. Unlike id, it can change. When Dana later changes surname to Mensah, the user name becomes dana.mensah while the id stays exactly as it was. A client that addresses accounts by user name will eventually update the wrong account, or none at all, the same problem What a directory holds described for directory entries.
The remaining attributes describe Dana. name is a complex attribute, made of sub-attributes: givenName, familyName, and formatted, the full name ready to display. displayName is the name the application shows on screen. emails is multi-valued: a list in which each entry has a value, a type such as work or home, and primary to mark the address to use, which at most one entry may set. Dana has only a work address. A second address would join the list rather than replace the first, which is why a change to one email has to say which entry it means.
active is the account's administrative status: true while it is in use and false when it is disabled. SCIM leaves the exact effect of false to each application, so Harbor confirmed what it does in the records system: it blocks sign-in and ends sessions that are already open.
Finally, meta holds information the service maintains: the resourceType, when the user was created and lastModified, its full location, and its version. The version is an entity tag (ETag), the same value as the ETag header on the response, and it changes whenever the user changes. The W/ prefix marks a weak tag, which is enough for telling versions apart. The value is opaque: clients compare it, but should not try to read meaning into it. SCIM requests and responses uses it to avoid overwriting a change the client has not seen.
Groups and members
Dana's user listed one group, Pediatrics nurses. In the records system, membership in that group grants EHR_PED_RW, permission to read and update pediatric patient records. Here is the group, trimmed to two of its members:
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
"id": "e2a6b8d4-7c1f-4f39-a5e0-9b3d2c8f6a71",
"displayName": "Pediatrics nurses",
"members": [
{
"value": "a3f1c9e2-58d4-4b7e-9c61-2e7d0b4f8a15",
"$ref": "https://records.harborclinic.example/scim/v2/Users/a3f1c9e2-58d4-4b7e-9c61-2e7d0b4f8a15",
"display": "Dana Okafor"
},
{
"value": "5b9e2d70-1c3f-4a86-b2d9-7e4f0c6a1d38",
"$ref": "https://records.harborclinic.example/scim/v2/Users/5b9e2d70-1c3f-4a86-b2d9-7e4f0c6a1d38",
"display": "Sam Reyes"
}
],
"meta": {
"resourceType": "Group",
"created": "2024-03-04T10:22:40Z",
"lastModified": "2026-10-26T09:14:07Z",
"location": "https://records.harborclinic.example/scim/v2/Groups/e2a6b8d4-7c1f-4f39-a5e0-9b3d2c8f6a71",
"version": "W/\"57\""
}
}
A group has a displayName and a multi-valued members attribute. Each member's value is that member's id in the records system, not an HR number. $ref is the member's full address, and display is a readable name. A member can also be another group where the service supports groups inside groups, though many services do not.
Sam Reyes is still listed, because Sam's move to oncology on 9 November has not happened yet. The next lesson follows the requests that move Sam between groups.
Membership is recorded on the group. The groups attribute on Dana's user is a read-only reflection that the service fills in from the groups that list Dana, and on some services from groups Dana belongs to through nesting or a rule. A client cannot add Dana to Oncology nurses by editing Dana's user. It changes the group instead.
Groups can be large. A group for every nurse at the clinic could list well over a hundred members, and sending them all back on every read is slow. A client that needs only the group's name can ask for less, by adding attributes=displayName or excludedAttributes=members to the query string.
Extensions
The core user schema covers what almost every application needs. Employers usually need more, and RFC 7643 defines one extension for them: the enterprise user extension, urn:ietf:params:scim:schemas:extension:enterprise:2.0:User. It adds employeeNumber, costCenter, organization, division, department, and manager. This is the part of Dana's user that was left out earlier:
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {
"employeeNumber": "E10482",
"department": "Pediatrics",
"manager": {
"value": "7c2e9a41-d3b8-4f60-a1e7-5b0d8c3f9e12",
"$ref": "https://records.harborclinic.example/scim/v2/Users/7c2e9a41-d3b8-4f60-a1e7-5b0d8c3f9e12"
}
}
Extension attributes sit in their own object, named by the extension's URN, and the same URN appears in schemas. Keeping them apart means an extension's attribute names can never collide with core attributes or with another extension's.
manager is a complex attribute. Its value is the manager's id in the records system, which is why Harbor's mapping looks the manager up instead of sending an HR number, and $ref is the manager's address. A third sub-attribute, displayName, holds the manager's name. It is read-only, so clients never send it, and services that support it fill it in themselves.
employeeNumber repeats E10482, which can look redundant next to externalId. The two have different jobs. externalId is the client's matching key and exists on every kind of resource. employeeNumber is a business attribute that the application can display or use. For employees, Harbor sends the same value to both. Dr. Lee Moreau, a contractor, has the register ID C2291 as externalId and no employee number at all.
Services can also define their own extensions. Harbor's records system needs to know at which of the clinic's three sites each person works, which neither schema covers, so it accepts an extension named urn:example:harborclinic:scim:schemas:extension:site:1.0:User with a site attribute. A client uses the URN exactly as the service publishes it, and the service describes the extension's attributes in the same way it describes the standard ones.
Asking what a service supports
Before sending its first change, the provisioning service asked the records system what it supports. SCIM defines three discovery endpoints for that, each under the same base address as /Users. The first, /ServiceProviderConfig, lists the optional features the service offers:
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
"patch": { "supported": true },
"bulk": { "supported": false, "maxOperations": 0, "maxPayloadSize": 0 },
"filter": { "supported": true, "maxResults": 200 },
"changePassword": { "supported": false },
"sort": { "supported": false },
"etag": { "supported": true },
"authenticationSchemes": [
{
"type": "oauthbearertoken",
"name": "OAuth bearer token",
"description": "Access tokens issued by login.harborclinic.example"
}
]
}
Each entry shapes how the client behaves. patch is supported, so the client can send small changes instead of whole users. bulk is not, so each change travels as its own request. filter is supported with at most 200 results in a response, which sets the largest page the client can ask for. changePassword is off, so the records system will not accept passwords through SCIM, which suits Harbor because staff sign in through single sign-on. sort is off. etag is on, so the client can use versions to avoid overwriting changes. authenticationSchemes says the API expects OAuth bearer tokens.
/ResourceTypes lists the kinds of resources the service holds, each with its endpoint, its core schema, and its extensions. The records system's user type, from /ResourceTypes/User, looks like this:
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:ResourceType"],
"id": "User",
"name": "User",
"endpoint": "/Users",
"schema": "urn:ietf:params:scim:schemas:core:2.0:User",
"schemaExtensions": [
{
"schema": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User",
"required": false
},
{
"schema": "urn:example:harborclinic:scim:schemas:extension:site:1.0:User",
"required": false
}
]
}
/Schemas describes every attribute of every schema in detail. Here is its entry for the password attribute of the core user schema:
{
"name": "password",
"type": "string",
"multiValued": false,
"description": "The user's password. Can be set, never read.",
"required": false,
"caseExact": false,
"mutability": "writeOnly",
"returned": "never",
"uniqueness": "none"
}
Each attribute carries characteristics that tell a client how to treat it. mutability says who can change the attribute, and returned says when it appears in responses:
| Characteristic and value | Meaning | Example |
|---|---|---|
mutability: readOnly | Set by the service. Clients cannot change it. | id, meta, a user's groups |
mutability: readWrite | Clients can set it and change it. | displayName, active |
mutability: immutable | Can be set once, when the resource or the value it belongs to is created, and never changed afterwards. | The value inside one entry of a group's members: entries can be added and removed, but not edited |
mutability: writeOnly | Can be set, never read back. | password |
returned: always | In every response. | id |
returned: never | In no response. | password |
returned: default | In responses unless the client excludes it. | emails, members |
returned: request | Only when the client asks for it by name. | Large or sensitive attributes, at the service's choice |
Together they explain the password entry. writeOnly means a client may set a password. returned: never means no response will ever include it, even when a client asks for it by name. When the provisioning service reads Dana's user, the response has no password attribute at all, not even a scrambled one.
The same descriptions mark userName as required, with uniqueness set to server, so the records system refuses a second user with a name that is already taken. Continue to SCIM requests and responses to create, find, change, and remove users and groups, and to read what the service says back.