# How to Integrate OpenWork with Microsoft Entra ID for SSO and SCIM Provisioning

> Integrate OpenWork with Microsoft Entra ID for seamless SSO and SCIM provisioning. Follow our guide to configure SAML and automate user management efficiently.

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

---

**To integrate OpenWork with Microsoft Entra ID, configure SAML SSO in the OpenWork dashboard, exchange ACS and metadata URLs with your Entra enterprise application, then enable SCIM provisioning with a bearer token-generated endpoint.**

OpenWork provides native support for **Microsoft Entra ID integration** through SAML-based single sign-on (SSO) and SCIM 2.0 user provisioning. This guide explains how to integrate OpenWork with Microsoft Entra ID based on the actual source code implementation in the `different-ai/openwork` repository.

## Architecture Overview

OpenWork exposes dedicated endpoints for SSO and SCIM operations, with **Better Auth** handling the underlying protocol implementation.

| Component | OpenWork URL | Purpose |
|-----------|-------------|---------|
| **SSO management** | `/dashboard/sso`, `/v1/sso/saml` | Create and manage organization-scoped SSO connections |
| **SAML callback** | `/api/auth/sso/saml2/sp/acs/openwork-sso-<org-id>` | Consumes Entra's SAML response |
| **SAML metadata** | `/api/auth/sso/saml2/sp/metadata?providerId=openwork-sso-<org-id>` | Service provider metadata for Entra |
| **SSO sign-in** | `/sso/<org-slug>` | Entry point for SP-initiated login |
| **SCIM management** | `/dashboard/scim`, `/v1/scim/token` | Generate and rotate SCIM bearer tokens |
| **SCIM provisioning** | `/api/auth/scim/v2` | Handles user create, update, delete, and group sync |

The OpenWork layer adds **organization-scoping**, **policy enforcement**, and **audit logging** on top of Better Auth's protocol handling. Security features include signed assertion verification, issuer validation, timestamp checks, and rejection of deprecated algorithms.

## How OpenWork Detects Microsoft Entra ID

