How to Implement SCIM Provisioning for Team Management in OpenWork Den

OpenWork Den implements SCIM provisioning through a compatibility layer that wraps Better-Auth's SCIM plugin, using custom Den-owned route handlers with hashed bearer token authentication and optional group-to-team mapping.

This guide explains how to configure and extend SCIM user and group provisioning in the OpenWork Den control-plane component. The implementation separates IdP-specific identity data from the core user model, enabling secure multi-organization deployments with external identity providers like Okta, Azure AD, or OneLogin.

SCIM Architecture Overview

OpenWork Den's SCIM implementation consists of three integrated layers:

  • Identity model (scim_identity table) — Stores per-organization mappings between IdP SCIM fields and local Better-Auth users
  • Route handlers — Custom Den-owned handlers that sit in front of the Better-Auth SCIM plugin
  • Token security — Hashed bearer token storage with constant-time verification

The architecture intentionally decouples SCIM data from the global user table. This design allows the same local user to have different SCIM representations across multiple organizations.

The SCIM Identity Model

The scim_identity table maintains organization-scoped mappings between external IdP identities and internal users.

Key fields include:

  • organizationId — Isolates data by organization
  • providerId — Supports multiple IdPs per organization
  • userId — Links to the local Better-Auth user record
  • externalId, userName, displayName, emailsJson, active — SCIM-standard attributes

This model is defined in prds/scim/scim-compatibility-layer-plan.md and implemented in the Den database schema. By storing SCIM attributes separately, the system preserves IdP-specific metadata without polluting the core user model.

SCIM Route Implementation

OpenWork Den implements custom route handlers for SCIM v2 protocol support at /scim/v2/Users and /scim/v2/Groups.

These handlers address several protocol requirements that the base Better-Auth plugin does not fully satisfy:

Requirement Den Implementation
Pagination Correct startIndex/count handling with proper response metadata
Error codes 404 for unknown resource IDs
Response shape Preserves userName, externalId, and SCIM-standard fields
User linking Creates or joins existing Better-Auth users based on email match

The route handlers extract SCIM attributes from incoming requests, manage the scim_identity mapping table, and return spec-compliant responses.

Token Storage and Verification

SCIM providers authenticate using bearer tokens. OpenWork Den never stores raw tokens — instead, tokens are hashed using Better-Auth-compatible algorithms.

The implementation in ee/apps/den-api/src/scim-token-storage.js provides two core functions:

// Hash a token when creating a new SCIM provider
import { hashScimToken } from './scim-token-storage.js';

const rawToken = crypto.randomUUID(); // Generate secure token
const storedHash = hashScimToken(rawToken);
// Store `storedHash` in `scim_token` table; return `rawToken` to user once
// Verify token on incoming SCIM requests
import { verifyStoredScimToken } from './scim-token-storage.js';

function authenticateScimRequest(req, storedHash) {
  const bearer = req.headers['authorization']?.replace(/^Bearer\s+/i, '');
  if (!bearer) return false;
  
  return verifyStoredScimToken({
    storedToken: storedHash,
    rawToken: bearer
  });
}

Both functions use constant-time comparison to prevent timing attacks. The verifyStoredScimToken function is tested in ee/apps/den-api/test/scim-token-storage.test.ts to ensure compatibility with Better-Auth's hashing expectations.

User Provisioning Workflows

Creating SCIM Users

When an IdP sends POST /scim/v2/Users, the handler:

  1. Extracts SCIM attributes (userName, externalId, emails, name, active)
  2. Searches for existing Better-Auth user by email
  3. Creates new user or links to existing user
  4. Writes scim_identity row with IdP-specific metadata
  5. Returns 201 with complete SCIM representation
curl -X POST https://den-api.example.com/scim/v2/Users \
  -H "Authorization: Bearer $SCIM_TOKEN" \
  -H "Content-Type: application/scim+json" \
  -d '{
    "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
    "userName": "jdoe",
    "externalId": "ext-12345",
    "name": {"familyName": "Doe", "givenName": "John"},
    "emails": [{"value": "john.doe@example.com", "primary": true}],
    "active": true
  }'

Updating and Deactivating Users

  • PUT/PATCH requests modify the scim_identity row and optionally update the linked Better-Auth user
  • DELETE removes the organization-scoped mapping and deactivates the user's membership — the global user record remains intact

This soft-deletion approach preserves audit trails and allows re-provisioning without data loss.

