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.