OpenWork automatically recognizes Entra-managed domains through issuer URL pattern matching. The detection logic in [[`sso-entra-domain.ts`](https://github.com/different-ai/openwork/blob/main/sso-entra-domain.ts)](https://github.com/different-ai/openwork/blob/dev/ee/apps/den-api/src/sso-entra-domain.ts) validates the tenant ID format:

```ts
function getMicrosoftTenantId(value: string) {
  try {
    const url = new URL(value);
    const host = url.hostname.toLowerCase();
    if (host !== "sts.windows.net" && host !== "login.microsoftonline.com") return null;
    const tenantId = url.pathname.split("/").filter(Boolean)[0];
    return tenantId && /^[0-9a-f-]{36}$/i.test(tenantId) ? tenantId.toLowerCase() : null;
  } catch { return null; }
}

export function isMicrosoftEntraManagedDomain(input: { domain: string; issuer: string; entryPoint?: string | null }) {
  return getMicrosoftTenantId(input.issuer) !== null;
}

```

When you submit an **IdP Issuer URL** like `https://sts.windows.net/<tenant-id>/`, OpenWork calls `isMicrosoftEntraManagedDomain`. A `true` result enables **automatic domain verification** without DNS TXT validation for `.onmicrosoft.com` tenants.

## SCIM Token Authentication

SCIM requests use **bearer token authentication** with a multi-part encoding scheme. The resolver in [[`scim.ts`](https://github.com/different-ai/openwork/blob/main/scim.ts)](https://github.com/different-ai/openwork/blob/dev/ee/apps/den-api/src/scim.ts) extracts the organization and provider context:

```ts
export async function resolveScimProviderFromBearerToken(bearerToken: string) {
  let decoded: string;
  try { decoded = decodeBase64Url(bearerToken); } catch { return null; }

  const [rawToken, providerId, ...organizationParts] = decoded.split(":");
  const organizationId = organizationParts.join(":");
  if (!rawToken || !providerId || !organizationId) return null;

  const providerRows = await db
    .select()
    .from(ScimProviderTable)
    .where(and(eq(ScimProviderTable.providerId, providerId),
               eq(ScimProviderTable.organizationId, organizationId as OrganizationId)))
    .limit(1);

  const provider = providerRows[0] ?? null;
  if (!provider || !verifyStoredScimToken({ storedToken: provider.scimToken, rawToken })) {
    return null;
  }
  return provider;
}

```

Tokens are stored **hashed** (`SCIM_TOKEN_STORAGE_STRATEGY = "hashed"`) and verified via `verifyStoredScimToken`. This prevents raw token exposure and supports safe rotation. The implementation also includes **retry-back-off logic** through `SCIM_SYNC_RETRY_BASE_MS` and `SCIM_SYNC_MAX_ATTEMPTS` configuration.

## Step-by-Step Integration Guide

Follow these steps to complete your **OpenWork Microsoft Entra ID SSO and SCIM provisioning** setup:

### 1. Prepare the Entra Enterprise Application

- Create a new **non-gallery application** or reuse an existing one
- Assign at least one test user for validation
- Note the **Microsoft Entra Identifier** (IdP Issuer) and **Login URL** (Entry Point)

### 2. Configure SAML in OpenWork

Navigate to **Dashboard → SSO → SAML** and enter:
- **IdP Issuer URL**: Your Entra identifier (e.g., `https://sts.windows.net/<tenant-id>/`)
- **Domain**: Your organization's email domain
- **SAML Entry Point**: Entra login URL
- **Audience URL**: Usually your OpenWork base URL
- **IdP Certificate**: Base64-encoded X.509 certificate from Entra

Save the connection to generate OpenWork's **ACS URL** and **Metadata URL**.

### 3. Exchange Metadata with Entra

In your Entra enterprise application's **Single sign-on** settings:
- Paste OpenWork's **ACS URL** into **Reply URL (Assertion Consumer Service URL)**
- Paste OpenWork's **Audience URL** into **Identifier (Entity ID)**
- Download Entra's **Federation Metadata XML** if needed for verification

### 4. Enable SCIM Provisioning

In OpenWork, go to **Dashboard → SCIM**:
- Copy the **SCIM base URL**: `https://<your-openwork-domain>/api/auth/scim/v2`
- Generate a **bearer token** — this displays once; copy it immediately

### 5. Configure Provisioning in Entra

In your Entra application, select **Provisioning**:
- Set **Provisioning Mode** to **Automatic**
- Enter the **SCIM base URL** as **Tenant URL**
- Enter the **bearer token** as **Secret Token**
- Click **Test Connection** to validate

Configure attribute mappings:
- `userName` → `userPrincipalName` or `mail`
- `displayName` → standard mapping
- `emails[type eq "work"].value` → primary email

Enable group provisioning only if you plan to sync Entra groups to OpenWork teams.

### 6. Validate the Integration

| Test | Expected Result |
|------|---------------|
| Visit `/sso/<org-slug>` | Redirects to Microsoft login page |
| Complete Entra authentication | OpenWork creates/updates user and organization membership |
| Attempt email/password login for managed user | Returns "SSO required" error |
| Entra **Test Connection** | Success with valid bearer token |
| Assign user in Entra | User appears in OpenWork member list |
| Remove user assignment in Entra | `ScimUserTombstoneTable` records deprovisioning |

## Troubleshooting Common Issues

| Error | Cause | Solution |
|-------|-------|----------|
| "Reply URL is invalid" | ACS URL mismatch | Copy the exact **ACS URL** from OpenWork post-save and update Entra's **Reply URL** |
| `AADSTS700016` with issuer URL | Entra using IdP issuer as SP Entity ID | Set Entra **Identifier (Entity ID)** to OpenWork's audience URL, not the issuer |
| SCIM "Test Connection" fails with 401 | Token copied incorrectly or rotated | Regenerate token in OpenWork **Dashboard → SCIM** and update Entra's **Secret Token** |
| Group provisioning errors | OpenWork SCIM group sync limitations | Disable group object mapping in Entra; use group assignment for user scoping only |
| User not created on SCIM sync | Attribute mapping mismatch | Verify `userName` maps to a valid email attribute in Entra |

For comprehensive troubleshooting, reference the official guide at [[`docs/microsoft-entra-sso-scim.md`](https://github.com/different-ai/openwork/blob/main/docs/microsoft-entra-sso-scim.md)](https://github.com/different-ai/openwork/blob/dev/docs/microsoft-entra-sso-scim.md).

## Code Implementation Details

### Verifying SCIM Bearer Tokens

Internal handlers use `resolveScimProviderFromBearerToken` to authenticate incoming SCIM requests:

```ts
import { resolveScimProviderFromBearerToken } from "./scim.js";

async function handleScimRequest(req) {
  const token = req.headers.get("Authorization")?.replace(/^Bearer\s+/i, "");
  const provider = await resolveScimProviderFromBearerToken(token);
  if (!provider) throw new Error("Invalid SCIM token");
  // provider contains organizationId, providerId for request routing
}

```

*Source*: [[`scim.ts`](https://github.com/different-ai/openwork/blob/main/scim.ts)](https://github.com/different-ai/openwork/blob/dev/ee/apps/den-api/src/scim.ts), lines 30-55

### Rotating SCIM Tokens Programmatically

Administrators can rotate tokens via the internal API:

```ts
import { rotateOrganizationScimToken } from "./scim.js";

const { connection, scimToken } = await rotateOrganizationScimToken({
  organizationId: "org_01H2ABCDEF1234567890",
  headers: new Headers({ cookie: "session=…" })
});
// scimToken displays once — copy to Entra immediately

```

*Source*: [[`scim.ts`](https://github.com/different-ai/openwork/blob/main/scim.ts)](https://github.com/different-ai/openwork/blob/dev/ee/apps/den-api/src/scim.ts), lines 191-214

## Key Source Files

| File | Purpose |
|------|---------|
| [[`docs/microsoft-entra-sso-scim.md`](https://github.com/different-ai/openwork/blob/main/docs/microsoft-entra-sso-scim.md)](https://github.com/different-ai/openwork/blob/dev/docs/microsoft-entra-sso-scim.md) | Complete user-facing integration guide |
| [[`ee/apps/den-api/src/sso-entra-domain.ts`](https://github.com/different-ai/openwork/blob/main/ee/apps/den-api/src/sso-entra-domain.ts)](https://github.com/different-ai/openwork/blob/dev/ee/apps/den-api/src/sso-entra-domain.ts) | Entra domain detection (`isMicrosoftEntraManagedDomain`) |
| [[`ee/apps/den-api/src/scim.ts`](https://github.com/different-ai/openwork/blob/main/ee/apps/den-api/src/scim.ts)](https://github.com/different-ai/openwork/blob/dev/ee/apps/den-api/src/scim.ts) | SCIM token resolution, provisioning sync, retry logic |
| [[`ee/apps/den-api/src/sso.ts`](https://github.com/different-ai/openwork/blob/main/ee/apps/den-api/src/sso.ts)](https://github.com/different-ai/openwork/blob/dev/ee/apps/den-api/src/sso.ts) | SSO routes and ACS handling |
| [[`ee/apps/den-api/src/routes/org/oauth-providers.ts`](https://github.com/different-ai/openwork/blob/main/ee/apps/den-api/src/routes/org/oauth-providers.ts)](https://github.com/different-ai/openwork/blob/dev/ee/apps/den-api/src/routes/org/oauth-providers.ts) | Entra tenant validation on OAuth provider creation |

## Summary

- **OpenWork Microsoft Entra ID integration** uses SAML for SSO and SCIM 2.0 for user provisioning, built on Better Auth with organization-scoped policies
- **Automatic domain detection** occurs when the IdP Issuer URL matches `sts.windows.net` or `login.microsoftonline.com` patterns
- **SCIM authentication** uses hashed bearer tokens with encoded organization and provider context, preventing token leakage
- **Metadata exchange** requires copying OpenWork's generated ACS URL and Audience URL into Entra's SAML configuration
- **Troubleshooting** focuses on URL mismatches, token rotation, and attribute mapping alignment

## Frequently Asked Questions

### What SAML versions does OpenWork support for Microsoft Entra ID?

OpenWork supports **SAML 2.0** through Better Auth's implementation. The integration requires signed assertions and validates issuer, timestamps, and certificate chains. Deprecated signature algorithms are rejected according to the security policy in [[`scim.ts`](https://github.com/different-ai/openwork/blob/main/scim.ts)](https://github.com/different-ai/openwork/blob/dev/ee/apps/den-api/src/scim.ts).

### Can I use OpenID Connect instead of SAML with Microsoft Entra ID?

Yes. While this guide focuses on SAML, OpenWork also exposes `/v1/sso/oidc` endpoints. The Entra detection logic in [[`sso-entra-domain.ts`](https://github.com/different-ai/openwork/blob/main/sso-entra-domain.ts)](https://github.com/different-ai/openwork/blob/dev/ee/apps/den-api/src/sso-entra-domain.ts) is specific to SAML issuer URLs; OIDC configurations use standard OAuth 2.0 discovery without automatic domain verification.

### How does OpenWork handle SCIM user deprovisioning?

When Entra removes a user assignment, OpenWork creates a **tombstone record** in `ScimUserTombstoneTable` rather than hard-deleting the user. This preserves audit history while disabling organization access. The deprovisioned user's external identity remains marked inactive, and SSO-required enforcement prevents re-authentication through email/password.

### Is group provisioning required for OpenWork SCIM?

No. Group provisioning is **optional** and currently has limitations in OpenWork. You can disable group object mapping in Entra and rely solely on user provisioning. If enabled, groups sync to OpenWork's **Teams** feature with member assignments, but verify compatibility with your OpenWork version as group SCIM support varies.