Listing Users with Pagination

SCIM标准要求分页响应包含 startIndex, itemsPerPage, 和 totalResults:

curl -X GET "https://den-api.example.com/scim/v2/Users?startIndex=21&count=10" \
  -H "Authorization: Bearer $SCIM_TOKEN"

The handler translates SCIM 1-based indexing to database 0-based offsets and returns properly formatted ListResponse objects.

Group Provisioning and Team Mapping

SCIM groups in OpenWork Den support three operational modes controlled by the scimGroupMappingMode organization setting:

Mode Behavior
metadata_only Store group data in scim_group table; no OpenWork team created
create_teams Automatically create corresponding OpenWork teams
manual_mapping Admin must explicitly link SCIM groups to existing teams

When team mapping is enabled, group membership changes propagate to OpenWork team membership.

Creating SCIM Groups

curl -X POST https://den-api.example.com/scim/v2/Groups \
  -H "Authorization: Bearer $SCIM_TOKEN" \
  -H "Content-Type: application/scim+json" \
  -d '{
    "displayName": "Engineering",
    "externalId": "eng-group-001",
    "members": []
  }'

With scimGroupMappingMode = "create_teams", this creates:

  • A scim_group record with provider metadata
  • An OpenWork team named "Engineering"
  • Bidirectional sync for future membership changes

Group membership operations (PATCH with add/remove operations) update both the SCIM representation and the linked team.

Security Considerations

Token Lifecycle

  • Generate tokens with crypto.randomUUID() or equivalent CSPRNG
  • Hash immediately with hashScimToken(); discard raw value
  • Store only the hash in scim_token table with organizationId and providerId references
  • Support token rotation via scim-provider-rotation.test.ts patterns

Constant-Time Verification

The verifyStoredScimToken implementation prevents timing side-channels that could leak valid token prefixes. This is validated in the test suite against known oracle scenarios.

Organization Isolation

All SCIM queries include organizationId filters, ensuring cross-tenant data isolation at the database layer.

Key Implementation Files

Path Purpose
prds/scim/scim-compatibility-layer-plan.md Complete design specification for identity model, route semantics, pagination, and group handling
ee/apps/den-api/src/scim-token-storage.js hashScimToken() and verifyStoredScimToken() implementations
ee/apps/den-api/test/scim-token-storage.test.ts Token hashing and verification test suite
ee/apps/den-api/test/scim-groups.test.ts Group CRUD and team mapping tests
ee/apps/den-api/test/scim-provider-rotation.test.ts Token rotation workflow validation

Summary

  • OpenWork Den implements SCIM provisioning through a compatibility layer that wraps Better-Auth with custom route handlers
  • The scim_identity table decouples IdP-specific attributes from the global user model, enabling multi-organization deployments
  • Hashed bearer tokens with constant-time verification provide secure API authentication without storing raw secrets
  • Configurable group mapping (scimGroupMappingMode) allows organizations to choose between metadata-only storage, automatic team creation, or manual admin mapping
  • All SCIM endpoints follow SCIM v2 protocol requirements including proper pagination, error codes, and response formats

Frequently Asked Questions

What identity providers are compatible with OpenWork Den SCIM?

Any SCIM v2-compliant identity provider works, including Okta, Azure Active Directory, OneLogin, and JumpCloud. The implementation follows the IETF SCIM standard without vendor-specific extensions, though you should verify that your IdP supports standard user and group schemas.

How does OpenWork Den handle user email conflicts during SCIM provisioning?

When a SCIM POST /Users request arrives with an email matching an existing Better-Auth user, the system links the SCIM identity to that user rather than creating a duplicate. This preserves existing user data while establishing the IdP-managed SCIM mapping. The scim_identity table tracks which organization and provider own the mapping.

Can I migrate from metadata-only groups to automatic team mapping?

Yes. Change the organization's scimGroupMappingMode setting to "create_teams". Existing scim_group records without linked teams will generate corresponding OpenWork teams on the next group update from your IdP. Test this migration in a non-production environment first, as described in scim-groups.test.ts.

How do I rotate a compromised SCIM provider token?

Generate a new token, hash it with hashScimToken(), and update the stored hash in the scim_token table. The old hash becomes invalid immediately. Coordinate the rotation with your IdP admin to minimize provisioning interruptions — most IdPs allow configuring a new token before removing the old one.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →