
How to add SSO and SAML to my SaaS Product - Part 4
Part 4 of 4. Start with How to add SSO and SAML to my SaaS Product.
This is Part 4 of our series on adding SSO and SAML to your SaaS product. In previous parts, we covered the fundamentals of SAML and SCIM, and walked through a complete implementation using Clerk. In this final part, we'll explore alternative implementations using Auth0 and WorkOS, cover how to give customer admins self-serve SSO configuration, compare the vendors side-by-side, and discuss common pitfalls and best practices for maintaining a secure enterprise authentication posture.
Implementing with Auth0 (code snippets)
Auth0 is Okta-owned and aimed at broader general-purpose CIAM than Clerk's B2B-first posture. If you're already on Auth0 for user auth and need to add SAML, the path is short. Auth0's February 12, 2026 B2B plan upgrade materially improved the Free tier (1 Enterprise Connection, Self-Service SSO, and Inbound SCIM are all included there now). If you're choosing between Auth0 and Clerk for a greenfield B2B SaaS, Auth0's remaining drawbacks are the tier-cliff pricing (3 → 5 SSO connections forces a 5.3× price jump from Essentials to Professional) and the reliance on Auth0 Actions for customization (a function-as-a-service model that's flexible but more code than Clerk's Dashboard + component approach).
Setting up an enterprise connection
In the Auth0 Dashboard: Authentication → Enterprise → SAML → Create Connection. The form collects a connection name, a Sign In URL (the IdP's SSO endpoint), an optional Sign Out URL, and either a metadata URL or a manually pasted X.509 cert.
After creation, Auth0 generates two URLs you paste into the IdP configuration. Using the placeholder pattern Auth0 documents (replace YOUR_AUTH0_DOMAIN with your Auth0 tenant domain and CONNECTION_NAME with the connection you created):
- Post-back URL pattern:
https://YOUR_AUTH0_DOMAIN/login/callback?connection=CONNECTION_NAME. - Entity ID pattern:
urn:auth0:YOUR_TENANT:YOUR_CONNECTION_NAME.
Programmatic creation via the Management API:
// Auth0 Management API — create a SAML enterprise connection
await fetch(`https://${AUTH0_DOMAIN}/api/v2/connections`, {
method: 'POST',
headers: {
Authorization: `Bearer ${managementApiToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'acme-okta',
strategy: 'samlp',
enabled_clients: [AUTH0_CLIENT_ID],
options: {
metadataUrl: 'https://acme.okta.com/app/.../sso/saml/metadata',
signSAMLRequest: true,
},
}),
})For B2B multi-tenancy with Auth0 Organizations:
// Assign the SAML connection to an Auth0 Organization
await fetch(`https://${AUTH0_DOMAIN}/api/v2/organizations/${orgId}/enabled_connections`, {
method: 'POST',
headers: {
Authorization: `Bearer ${managementApiToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
connection_id: connectionId,
assign_membership_on_login: true,
}),
})assign_membership_on_login: true is Auth0's equivalent of Clerk's automatic-invitation enrollment mode — the first time a user signs in via the connection, they're added to the Organization.
Attribute mapping via Actions
Auth0 attribute mapping happens in a post-login Action — a small serverless function that runs after authentication. The idiomatic pattern:
// Auth0 Action: post-login trigger
exports.onExecutePostLogin = async (event, api) => {
if (event.connection.strategy === 'samlp') {
const idpDepartment = event.user.app_metadata?.saml_department
if (idpDepartment) {
api.idToken.setCustomClaim('https://your-app.com/department', idpDepartment)
api.user.setAppMetadata('department', idpDepartment)
}
}
}The custom claim namespace (https://your-app.com/department) is required by Auth0's claim validation — bare claim names without a namespace get stripped.
Next.js 16 integration
@auth0/nextjs-auth0 v4.18.0 supports Next.js 16 proxy.ts:
// proxy.ts
import type { NextRequest } from 'next/server'
import { auth0 } from './lib/auth0'
export default async function proxy(request: NextRequest) {
return auth0.middleware(request)
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico|api/auth).*)'],
}Where Auth0 differs from Clerk
- Universal Login + Actions vs prebuilt React components. Auth0's default flow routes through a hosted Universal Login page. Customization is done via Actions and the Auth0 Dashboard branding UI. Clerk ships drop-in React components (
<SignIn />,<OrganizationProfile />,<OrganizationSwitcher />) that you style with CSS. - 3 / 5 SSO connection cap per B2B tier. B2B Essentials ($150/month) tops out at 3 SSO connections; each additional connection is $100/month. B2B Professional ($800/month) includes 5 SSO connections. Scaling past Essentials' 3 connections forces the 5.3× price jump to Professional.
- February 12, 2026 Free-tier upgrade. Auth0 added 1 Enterprise Connection, Self-Service SSO, and Inbound SCIM to the Free plan — a meaningful improvement that narrowed the gap with Clerk and WorkOS. Self-Service SSO is a ticket-URL-driven guided flow, not a standing admin portal.
- Pricing cliff is still procurement-hostile. Your customer security review will ask to see your auth vendor's pricing. A 5.3× cliff at 4 connections looks like exactly the "SSO tax" pattern procurement is trained to reject. Have an answer ready.
Implementing with WorkOS (code snippets)
WorkOS is SSO-first and designed to drop on top of existing user authentication. It's the most procurement-friendly managed SSO service if your app already has users and sessions — the mental model is "we'll handle enterprise auth; you keep your user model." The differentiator is the Admin Portal: a hosted, white-labeled UI that lets customer IT admins self-configure SSO without touching your dashboard or theirs.
Creating an SSO connection
WorkOS connections are scoped to Organizations. Create the organization first, then generate an admin portal link for the customer to configure their own IdP.
// Server: start the OAuth-style redirect to the customer's IdP
import { WorkOS } from '@workos-inc/node'
const workos = new WorkOS(process.env.WORKOS_API_KEY!)
const authorizationUrl = workos.sso.getAuthorizationUrl({
organization: 'org_01HXYZ...',
redirectUri: 'https://your-app.com/callback',
clientId: process.env.WORKOS_CLIENT_ID!,
})
// Redirect the user to authorizationUrlThe callback handler exchanges a code for a profile:
// Server: handle the callback after the IdP authenticates the user
const { profile, accessToken } = await workos.sso.getProfileAndToken({
code: searchParams.get('code')!,
clientId: process.env.WORKOS_CLIENT_ID!,
})
// profile.email, profile.firstName, profile.lastName, profile.idpIdWorkOS's built-in staging organization org_test_idp is a mock IdP useful for local development — every sign-in succeeds with predictable test user attributes.
Admin Portal — the differentiator
// Server: generate a magic link the customer admin clicks to configure SSO
const { link } = await workos.portal.generateLink({
organization: 'org_01HXYZ...',
intent: 'sso',
})
// Send the link to the customer admin; they click through, configure their IdP,
// and the connection appears in your WorkOS dashboard automatically.The magic link is valid for 5 minutes, single-use. When the admin clicks through, WorkOS runs them through a step-by-step walkthrough for their specific IdP — 26+ IdPs with dedicated flows. This is what teams building from scratch effectively replicate when they build their own "customer-facing SSO admin portal"; with WorkOS, it's an API call.
The intent parameter supports: sso, dsync (Directory Sync), audit_logs, log_streams, domain_verification, certificate_renewal.
Next.js 16 integration
@workos-inc/authkit-nextjs v3 supports Next.js 16 proxy.ts via the authkitProxy helper:
// proxy.ts
import { authkitProxy } from '@workos-inc/authkit-nextjs'
export default authkitProxy({
middlewareAuth: {
enabled: true,
unauthenticatedPaths: ['/', '/about', '/pricing'],
},
})
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}AuthKit v3 always uses PKCE for the authorization code flow, which means the redirect URI must be registered as a public client in the WorkOS dashboard.
Where WorkOS differs from Clerk
- SSO-first, not full CIAM. Classic WorkOS is positioned as "we add enterprise auth on top of your existing user system." AuthKit — their newer full-auth product — closes that gap, but adoption patterns still lean on existing-app integration.
- Admin Portal is the marquee feature. If self-serve customer SSO configuration is a hard requirement, WorkOS wins this specific comparison. Clerk has a gap here (addressed via the custom-page workaround below).
- Higher per-connection price — $125/month per SSO connection and another $125/month per Directory Sync connection, vs Clerk's $75/month per enterprise connection with Directory Sync included. On volume (Clerk drops to $15/month at 500+ connections; WorkOS tiers down similarly but from a higher base), pricing is closer but Clerk remains the cheaper choice at most customer counts.
- Less ergonomic for new apps that don't yet have an organizations model or user table. Clerk gives you Organizations, users, and sessions out of the box; WorkOS gives you SSO alone (or AuthKit if you want more).
- Generous AuthKit free tier — 1M MAU on AuthKit is real and useful for early-stage products.
Self-serve SSO configuration for customer admins
WorkOS's Admin Portal (above) hands SSO setup to customer IT directly. Clerk has no built-in equivalent yet — here's how to build one with Clerk's custom pages pattern.
Clerk does not ship a white-labeled, customer-facing admin portal the way WorkOS's Admin Portal does. \<OrganizationProfile /> has tabs for General, Members, and Billing, but no built-in SSO tab as of April 2026. That's a real gap if handing configuration to customer IT without Dashboard access is a hard requirement.
The workaround is to build it yourself with Clerk's custom pages pattern:
- Add a custom
\<OrganizationProfile.Page label="SSO" url="sso">tab to the\<OrganizationProfile />component for organization admins. - Inside that page, render a React form collecting the customer's IdP metadata URL (preferred) or static IdP entity ID + SSO URL + X.509 certificate (fallback).
- On submit, POST to your own API route — the
app/api/sso/create/route.tsroute from Part 3. - The API route calls
(await clerkClient()).enterpriseConnections.createEnterpriseConnection({ saml: \{ ... \} })on behalf of the Organization admin. - Gate the page with
auth.protect({ role: 'org:admin', unauthorizedUrl: '/not-authorized' })so non-admins don't see the SSO tab.
A sketch of the Page component:
// app/account/(routes)/sso/page.tsx
'use client'
import { OrganizationProfile } from '@clerk/nextjs'
export default function SSOConfigPage() {
return (
<OrganizationProfile path="/account">
<OrganizationProfile.Page label="SSO" url="sso" labelIcon={<SSOIcon />}>
<SSOConfigForm />
</OrganizationProfile.Page>
</OrganizationProfile>
)
}The SSOConfigForm collects name, domain, and idpMetadataUrl and POSTs to your API route; see Clerk's custom-flows and custom-pages docs for the full implementation.
Side-by-side comparison
Feature matrix
Pricing models and hidden costs
Clerk. $20/month annual ($25/month monthly) Pro plan includes 1 enterprise connection. Additional connections: $75/month each, scaling down to $15/month at 500+ connections. 50,000 MAU free. Directory Sync included with each enterprise connection.
Auth0 Free (B2B). After the February 12, 2026 upgrade: 25,000 MAU, 1 Enterprise Connection, Self-Service SSO, and Inbound SCIM all included.
Auth0 B2B Essentials. $150/month. Includes 3 SSO connections. Additional SSO connections past 3 are $100/month each. SCIM included.
Auth0 B2B Professional. $800/month. Includes 5 SSO connections. SCIM included.
WorkOS. $125/month per SSO connection (tiered — scales down at higher connection counts). Directory Sync is an additional $125/month per connection. AuthKit (full auth product) is free to 1M MAU.
DIY. $27,000–$150,000 year-one engineering and operational cost per SSOJet's 2025 analysis. $10,000–$20,000 per year in ongoing maintenance. CVE-response is on you.
Time to first production SAML login
- Clerk via Dashboard: 1–2 days including IdP round-tripping with the customer.
- Clerk via
/enterprise_connectionsAPI + existing tests: 1–3 hours. - WorkOS with Admin Portal: 1 day. Customer IT admin does most of the work through the magic link.
- Auth0 with Actions customization: 1–2 weeks. More configuration surface area; Actions require code review.
- DIY with
@node-saml/passport-samland friends: 4–8 weeks for an MVP. 3–9 months for a production-hardened implementation with admin UI, cert rotation monitoring, and the 2025–2026 CVEs all patched.
Long-term maintenance considerations
Managed services absorb the CVE cycle for you. Your obligation is to keep the SDK version current and read the release notes when a security advisory lands. DIY means you own every CVE in the SAML stack — which, in the last twelve months, includes:
- SAMLStorm (CVE-2025-29774 and CVE-2025-29775) — March 2025.
xml-crypto≤ 6.0.0. Signature validation bypass. - ruby-saml parser differentials (CVE-2025-25291 and CVE-2025-25292) — March 2025. Dual-parser XSW class; "Sign in as Anyone" disclosure by GitHub Security Lab.
- samlify (CVE-2025-47949) — May 2025. CVSS 9.9. Signature wrapping. Patched in 2.10.0.
- node-saml (CVE-2025-54369) — July 2025. Schema-validate-before-signature-verify ordering bug.
- node-saml (CVE-2025-54419) — August 2025. Related class, different surface.
Five critical-severity CVEs in the Node.js SAML ecosystem in under twelve months is the ongoing baseline, not an anomaly. If you DIY, your on-call rotation needs a "SAML CVE" runbook.
Common pitfalls and best practices
Signature and certificate validation
Schema-validate before signature-verify. The XML parser has to know the document is well-formed before the signature check has any meaning — and more importantly, the parser used for signature validation must be the same parser used for attribute extraction. CVE-2025-25291 and CVE-2025-25292 (ruby-saml, "Sign in as Anyone") and CVE-2025-54369 (node-saml, July 2025) both exploit the gap between two different parsers seeing two different trees.
Always use absolute XPath, not relative XPath. Relative XPath makes signature wrapping trivial because an attacker can inject a signed subtree wherever the parser is looking relatively. Extract attributes only from signed, canonicalized content — never from the original document.
Clock skew and assertion time windows
SAML assertions carry NotBefore and NotOnOrAfter constraints. If your server clock drifts even a few seconds, valid assertions get rejected as expired or not-yet-valid. Enforce 1–5 minutes of clock-skew tolerance, synchronize servers with NTP, and cache assertion IDs to prevent replay. If you run more than one SP instance behind a load balancer, share the replay cache in Redis or similar so an assertion used against instance A can't be replayed against instance B.
Metadata refresh and key rotation
Accept an IdP metadata URL, not a pasted X.509 certificate. Cache the metadata with a version tag and refresh it on a schedule your SP controls (daily is reasonable). Monitor certificate expiry at 60, 30, and 7 days with loud alerts — a silent cert expiry is an Friday-afternoon outage waiting to happen. Support two active signing certificates during rotation windows so the IdP can swap keys without coordinating with you.
Scalekit's analysis of production SSO outages puts certificate rotation mismatches as the single most common cause. A weekend rotation by the customer's IT team, followed by a Monday-morning wave of "SSO is down" tickets, is the canonical failure mode.
Account linking: matching SSO users to existing accounts
When an existing user (created via magic link or password) later signs in via SAML for the first time, you have a choice: match them to the existing account or create a new one.
Clerk's default is to match if the IdP email matches a Clerk user's verified primary email. If the user's existing Clerk email is unverified and "require verified email before linking" is on, a new account is created. Call this edge case out with your customer success team during onboarding — you'll see tickets that look like "my account disappeared" when in reality a new un-linked account was created.
Handling multiple IdPs for the same tenant
Two common scenarios:
- A customer tenant spans multiple email domains (e.g.
acme.comprimary plusacme.co.ukUK subsidiary). Use Clerk's June 2025 "multiple domains per SSO connection" feature to attach all domains to one Organization's single connection. - Genuinely multi-IdP tenants (an acquired company still signs in with the acquirer's IdP for a while). Create multiple Enterprise Connections scoped to the same Organization. Verify each domain with the correct connection; Clerk routes the sign-in flow by email domain.
Role and group mapping from IdP claims
Prefer SCIM groups-to-role mapping (Clerk Directory Sync public beta sub-feature) over SAML claim-based role inference. SAML claim mapping runs only on sign-in — if a user's role changes in Okta while they're signed in, your app won't know until their session expires. SCIM updates propagate continuously via PATCH events, so role changes take effect within seconds.
Testing without a real IdP
MockSAML.com, SAMLTest.id, DummyIDP.com for quick browser-based tests. boxyhq/mock-saml Docker for CI. Keycloak Docker for fuller-fidelity including SCIM. Never let "we can only test in prod" survive a code review — there are too many free and self-hosted mock IdPs for that to be acceptable in 2026.
Graceful degradation when SSO is misconfigured
Keep a break-glass admin account: a non-federated, non-SCIM-managed account with a password stored in a physical safe or offline password manager. Dormant under normal operations. Tested quarterly to confirm the credentials still work. Used only when the IdP is down AND no SSO admin is currently signed in. PagerDuty's publicly documented break-glass pattern is the reference implementation.
Without break-glass, a failed IdP cert rotation on a Saturday morning can lock your own staff out of the admin console until the customer's IT team wakes up on Monday.
IdP-initiated SSO is usually a trap
Disable IdP-initiated assertions by default. Only enable for specific portal / tile use cases — Okta dashboard, Microsoft MyApps, Google Cloud Identity portal — where the customer has a concrete need. Scott Brady, IdentityServer, and Teleport all recommend this. NIST SP 800-63C (final, July 31, 2025) requires the federation transaction to be RP-initiated at FAL2 and FAL3 (SHALL), which effectively rules out unsolicited IdP-initiated assertions whenever you're targeting those assurance levels; at FAL1 the requirement is a SHOULD. Auth0's own SAML docs likewise flag IdP-initiated as "not recommended" due to the Login CSRF exposure.
If you must enable IdP-initiated for a specific tile flow:
- Per-connection allowlist — don't enable globally.
- Short assertion TTL (≤ 2 minutes).
- Strict audience and recipient validation — reject any assertion not explicitly intended for your ACS.
- Mandatory
RelayStateround-trip integrity check.
In Clerk, set allowIdpInitiated: false on the SAML connection by default (the Step 3 code example in Section 5 does this).
Debugging is the worst part
SAML debugging is "staring at base64 XML until your eyes bleed." SAML-tracer extension helps. Structured logging with redacted PII helps more. SAMLTool.com decoder for ad-hoc base64-to-XML. The fact that a SAML assertion is signed XML means most of the production debugging tools developers are used to (browser network tab, structured JSON logs) are less useful than they'd be for OIDC. Budget time for this — new engineers on the team will need a few days of exposure before they can triage a SAML error from the logs alone.
FAQ
Conclusion and next steps
You've just walked the path from an OAuth-familiar developer who knew the Authorization Code + PKCE flow to someone who can ship production SAML SSO on Next.js 16 with a real IdP, a per-tenant connection model, JIT provisioning, SCIM deprovisioning, a self-serve admin UX, and a CVE-aware security posture. The 2026 reality is that this is about two to three days of focused work with a managed service — Clerk being the default recommendation for Next.js App Router and B2B SaaS, with WorkOS as the alternative when customer-facing Admin Portal is non-negotiable, and Auth0 as the pick when you already live in the Okta ecosystem.
If you've followed along, your app is now enterprise-ready for SAML. Next up on the enterprise posture checklist:
- Audit logs. Customers will ask for a downloadable audit log of sign-ins, permission changes, and data access events within the first enterprise deal. Start wiring webhooks to an audit table now; don't wait.
- IP allowlists. Procurement sometimes asks whether your app can restrict access by corporate IP range. Managed auth services ship this as a configuration; DIY needs to build it.
- DPA (Data Processing Agreement). Legal will ask for a DPA template. Have one ready.
- Customer-facing SOC 2 Type II. Most enterprise customers will ask for your SOC 2 Type II report. Start the audit engagement as soon as you have 3 enterprise customers.
Further reading and reference documentation
- Clerk Enterprise SSO overview — product-level overview of SAML, OIDC, and EASIE in Clerk.
- Clerk Directory Sync docs — configuration, attribute mapping, and role mapping.
- Clerk Directory Sync GA changelog, April 16, 2026 — GA announcement and public-beta sub-feature notes.
- Clerk Backend API unification changelog, March 9, 2026 —
/enterprise_connectionsendpoint migration. - Clerk authorization checks guide —
auth.protect()vshas()trade-offs. - OWASP SAML Security Cheat Sheet — the single best DIY reference.
- NIST SP 800-63-4 (final, July 31, 2025) — federation assurance levels, phishing-resistant MFA guidance.
- OASIS SAML 2.0 specifications — Core, Bindings, Profiles, Metadata.
- IETF RFC 7643 / RFC 7644 — SCIM Core Schema and Protocol.
This concludes our four-part series on adding SSO and SAML to your SaaS product. By understanding the underlying protocols, evaluating the right vendor for your needs, and following security best practices, you can confidently offer enterprise-grade authentication to your B2B customers.
In this series
- How to add SSO and SAML to my SaaS Product
- How to add SSO and SAML to my SaaS Product - Part 2
- How to add SSO and SAML to my SaaS Product - Part 3
- How to add SSO and SAML to my SaaS Product - Part 4 (you are here)