Skip to content

Add SCIM 2.0 support to enterprise IdP plugin#20

Open
Tbsheff wants to merge 17 commits intovercel-labs:mainfrom
Tbsheff:feature/idp-scim
Open

Add SCIM 2.0 support to enterprise IdP plugin#20
Tbsheff wants to merge 17 commits intovercel-labs:mainfrom
Tbsheff:feature/idp-scim

Conversation

@Tbsheff
Copy link
Copy Markdown

@Tbsheff Tbsheff commented Mar 24, 2026

Summary

Add full SCIM 2.0 support to the idp plugin (from PR #9) — both as a provider (expose /scim/v2/* API) and a client (push provisioning to configured targets). Supports Users, Groups, Enterprise User extension, full RFC 7644 filter spec, and PATCH operations. Zero new npm dependencies.

Note: This PR builds on #9 (enterprise IdP plugin with OIDC + SAML). It can be merged after #9 or squashed together.

SCIM Provider Endpoints (16)

Endpoint Description
GET /scim/v2/ServiceProviderConfig Capabilities discovery
GET /scim/v2/Schemas Schema definitions
GET /scim/v2/ResourceTypes Resource type list
GET /scim/v2/Users List/search users (full filter + pagination)
GET /scim/v2/Users/:id Get user
POST /scim/v2/Users Create user
PUT /scim/v2/Users/:id Replace user
PATCH /scim/v2/Users/:id Partial update (RFC 7644 PatchOp)
DELETE /scim/v2/Users/:id Delete user
GET /scim/v2/Groups List/search groups (with filter)
GET /scim/v2/Groups/:id Get group
POST /scim/v2/Groups Create group (with members)
PUT /scim/v2/Groups/:id Replace group (with membership sync)
PATCH /scim/v2/Groups/:id Partial update with membership sync
DELETE /scim/v2/Groups/:id Delete group (removes from user.groups[])

Key Features

RFC 7644 Filter Parser

Full recursive descent parser — zero dependencies:

  • All operators: eq, ne, co, sw, ew, gt, ge, lt, le, pr
  • Logical: and, or, not with nested parentheses
  • Value path filters: emails[type eq "work"].value eq "x"
  • URN-prefixed paths for enterprise extension attributes
  • Case-insensitive string comparison per SCIM spec
  • Recursion depth limit (50) for DoS prevention
  • Type-aware numeric comparison for ordering operators

PATCH Operations (RFC 7644 §3.5.2)

  • add / replace / remove with dot-path and URN-path resolution
  • Array element removal with value filters
  • Auto-vivification of intermediate objects for add
  • Single value append to existing arrays

Enterprise User Extension

urn:ietf:params:scim:schemas:extension:enterprise:2.0:User maps to IdpUser.attributes:

  • department, employeeNumber, manager

SCIM Client (Push Provisioning)

  • ScimClient class pushes create/update/patch/delete to configured targets
  • Fire-and-forget with debug logging
  • Configurable via scim.clients[] in seed config

Authentication & Security

  • Bearer token middleware with timingSafeEqual constant-time comparison
  • application/scim+json content type on all responses (including errors)
  • ID validation prevents NaN-based 404 masking
  • JSON body parsing wrapped in try-catch with SCIM error format
  • userName uniqueness enforced (409 with uniqueness scimType)

Group Membership Sync

  • PATCH add/remove members updates affected users' groups[] arrays
  • PUT with members does full membership replacement
  • Group rename propagates to all user group references
  • Group delete removes group from all users

Usage

idp:
  scim:
    bearer_token: my-scim-token
    clients:
      - target_url: http://localhost:1769/api/auth/scim/v2
        bearer_token: target-token
# List users
curl -H "Authorization: Bearer my-scim-token" http://localhost:4003/scim/v2/Users

# Create user
curl -X POST -H "Authorization: Bearer my-scim-token" \
  -H "Content-Type: application/scim+json" \
  -d '{"schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],"userName":"alice@example.com"}' \
  http://localhost:4003/scim/v2/Users

# Filter
curl -H "Authorization: Bearer my-scim-token" \
  "http://localhost:4003/scim/v2/Users?filter=userName%20eq%20%22alice%40example.com%22"

Architecture

Zero new npm dependencies beyond what PR #9 added. All SCIM code is pure TypeScript:

src/scim/
  constants.ts       — Schema URNs, ServiceProviderConfig, ResourceTypes
  types.ts           — ScimUser, ScimGroup, ListResponse, Error, PatchOp interfaces
  filter-parser.ts   — Recursive descent RFC 7644 filter parser (~300 lines)
  patch-handler.ts   — PATCH operation interpreter with path resolution
  schema-mapper.ts   — Bidirectional IdpUser ↔ ScimUser, IdpGroup ↔ ScimGroup
  response.ts        — ListResponse, Error builders, 1-based pagination
  auth.ts            — Bearer token middleware (constant-time)
  client.ts          — Push provisioning client
src/routes/
  scim.ts            — All 16 route handlers with debug logging

Testing

67 SCIM-specific tests (TDD) across 5 test files:

  • Filter parser: 22 tests — all operators, logical combinators, value paths, URN paths, edge cases
  • PATCH handler: 11 tests — add/replace/remove, arrays, URN paths, errors
  • Schema mapper: 14 tests — round-trips, enterprise extension, manager, group membership
  • Route integration: 28 tests — full CRUD lifecycle, filtering, pagination, auth, membership sync, error formats
  • Client: 8 tests — HTTP methods, headers, error handling, URL normalization

164 total IDP tests passing. All existing OIDC + SAML tests unaffected.

Review History

  • 3 rounds of deep code review (Carmack-level)
  • Ship verdict after final review: no CRITICAL or MAJOR bugs
  • 3 pattern-continuity reviews against existing codebase conventions
  • All findings addressed

Tbsheff added 17 commits March 23, 2026 20:04
New first-class `idp` service plugin providing a local enterprise identity
provider for end-to-end testing of OIDC and SAML SSO flows.

OIDC Provider:
- OpenID Connect discovery, JWKS, authorize, token, userinfo, revoke, logout
- RS256 JWT signing with auto-generated or seeded RSA keys
- Authorization code flow with PKCE (S256/plain)
- Refresh token support with rotation (offline_access scope)
- Custom claim mappings via dot-path resolution
- client_secret_basic and client_secret_post authentication
- Browser sign-in picker matching existing emulate UX

SAML 2.0 IdP:
- IdP metadata endpoint with X.509 signing certificate
- SP-initiated SSO with HTTP-Redirect binding
- Signed SAML assertions (RSA-SHA256) via xml-crypto
- Auto-submit POST form to Assertion Consumer Service
- Microsoft Entra ID claim URI presets (nameidentifier, emailaddress, etc.)
- Configurable attribute_mappings and name_id_format per service provider
- RelayState roundtrip support

Shared:
- Strict mode: enforces client/SP validation, requires PKCE, disables debug
- Seeded users with groups, roles, and custom attributes
- Debug endpoint for inspecting sessions, codes, and tokens
- CLI integration: start, list, init, programmatic API
- Zero-config boot with sensible defaults

Dependencies:
- xml-crypto ^6 (XML signing with Exclusive C14N)
- @peculiar/x509 ^2 (self-signed X.509 certificate generation)

Testing:
- 67 plugin tests (TDD: crypto, seed, OIDC integration, SAML integration)
- All 160+ monorepo tests passing
- Fix require() in ESM module — use static import
- Validate redirect_uri before constructing URL (prevents 500)
- Validate redirect_uri and client_id match in token exchange
- Strict mode requires PKCE code_challenge
- Fix TokenMap id type (use entity id, not uid string)
- Validate client_id on refresh token exchange
- Escape clientName in HTML output (XSS prevention)
- XML-escape all interpolated SAML response parameters
- Add xs namespace declaration for xsi:type attribute
- Add TTL expiry check for pending SAML requests
- Strict mode rejects unknown SPs even when none configured
- Update service list label to include SAML
- Type grant handler context as Context instead of any
- Add comment explaining reflect-metadata import
- Add test: JSON Content-Type on /token endpoint
- Add test: unsupported_grant_type rejection
- Add test: invalid SAML request ref rejection
- Use timingSafeEqual for PKCE verification
…d code

- Add eviction cap on revokedTokens and tokenClients (10k max)
- Remove dead sessions code (never written to)
- Validate response_type=code in /authorize (reject unsupported types)
- Wrap new URL() in try/catch in /logout endpoint
- Fix client_id binding: reject mismatch when code was bound to a client
- Only issue refresh token on refresh grant if offline_access in scope
- HTML-escape user inputs in error messages (XSS prevention)
- Remove dead getCertificatePem from crypto.ts
- Move @xmldom/xmldom to devDependencies
- Consolidate duplicate @internal/core imports in saml.ts
1. Regenerate lockfile to fix frozen-lockfile CI failures
2. Check revoked tokens in /userinfo so revocation is enforced
3. Return error for unknown uid instead of silently falling back to first user
4. Update all tests to use real user uids from the store
5. Add /logout endpoint test coverage (5 cases)
6. Add test verifying revoked tokens are rejected at /userinfo
…idp-plugin

# Conflicts:
#	packages/emulate/package.json
#	packages/emulate/src/api.ts
#	packages/emulate/src/commands/init.ts
#	packages/emulate/src/commands/start.ts
#	pnpm-lock.yaml
- CRITICAL: Remove SAML user fallback (auth bypass) — unknown uid now returns error
- Add memory cap on pendingSamlRequests and pendingCodes maps
- Increase eviction threshold to 50k with safety-valve documentation
- Add test for response_type validation
- Add test for PKCE strict mode enforcement
- Document intentional ID token claim inclusion
- SCIM schema URNs, ServiceProviderConfig, ResourceTypes
- TypeScript interfaces for SCIM User, Group, ListResponse, Error, PatchOp
- Bidirectional schema mapper (IdpUser ↔ ScimUser, IdpGroup ↔ ScimGroup)
- Enterprise User extension mapping (department, employeeNumber, manager)
- Response builders (ListResponse, Error, pagination)
- 12 schema mapper tests
- Supports add, replace, remove operations
- Dot-path and URN-prefixed path resolution
- Array element removal with value filters
- Top-level merge when no path specified
- Multiple sequential operations
- 11 PATCH handler tests
- Recursive descent parser for SCIM filter grammar
- Tokenizer, AST parser, evaluator
- All operators: eq, ne, co, sw, ew, gt, ge, lt, le, pr
- Logical: and, or, not with nested parentheses
- Value path filters: emails[type eq "work"].value
- URN-prefixed enterprise extension paths
- Case-insensitive string comparison per spec
- 20 filter parser tests
- SCIM bearer token auth middleware with constant-time comparison
- IdpSeedConfig extended with scim.bearer_token and scim.clients
- 16 SCIM provider endpoints: ServiceProviderConfig, Schemas, ResourceTypes,
  Users CRUD (list, get, create, replace, patch, delete),
  Groups CRUD (list, get, create, replace, patch, delete)
- Group membership sync: PATCH add/remove members updates user.groups[]
- Full filter support on list endpoints
- Pagination with SCIM 1-based startIndex
- Content-Type: application/scim+json
- userName uniqueness enforcement (409 on conflict)
- 20+ route integration tests
- ScimClient class for push provisioning to configured targets
- Fire-and-forget with error logging
- getScimClients() helper reads from store config
- Init template includes SCIM bearer token
- List description updated with SCIM endpoints
- 5 client tests (mock fetch)
- All tests passing
- CRITICAL: Group PATCH returns fresh data after mutation (re-fetch from store)
- MAJOR: Validate userName required on POST User (return 400)
- MAJOR: Type-aware comparison for gt/ge/lt/le filter operators
- MAJOR: Group PATCH replace handles members path (full member list replacement)
- MAJOR: Wire ScimClient push provisioning into route handlers
- MINOR: Remove dead code loop in filter parser
- MINOR: Validate count param for NaN
…pe coercion

- CRITICAL: undefined treated as null for eq/ne (SCIM spec compliance)
- CRITICAL: URN + dot-path resolution (urn:...:User:name.givenName)
- MAJOR: Recursion depth limit of 50 (prevents stack overflow DoS)
- MAJOR: Type coercion consistency for eq (number/string interop)
- MAJOR: Empty string not coerced to 0 in comparisons
- Short-circuit evaluation for and/or
…hip sync, type safety

- CRITICAL: Group POST now handles members in request body
- CRITICAL: PUT/PATCH preserves attributes via deep merge (no more data destruction)
- CRITICAL: Schema mapper uses !== undefined checks (empty strings allowed)
- MAJOR: Auth errors return application/scim+json content type
- MAJOR: Patch add creates intermediate objects for dot-paths
- MAJOR: Patch add single value to array appends instead of replacing
- MAJOR: Client URL strips trailing slash (prevents double-slash)
- MAJOR: Group rename updates user.groups references
- Groups CRUD: get, PUT replace, DELETE with user cleanup
- Missing userName validation (400 on POST)
- Exact count assertions replacing weak toBeGreaterThanOrEqual
- Manager attribute mapping (object and string forms)
- Filter: missing attribute treated as null, recursion depth limit
- Client: updateUser PUT, deleteGroup, trailing slash
- SCIM error response body format verification
- Auth: open access mode, missing auth header
- 20+ new test cases
- Add debug logging for all SCIM CRUD operations
- Wrap c.req.json() in try-catch for malformed JSON (returns 400 SCIM error)
- Validate parsed IDs for NaN (returns 400 instead of misleading 404)
- Type scimJson helper properly (Context instead of any)
- Import TokenMap type in test instead of inlining
- Add comment explaining SCIM auth middleware pattern
Copilot AI review requested due to automatic review settings March 24, 2026 19:19
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Mar 24, 2026

@Tbsheff is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

@socket-security
Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedreflect-metadata@​0.2.21001001008080
Added@​xmldom/​xmldom@​0.8.119910010081100
Addedxml-crypto@​6.1.29910010086100
Added@​peculiar/​x509@​2.0.09910010089100

View full report

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new enterprise IdP plugin package (@internal/idp) and wires it into the emulate CLI/API, extending the emulator suite with OIDC + SAML + SCIM 2.0 capabilities for local/CI identity testing.

Changes:

  • Introduces @internal/idp plugin with OIDC provider, SAML IdP, and SCIM v2 provider/client utilities.
  • Registers the new idp service in emulate CLI commands and programmatic API, including default init config.
  • Adds extensive Vitest coverage for OIDC/SAML/SCIM behavior and bundling updates for Node ESM/CJS interop.

Reviewed changes

Copilot reviewed 36 out of 37 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
pnpm-lock.yaml Adds lock entries for IdP deps
packages/emulate/tsup.config.ts Node platform + ESM require banner
packages/emulate/src/commands/start.ts Registers/seeds idp service
packages/emulate/src/commands/list.ts Lists IdP endpoints in CLI
packages/emulate/src/commands/init.ts Adds default IdP seed config
packages/emulate/src/api.ts Adds idp to programmatic API
packages/emulate/src/tests/api.test.ts Smoke test for IdP discovery
packages/emulate/package.json Adds @internal/idp workspace dep
packages/@internal/idp/tsup.config.ts Builds IdP package with tsup
packages/@internal/idp/tsconfig.json TS config for IdP package
packages/@internal/idp/src/store.ts Defines IdP store collections
packages/@internal/idp/src/scim/types.ts SCIM type definitions
packages/@internal/idp/src/scim/schema-mapper.ts IdP ↔ SCIM mapping helpers
packages/@internal/idp/src/scim/response.ts SCIM list/error response helpers
packages/@internal/idp/src/scim/patch-handler.ts SCIM PATCH operation executor
packages/@internal/idp/src/scim/filter-parser.ts RFC7644 filter parser/evaluator
packages/@internal/idp/src/scim/constants.ts SCIM URNs + resource metadata
packages/@internal/idp/src/scim/client.ts SCIM push-provisioning client
packages/@internal/idp/src/scim/auth.ts SCIM bearer auth middleware
packages/@internal/idp/src/saml-xml.ts SAML XML builders + signing
packages/@internal/idp/src/saml-constants.ts SAML constants + Entra mappings
packages/@internal/idp/src/routes/scim.ts SCIM v2 route handlers
packages/@internal/idp/src/routes/saml.ts SAML metadata + SSO routes
packages/@internal/idp/src/routes/oidc.ts OIDC provider routes
packages/@internal/idp/src/index.ts Plugin entry + seeding
packages/@internal/idp/src/helpers.ts Ephemeral maps + helpers
packages/@internal/idp/src/entities.ts IdP entity interfaces
packages/@internal/idp/src/crypto.ts JWT signing, PKCE, X509 cert gen
packages/@internal/idp/src/tests/scim.test.ts SCIM integration tests
packages/@internal/idp/src/tests/scim-patch.test.ts PATCH handler unit tests
packages/@internal/idp/src/tests/scim-mapper.test.ts Mapper unit tests
packages/@internal/idp/src/tests/scim-filter.test.ts Filter parser unit tests
packages/@internal/idp/src/tests/scim-client.test.ts SCIM client unit tests
packages/@internal/idp/src/tests/saml.test.ts SAML unit + integration tests
packages/@internal/idp/src/tests/idp.test.ts OIDC integration test suite
packages/@internal/idp/src/tests/crypto.test.ts Crypto utility unit tests
packages/@internal/idp/package.json New IdP package manifest
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +323 to +327
const input = scimGroupToIdpGroupInput(body);
const newGroup = idp.groups.insert({
name: (input.name as string) ?? "",
display_name: (input.display_name as string) ?? "",
});
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create Group does not validate required fields. If the request omits displayName, scimGroupToIdpGroupInput returns {} and this inserts a group with empty name / display_name, which will break membership sync and filtering. Validate displayName is a non-empty string and return a SCIM 400 invalidValue when missing/invalid.

Copilot uses AI. Check for mistakes.
Comment on lines +407 to +409
const operations = body.Operations ?? [];

for (const op of operations) {
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PATCH /Groups assumes every operation has a string op ((op.op as string).toLowerCase()). If op is missing or not a string, this will throw and bypass SCIM error formatting. Validate that each operation is an object with op as a string before processing, and return a SCIM 400 invalidValue for invalid operations.

Suggested change
const operations = body.Operations ?? [];
for (const op of operations) {
const operations = body.Operations;
if (!Array.isArray(operations)) {
return scimJson(
c,
scimError(400, "Invalid PATCH body: Operations must be an array", "invalidValue"),
400,
);
}
for (const op of operations) {
if (!op || typeof op !== "object" || typeof (op as { op?: unknown }).op !== "string") {
return scimJson(
c,
scimError(400, "Invalid PATCH operation: missing or invalid 'op'", "invalidValue"),
400,
);
}

Copilot uses AI. Check for mistakes.
Comment on lines +251 to +252
const updated = idp.users.update(id, {
email: (input.email as string) ?? user.email,
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PATCH /Users updates email from the patched resource but does not re-check uniqueness the way POST and PUT do. A PATCH that replaces userName can silently create duplicate emails. After deriving input.email, run the same uniqueness check as PUT (allowing the current user id).

Suggested change
const updated = idp.users.update(id, {
email: (input.email as string) ?? user.email,
const newEmail = (input.email as string) ?? user.email;
// Ensure email uniqueness (same behavior as PUT; allow current user id)
if (newEmail !== user.email) {
const existingUsers = idp.users.all();
const conflictingUser = existingUsers.find(u => u.email === newEmail && u.id !== id);
if (conflictingUser) {
return scimJson(
c,
scimError(409, "A user with this email already exists", "uniqueness"),
409,
);
}
}
const updated = idp.users.update(id, {
email: newEmail,

Copilot uses AI. Check for mistakes.
Comment on lines +143 to +155
parent = current;
if (current == null || typeof current !== "object") {
if (createIntermediates) {
(parent as Record<string, unknown>)[parts[i]] = {};
current = (parent as Record<string, unknown>)[parts[i]];
} else {
return { target: undefined, parent: null, key };
}
} else {
current = (current as Record<string, unknown>)[parts[i]];
if (current === undefined && createIntermediates) {
(parent as Record<string, unknown>)[parts[i]] = {};
current = (parent as Record<string, unknown>)[parts[i]];
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In resolvePatchPath, when createIntermediates is true and an intermediate segment is non-object, the code sets parent = current and then writes (parent as Record)[parts[i]] = {}. If current is a primitive (e.g., string), this will throw at runtime. Track the previous container object separately and either replace the non-object with {} (if allowed) or surface a SCIM invalidPath/invalidValue error.

Suggested change
parent = current;
if (current == null || typeof current !== "object") {
if (createIntermediates) {
(parent as Record<string, unknown>)[parts[i]] = {};
current = (parent as Record<string, unknown>)[parts[i]];
} else {
return { target: undefined, parent: null, key };
}
} else {
current = (current as Record<string, unknown>)[parts[i]];
if (current === undefined && createIntermediates) {
(parent as Record<string, unknown>)[parts[i]] = {};
current = (parent as Record<string, unknown>)[parts[i]];
const part = parts[i];
if (current == null || typeof current !== "object") {
if (!createIntermediates) {
return { target: undefined, parent: null, key };
}
// current is not an object; create an intermediate object on the last known parent
if (parent == null || typeof parent !== "object") {
throw new Error("Invalid SCIM path: cannot create intermediate on non-object");
}
(parent as Record<string, unknown>)[part] = {};
current = (parent as Record<string, unknown>)[part];
} else {
// current is a non-null object; descend into it
parent = current;
current = (current as Record<string, unknown>)[part];
if (current === undefined && createIntermediates) {
(parent as Record<string, unknown>)[part] = {};
current = (parent as Record<string, unknown>)[part];

Copilot uses AI. Check for mistakes.
}

// Sync members if provided
if (body.members) {
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PUT /Groups calls syncGroupMembers(newGroupName, body.members) when body.members is truthy, but it does not ensure members is an array of { value: string }. A non-array value will cause runtime errors inside syncGroupMembers. Check Array.isArray(body.members) (and validate member ids) before syncing, otherwise return a SCIM 400 invalidValue.

Suggested change
if (body.members) {
if (body.members !== undefined) {
if (!Array.isArray(body.members)) {
return scimJson(
c,
scimError(400, "Invalid 'members' format; expected an array", "invalidValue"),
400,
);
}
const invalidMember = body.members.find(
(m: any) => !m || typeof m.value !== "string" || m.value.trim().length === 0,
);
if (invalidMember) {
return scimJson(
c,
scimError(
400,
"Invalid 'members' entries; each member must have a non-empty string 'value'",
"invalidValue",
),
400,
);
}

Copilot uses AI. Check for mistakes.
Comment on lines +118 to +119
const matcher = parseFilter(filterParam);
scimUsers = scimUsers.filter(u => matcher(u as unknown as Record<string, unknown>));
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

filter parsing errors from parseFilter(filterParam) are not handled. If a client sends an invalid filter, the handler will throw and Hono will fall back to the app-level error handler, returning a non-SCIM error body/content-type instead of a SCIM 400 (e.g., scimType=invalidFilter). Wrap parseFilter/matcher execution in try/catch and return a SCIM error response with application/scim+json.

Suggested change
const matcher = parseFilter(filterParam);
scimUsers = scimUsers.filter(u => matcher(u as unknown as Record<string, unknown>));
try {
const matcher = parseFilter(filterParam);
scimUsers = scimUsers.filter(u => matcher(u as unknown as Record<string, unknown>));
} catch (_err) {
return scimJson(c, scimError(400, "Invalid filter", "invalidFilter"), 400);
}

Copilot uses AI. Check for mistakes.
});

app.get("/scim/v2/ResourceTypes", (c) => {
return scimJson(c, RESOURCE_TYPES);
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/scim/v2/ResourceTypes currently returns a bare array. SCIM list endpoints are expected to return a ListResponse envelope, and some SCIM clients will fail to parse a raw array. Consider returning a SCIM ListResponse (schemas, totalResults, Resources, etc.).

Suggested change
return scimJson(c, RESOURCE_TYPES);
const resources = RESOURCE_TYPES;
return scimJson(c, scimListResponse(resources, resources.length, 1, resources.length));

Copilot uses AI. Check for mistakes.
});

app.get("/scim/v2/Schemas", (c) => {
return scimJson(c, SCHEMAS);
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/scim/v2/Schemas currently returns a bare array. SCIM list endpoints are expected to return a ListResponse envelope, and some SCIM clients will fail to parse a raw array. Consider returning a SCIM ListResponse (schemas, totalResults, Resources, etc.).

Suggested change
return scimJson(c, SCHEMAS);
return scimJson(c, scimListResponse(SCHEMAS, SCHEMAS.length, 1, SCHEMAS.length));

Copilot uses AI. Check for mistakes.
Comment on lines +245 to +247
// Convert user to SCIM representation for patching
const scimUser = idpUserToScimUser(user, baseUrl, allGroups) as unknown as Record<string, unknown>;
const patched = applyPatchOps(scimUser, body.Operations);
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PATCH /Users does not validate body.Operations before passing it to applyPatchOps. If Operations is missing or not an array, this will throw and the response will bypass SCIM error formatting. Validate the PATCH payload shape and return a SCIM 400 (invalidValue/invalidSyntax) when malformed.

Copilot uses AI. Check for mistakes.
// Convert user to SCIM representation for patching
const scimUser = idpUserToScimUser(user, baseUrl, allGroups) as unknown as Record<string, unknown>;
const patched = applyPatchOps(scimUser, body.Operations);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SCIM PATCH /Users/:id crashes with TypeError: undefined is not iterable when the request body lacks an Operations array, instead of handling it gracefully.

Fix on Vercel

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants