Skip to main content

Overview

PATCH lets an identity provider update a SCIM resource without sending the full representation. Instead of replacing the entire user object, you send a list of targeted operations. theAuth implements the RFC 7644 §3.5.2 PATCH protocol at PATCH /scim/v2/Users/{id}. The endpoint deserializes the current database row into a SCIM view, applies your operations against that view using the path engine in scim-patch.ts, and then maps the mutated fields back to database columns.

Operation types

Every PATCH request body must include the patch schema and an Operations array with at least one entry.

add

Adds a value. On a multi-valued attribute (like emails), it appends rather than replaces. On a scalar, it sets the value. When path is omitted, the value object is merged into the resource root.

replace

Overwrites the targeted attribute. On a multi-valued attribute targeted by a value filter, it updates only the matching elements. Without a path, it merges the value object into the root (same merge behavior as add).

remove

Removes the targeted attribute or, with a value filter, drops matching array elements. remove without a path is always rejected with noTarget.

Path expressions

Paths follow the RFC 7644 §3.5.2 grammar:
In plain English: a base attribute name, optionally filtered by a bracketed expression that selects elements of a multi-valued attribute, optionally followed by a dot-separated sub-attribute.

Simple attribute

Targets a top-level attribute directly.

Nested attribute

A dot separates the base attribute from a sub-attribute. The engine creates intermediate objects if they do not exist.

Multi-valued attribute

Targeting the base attribute without a filter operates on the whole array.

Value-filter selector

Brackets contain a filter expression that selects elements of the array. The filter uses SCIM attribute operators: eq, ne, co, sw, ew, pr, gt, lt, ge, le, and logical operators and/or/not.
The filter expression is parsed into an AST. No regex matching runs against user input.

Value-filter with sub-attribute

Append .attrName after the closing bracket to target a specific field on each matching element.
The value filter must attach to a top-level attribute. A path like name.sub[filter] is rejected with invalidPath.

URN-prefixed attribute

Extension schema attributes use the full URN as a namespace prefix. The engine splits on the last : to extract the schema URN and the attribute name, then writes the value under the URN key in the resource object.
Parsed as:
  • schemaUrn: urn:ietf:params:scim:schemas:extension:enterprise:2.0:User
  • base: department

Add semantics

When op is add and the target is a multi-valued attribute (an existing array), the engine appends rather than replaces. This is intentional per RFC 7644 §3.5.2.2.
After this operation, the original work and home emails are still present. The new entry is appended. Contrast with replace, which overwrites the array:
This leaves only the one entry. No-path add has the same append behavior for arrays nested inside the value object:

Remove semantics

remove with a plain path sets the attribute to undefined:
remove with a value filter drops all matching array elements and keeps the rest:
If the filter matches no elements, the operation is a no-op. The array is not modified. remove without any path is rejected:

No-path operations

add and replace accept operations without a path. The value must be an object. Each key in the object is merged into the resource root.
This is equivalent to two separate replace operations with explicit paths. Immutable checks still apply: if the value object contains id, schemas, or meta, the entire operation is rejected before any write happens. remove without a path is always rejected regardless of what value contains.

Immutable attributes

The following paths cannot be modified by any PATCH operation. They are server-controlled per RFC 7643 §7. Attempting to PATCH any of these returns:
The check runs before any write. If the first operation in a batch is valid and the second targets id, the first write does not persist.

Caller-defined readonly paths

The handlePatchUser handler passes an extra readonlyPaths option to the engine:
This means externalId cannot be changed via PATCH after the resource is created, even though it is not in the base immutable list. Paths in readonlyPaths are lowercased before comparison, so externalId, externalid, and ExternalID all match. If you build a custom SCIM handler, you can pass any additional paths you want to lock:

DoS guardrails

The engine caps the number of operations per request. The default is 1000. If the Operations array exceeds the cap, the engine throws before processing any operation.
This matters for public-facing deployments. A client that generates one PATCH operation per attribute per user can construct a single request containing thousands of operations. Without a cap, that becomes an amplification vector: one HTTP request triggers unbounded in-memory work. You can lower the cap per-handler by passing maxOperations:

Error reference


Worked examples

1. Deactivate a user (Okta-style)

Okta sends a single replace on active to suspend a user.
Note: op values are case-insensitive. Replace and replace are equivalent.

2. Update display name and email in one request (Okta-style)

3. No-path merge (Azure AD-style)

Azure AD often sends replace without a path, merging a partial object.

4. Add a phone number (Azure AD-style)

If phoneNumbers already exists, this appends the new entry.

5. Set enterprise extension attribute (Google Workspace-style)

Google Workspace sends URN-prefixed paths for extension attributes.

6. Remove the home email

7. Replace nested name fields

8. Add multiple emails at once


Current limitations

Groups PATCH is partial. PATCH /scim/v2/Groups/{id} routes through a separate handler (handlePatchGroup) that does not use the scim-patch.ts engine. Value-filter operations on group members are not supported. A full rewrite to use the shared engine is planned for a follow-up release. Value-filter member operations. Filtering on members[value eq "user-id"] in a group context is not yet evaluated against the engine’s AST walker. Sending such a path against the groups endpoint will not produce a predictable error — it will silently fail or return unexpected results. Filter operators. The current AST walker in scim-filter.ts supports the comparison operators (eq, ne, co, sw, ew, pr, gt, lt, ge, le) and logical operators (and, or, not). Complex nested logical expressions with more than two clauses may not parse correctly.

Security posture

The path parser and value-filter evaluator are pure AST walkers. No user-provided string is ever passed to eval, new Function, or a dynamic property accessor outside a whitelisted attribute list. Immutable checks happen before any write begins. If an operation batch is partially valid and the first invalid operation is the third in the list, no writes from operations one or two persist. The operation count cap prevents request-amplification attacks. Without it, a single PATCH request could encode thousands of operations, each triggering DB reads and writes. Audit logs record the final resource state after the patch is applied, not the raw operation strings. This prevents log injection through maliciously crafted path expressions or filter values.
Last modified on October 7, 2026