How to Set Up SSO Authentication with OpenWork Den Organization Management

Use Better Auth's SSO plugin through OpenWork Den's /v1/sso/* API routes, which wrap provider registration, generate org-scoped sign-in URLs like /sso/:orgSlug, and automatically provision users via the provisionUser hook into a unified external_identity table.

OpenWork Den provides enterprise-ready SSO authentication for organization management by layering its own API and identity logic on top of Better Auth's protocol handling. This architecture separates security-critical operations (SAML/OIDC flows) from org-specific business rules like domain verification, JIT provisioning, and SCIM integration. This guide walks you through the complete setup flow as implemented in the different-ai/openwork repository.


Prerequisites

Before configuring SSO, ensure you have:

  • Organization admin access to your Den organization
  • Better Auth SSO plugin enabled (already configured in dev/ee/apps/den-api/src/auth.ts)
  • IdP credentials from your identity provider (Microsoft Entra, Okta, Google Workspace, etc.)

The Den API guards all SSO operations—direct calls to Better Auth's auth.api.registerSSOProvider are blocked by the mutation denial list in auth.ts.


Step 1: Register an SSO Provider via Den API

Organization admins register providers through Den's REST API rather than calling Better Auth directly. This ensures the provider ID follows Den's naming convention and metadata is safely stored.

Register an OIDC Provider

curl -X POST https://api.openworklabs.com/v1/sso/oidc \
  -H "Authorization: Bearer <org-admin-token>" \
  -H "Content-Type: application/json" \
  -d '{
        "issuer": "https://login.microsoftonline.com/<tenant-id>/v2.0",
        "clientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "clientSecret": "*****",
        "redirectUris": ["https://app.openworklabs.com/callback"],
        "organizationId": "org_acme_corp_id"
      }'

Register a SAML Provider

curl -X POST https://api.openworklabs.com/v1/sso/saml \
  -H "Authorization: Bearer <org-admin-token>" \
  -H "Content-Type: application/json" \
  -d '{
        "metadataUrl": "https://<tenant>.okta.com/app/.../sso/saml/metadata",
        "organizationId": "org_acme_corp_id"
      }'

Den generates a deterministic provider ID in the format openwork-sso-<organizationId>. This ID is used for all subsequent operations and callback routing.


Step 2: Store Provider Metadata Securely

When you register a provider, Den performs two storage operations:

  1. Private Better Auth storage — Raw protocol configuration (certificates, secrets) goes to Better Auth's internal ssoProvider table
  2. Public Den metadata — Sanitized connection info persists in Den's sso_connection table, generated via the ssoConnection type ID

This split ensures sensitive credentials never leak through Den's API while still allowing org admins to view connection status and ACS URLs.


Step 3: Configure Domain Verification

Enable domain verification to prevent unauthorized SSO attempts. Den surfaces Better Auth's verification token in the admin UI:

curl -H "Authorization: Bearer <org-admin-token>" \
  https://api.openworklabs.com/v1/sso

Response includes the verification token:

{
  "providerId": "openwork-sso-org_acme_corp_id",
  "signInUrl": "https://app.openworklabs.com/sso/acme",
  "domainVerification": {
    "enabled": true,
    "token": "openwork-verification=abc123def456",
    "verifiedDomains": ["acme.com"]
  }
}

Add the verification token to your DNS TXT records to complete domain ownership proof.


Step 4: Distribute the Org-Specific Sign-In URL

Each organization receives a stable, branded sign-in URL:


https://app.openworklabs.com/sso/:orgSlug

When a user visits this URL:

  1. Den resolves the organization from :orgSlug
  2. Verifies an enabled SSO connection exists
  3. Calls auth.api.signInSSO with the org's slug and generated provider ID

Better Auth handles the actual IdP redirect. The callback route (/api/auth/sso/callback/:providerId) processes the protocol response before Den takes over for user provisioning.


Step 5: Handle User Provisioning with the provisionUser Hook

Every successful SSO login triggers the provisionUser hook defined in dev/ee/apps/den-api/src/auth.ts. This hook creates or updates records in the unified external_identity table.

Key Provisioning Logic

// Excerpt from dev/ee/apps/den-api/src/auth.ts
import { createDenTypeId, normalizeDenTypeId } from "@openwork-ee/utils/typeid";

sso({
  provisionUser: async ({ user, userInfo, provider }) => {
    if (!provider.organizationId) return;
    
    const remoteId = pickRemoteIdentity(userInfo);
    const orgId = normalizeDenTypeId("organization", provider.organizationId);
    
    const payload = {
      organizationId: orgId,
      userId: normalizeDenTypeId("user", user.id),
      source: "sso",
      ssoProviderId: provider.providerId,
      remoteId,
      email: maybeString(userInfo.email) ?? maybeString(user.email),
      displayName: maybeString(userInfo.name) ?? maybeString(user.name),
      attributesJson: userInfo,
      active: true,
      lastSsoLoginAt: new Date(),
    };
    
    await db.insert(schema.ExternalIdentityTable).values({
      id: createDenTypeId("externalIdentity"),
      ...payload,
    }).onDuplicateKeyUpdate({
      set: {
        // Merge with existing SCIM data if present
        source: sql<string>`case when ${schema.ExternalIdentityTable.scimProviderId} is null then 'sso' else 'scim+sso' end`,
        ...payload,
      },
    });
  },
}),

The source field handles SCIM and SSO integration: when both are enabled, SCIM remains the source of truth for membership lifecycle while SSO provides just-in-time login and attribute refresh.


Architecture Overview

Layer Responsibility Key File
Better Auth SAML/OIDC protocol handling, raw provider storage, authentication redirects auth.ts SSO plugin config
Den API Org access validation, provider ID generation (openwork-sso-<orgId>), sanitized metadata storage, admin endpoints dev/ee/apps/den-api/src/auth.ts
Den UI SSO configuration page, ACS/redirect URL display, org-specific sign-in link presentation dev/packages/docs/cloud/sso-microsoft-entra.mdx
Identity Store Unified external_identity table combining SCIM and SSO attributes with policy-driven precedence provisionUser hook implementation

Enabling SSO-Only Mode

Organizations can disable password and social login options:

curl -X PATCH https://api.openworklabs.com/v1/organizations/acme \
  -H "Authorization: Bearer <org-admin-token>" \
  -H "Content-Type: application/json" \
  -d '{"ssoOnly": true}'

When SSO-only mode is active, unauthenticated users are automatically redirected to the organization's SSO sign-in URL.


Complete User Authentication Flow


┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   User      │────▶│ /sso/acme   │────▶│  Den API    │
│  Browser    │     │  (Den UI)   │     │             │
└─────────────┘     └─────────────┘     └──────┬──────┘
                                               │
                                               ▼
                                        ┌─────────────┐
                                        │auth.api.    │
                                        │signInSSO()  │
                                        │(Better Auth)│
                                        └──────┬──────┘
                                               │
                                               ▼
                                        ┌─────────────┐
                                        │    IdP      │
                                        │  Redirect   │
                                        └──────┬──────┘
                                               │
                                               ▼
                                        ┌─────────────┐
                                        │/api/auth/   │
                                        │sso/callback │
                                        │(Better Auth)│
                                        └──────┬──────┘
                                               │
                                               ▼
                                        ┌─────────────┐
                                        │provisionUser│
                                        │   Hook      │──▶ external_identity
                                        │  (Den API)  │    table updated
                                        └─────────────┘


Summary

  • Register providers via Den's /v1/sso/* API — never call Better Auth directly due to mutation denials in auth.ts
  • Provider IDs follow the pattern openwork-sso-<organizationId> — generated deterministically by Den
  • Use /sso/:orgSlug for user-facing sign-in — Den resolves the org and initiates protocol flow
  • The provisionUser hook unifies SCIM and SSO identities — stores records in external_identity with source-appropriate precedence
  • Enable SSO-only mode to enforce organizational authentication policies

Frequently Asked Questions

Can I register multiple SSO providers for one organization?

No. OpenWork Den enforces one active SSO provider per organization by default. The provider ID is deterministically generated as openwork-sso-<organizationId>, ensuring a single configuration slot. If you need to migrate providers, delete the existing connection before registering a new one.

How does Den handle the SSO callback from the identity provider?

Better Auth owns the protocol callback routes at /api/auth/sso/callback/:providerId, but Den intercepts the final redirect through the provisionUser hook. This hook creates or updates the user's external_identity record and then redirects to the organization's dashboard. The split ensures protocol correctness while allowing custom provisioning logic.

What happens when both SCIM and SSO are enabled?

SCIM remains the source of truth for membership lifecycle — including provisioning and de-provisioning — while SSO provides just-in-time login and attribute refresh. The provisionUser hook detects existing SCIM records via the scimProviderId field and updates the source column to 'scim+sso' to reflect the dual integration.

Where are SSO credentials actually stored?

Raw protocol credentials (SAML certificates, OIDC client secrets) reside in Better Auth's private ssoProvider table, inaccessible through Den's API. Den maintains its own sso_connection table with sanitized metadata, verification tokens, and org relationships. This architecture prevents credential exposure while preserving operational visibility.

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 →