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_identitytable) — 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 organizationproviderId— Supports multiple IdPs per organizationuserId— Links to the local Better-Auth user recordexternalId,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:
- Extracts SCIM attributes (
userName,externalId,emails,name,active) - Searches for existing Better-Auth user by email
- Creates new user or links to existing user
- Writes
scim_identityrow with IdP-specific metadata - Returns
201with 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_identityrow 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_grouprecord 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_tokentable withorganizationIdandproviderIdreferences - Support token rotation via
scim-provider-rotation.test.tspatterns
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_identitytable 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →