Skip to content
Back to Build Tutorials

SSO Configuration

Set up SAML/OIDC single sign-on for enterprise customers with just-in-time provisioning.

Why

Enterprise customers will not buy your product without SSO — their security team requires it, and their IT team won't provision users one-by-one. SSO (SAML 2.0 or OIDC) lets users log in with their corporate identity (Okta, Azure AD, Google Workspace), and just-in-time provisioning creates their account on first login so there's no manual onboarding. The integration is fiddly (SAML XML, metadata exchange, certificate rotation) but it's a one-time per-customer setup that unlocks a 50-500 seat deal. Most B2B SaaS loses its biggest deals at the procurement stage because SSO isn't available.

When

When your first enterprise customer asks for it (usually >50 seats). Takes 1-2 days for SAML, 4h for OIDC. Skip for consumer-only products.

How

1. Choose the protocol by customer need: SAML 2.0 (legacy enterprise) or OIDC (modern)
   - SAML: XML-based, requires metadata exchange, dominant in Fortune 500
   - OIDC: JSON/REST, easier to implement, dominant in Google/Microsoft shops

2. Use a provider that handles the protocol complexity (WorkOS, Auth0, Clerk)
   - Do NOT implement SAML yourself — the XML signing + canonicalization will break
   - The provider gives you a per-customer connection (IdP-initiated or SP-Initiated)

3. Create a per-tenant connection when the customer onboards
   - They provide their IdP metadata URL or XML; you register it with the provider
   - Map their directory groups to your app roles (e.g., "Engineering" → admin)

4. Implement just-in-time provisioning in the callback handler
   - On first SSO login: create the user from IdP attributes (email, name, groups)
   - On subsequent logins: update groups/attributes (handles role changes in their IdP)

5. Provide a discovery endpoint /sso/login that accepts their email domain
   - User enters email → you look up the tenant by domain → redirect to their IdP
   - This avoids a per-customer login URL and feels like normal login

Example

// WorkOS-powered SSO callback
import { WorkOS } from "@workos-inc/node";
const workos = new WorkOS(env.WORKOS_API_KEY);

export async function GET(req: Request) {
  const { code } = parseQuery(req.url);
  const { user, organizationId } = await workos.userManagement.authenticateWithCode({
    client_id: env.WORKOS_CLIENT_ID, code,
  });
  // Just-in-time provisioning
  const dbUser = await db.user.upsert({
    where: { email: user.email },
    create: { email: user.email, orgId: organizationId, role: mapRole(user.groups) },
    update: { role: mapRole(user.groups) },  // sync roles on every login
  });
  await setSession(dbUser);
  redirect("/dashboard");
}

Common Problems

  • ⚠Hardcoding the IdP metadata instead of storing it per-tenant, so adding a second customer requires a code change.
  • ⚠Not handling IdP-initiated login (unsolicited SAML), so users who click their Okta tile get an error.
  • ⚠Forgetting to sync group/role changes on subsequent logins, so a demoted user keeps admin access.
  • ⚠Not providing a way to test SSO without the customer's real IdP — use a test IdP (WorkOS sandbox) for QA.

Best Practices

  • ✓Store the IdP connection config per-tenant in your DB so onboarding a new customer is a data change, not a deploy.
  • ✓Log every SSO login with the IdP, tenant, and assertion attributes — enterprise security teams will request this audit trail.
  • ✓Handle certificate rotation gracefully: fetch the IdP metadata periodically and update the signing cert without a code change.
  • ✓Offer OIDC first (easier for both sides) and only fall back to SAML when the customer's IdP doesn't support it.