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:
- Private Better Auth storage — Raw protocol configuration (certificates, secrets) goes to Better Auth's internal
ssoProvidertable - Public Den metadata — Sanitized connection info persists in Den's
sso_connectiontable, generated via thessoConnectiontype 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:
- Den resolves the organization from
:orgSlug - Verifies an enabled SSO connection exists
- Calls
auth.api.signInSSOwith 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 inauth.ts - Provider IDs follow the pattern
openwork-sso-<organizationId>— generated deterministically by Den - Use
/sso/:orgSlugfor user-facing sign-in — Den resolves the org and initiates protocol flow - The
provisionUserhook unifies SCIM and SSO identities — stores records inexternal_identitywith 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →