# How to Implement SCIM Provisioning for Team Management in OpenWork Den

> Learn to implement SCIM provisioning for team management in OpenWork Den. Discover how it uses a compatibility layer with Better-Auth's SCIM plugin for seamless user management.

- Repository: [Different AI/openwork](https://github.com/different-ai/openwork)
- Tags: how-to-guide
- Published: 2026-08-15

---

**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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/ee/apps/den-api/src/scim-token-storage.js) provides two core functions:

```javascript
// 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

```

```javascript
// 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`](https://github.com/different-ai/openwork/blob/main/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

```bash
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`:

```bash
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

```bash
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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/ee/apps/den-api/src/scim-token-storage.js) | `hashScimToken()` and `verifyStoredScimToken()` implementations |
| [`ee/apps/den-api/test/scim-token-storage.test.ts`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/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.