
How to add SSO and SAML to my SaaS Product
How do I add SSO and SAML to my SaaS product?
The fastest production path is to pick a managed auth service that exposes SAML as a first-class primitive, model every customer tenant as a per-organization SSO connection, let the customer's IdP admin upload metadata, enable SCIM (Directory Sync) for automated deprovisioning, and test end-to-end against a staging IdP before touching production. With Clerk on Next.js 16 that runs roughly 1–3 hours of code plus IdP round-tripping; building it yourself with the Node.js SAML libraries is realistic at 4–8 weeks for an MVP and 3–9 months production-hardened against the 2025–2026 XML signature and parser-differential CVEs. The walkthrough below covers the full Clerk implementation, side-by-side comparisons with Auth0 and WorkOS, the DIY reality check, and the pitfalls that trip up first-time SAML implementers.
Procurement, not revenue, is the trigger — the moment an enterprise security questionnaire asks whether your product supports SAML 2.0 SSO, the deal hinges on the answer. Ship SAML first because it is the protocol named by name in most enterprise workforce RFPs, then add OIDC or EASIE second for SMB and mobile.
SAML vs OIDC at a glance: the 30-second answer
Before you spend a sprint picking a protocol, here's the short version. If your buyer is a workforce enterprise on Okta, Entra ID, Ping, JumpCloud, or ADFS, ship SAML 2.0. If your buyer is a Google Workspace or Microsoft Entra tenant that doesn't require per-tenant cryptographic isolation, EASIE (multi-tenant OIDC) is the fastest route. If you're authenticating mobile, SPA, CLI, or service-to-service workloads, OIDC is the native fit.
Rule of thumb that fits in one sentence: ship SAML first for enterprise workforce SSO, OIDC (or EASIE) second for SMB and mobile. The three-column breakdown that adds EASIE is in Section 3; the decision tree is further down.
The "going enterprise" inflection point
A SaaS crosses from "growing" to "going enterprise" the moment an opportunity arrives that can't close without SSO. The trigger is procurement, not revenue. That moment can arrive at a $10,000 ACV contract with a company that bought enterprise-grade posture because the champion on the buyer side is a former CISO, or it can wait until a $250,000 ACV mid-market deal. Either way, the question arrives the same way: a PDF called something like "Vendor Security Questionnaire v4.3.xlsx" lands in your inbox, and line 47 asks whether your product supports SAML 2.0 SSO against a list of named identity providers.
If you haven't hit this inflection point yet, the data says you will. SoftwareFinder's 2025 SaaS Security Report measured that 68% of enterprise RFPs now mandate both MFA and SSO in the base plan — not as a premium upcharge — and that 43% of buyers have disqualified vendors for failing to provide verifiable credentials. The same report found that 61% of enterprises and 26% of SMBs require InfoSec sign-off before a software purchase can proceed. The inflection point isn't about company size anymore; it's about vertical and buyer sophistication.
The mechanical cost of not supporting SSO is real. Most vendors who land in procurement without documented SAML support get stuck in security review for additional weeks while InfoSec tries to map their auth model onto the customer's federation standards, and a meaningful share of those vendors never exit the security-review stage at all. The deal doesn't get negotiated down — it gets quietly moved to the "revisit next fiscal year" bucket while procurement finds a competitor who already ships SAML.
Who this guide is for
This article is written for an OAuth-familiar developer. You know the Authorization Code + PKCE flow. You've integrated "Sign in with Google." You understand what a redirect URI, an access token, a refresh token, and an ID token are. You can read a JWT. You have never shipped SAML in production.
You're about to meet a protocol from 2005 that speaks XML, uses HTTP POST to deliver assertions to something called an Assertion Consumer Service, trusts X.509 certificates that your customer's IT admin rotates on a schedule you cannot control, and patches a steady stream of signature-wrapping vulnerabilities that have shipped in production libraries as recently as August 2025. The goal here is to bridge the gap: every SAML concept is introduced by analogy to the OAuth concept you already know, and then the important differences are called out explicitly.
What you'll learn
This guide covers:
- Why enterprise customers mandate SSO and SAML, including the business, compliance, and security drivers that make "we don't support SSO" a deal-killer.
- How SAML differs from OIDC and OAuth, where your existing OAuth knowledge transfers cleanly, and where it doesn't.
- The three real implementation options — build it yourself, use a managed authentication service, or pick a single-purpose SSO gateway — with honest trade-offs and current pricing.
- A complete, production-ready walkthrough for adding enterprise SSO to a Next.js 16 app using Clerk, covering organizations, per-tenant SAML connections, attribute mapping, JIT provisioning, and SCIM-based deprovisioning.
- Side-by-side implementation snippets for Auth0 and WorkOS so you can see where each vendor diverges from Clerk.
- The security pitfalls that still ship in 2026 — XML Signature Wrapping (XSW), parser differentials, IdP-initiated replay attacks, and the certificate-rotation class of failures — and the mitigations you must implement regardless of which auth layer you pick.
- A comprehensive FAQ aimed at the questions procurement, customer IT, and your own team will ask once SAML lands in your codebase.
Why enterprise customers require SSO and SAML
Business drivers: unlocking enterprise deals
The "no SSO, no contract" reality
Enterprise procurement runs on checklists, and SAML is near the top of almost every one of them. The Vendor Security Alliance (VSA) questionnaire and the Shared Assessments SIG questionnaire — the two most common enterprise security intake forms — both ask explicitly whether a vendor supports SAML 2.0 SSO and whether the vendor supports SCIM 2.0 provisioning. The answer is pass/fail at many organizations. SOC 2, ISO 27001, and HIPAA — the three compliance frameworks procurement teams cite most often when describing "enterprise-ready" — all reference SAML and identity management as a standard control pattern, which is why procurement language often collapses "compliant vendor" and "SAML-capable vendor" into the same requirement.
The "SSO tax" debate has hardened buyer expectations. Ed Contreras, CISO of Frost Bank, famously called paying extra for SSO "an atrocity" in a widely cited 1Password blog interview. Rob Chahin's sso.tax catalog has turned the historical vendor practice of gating SSO behind premium tiers into a procurement veto signal. When 1Password, Notion, Tailscale, and others walked back their SSO-tier pricing between 2022 and 2024, they did so because procurement teams were using sso.tax listings to disqualify vendors before sales even got a call back.
Deal velocity and procurement checklists
ACV thresholds that trigger InfoSec review are predictable: under roughly $25,000 ACV you're selling to SMB, at $25,000–$100,000 you're in mid-market, and above $100,000 you're squarely in enterprise. The $50,000 threshold is where a full InfoSec review kicks in at most mid-market buyers (Tomasz Tunguz's SaaS benchmarks data, 2023). Every one of those reviews begins with a security questionnaire, and every questionnaire starts with authentication.
Operationally, the drag from a missing SAML answer on a security questionnaire is measured in weeks, not days: InfoSec has to run a longer vendor-risk assessment, your account executive has to escalate to customer IT, and the contract sits on the security-review bench until someone decides whether SAML is a blocker. That extra time is what turns a quarter-closing deal into a next-quarter problem.
Compliance drivers
SOC 2, ISO 27001, and HIPAA requirements
SOC 2 — Trust Services Criteria CC6.1, CC6.2, and CC6.3 — requires identity verification before granting access, credentialing and deprovisioning processes, and role-based access control with separation of duties. SSO is the standard control pattern auditors expect. The AICPA Trust Services Criteria (2017, revised 2022) frame SSO as a reasonable mechanism to satisfy all three.
ISO 27001:2022 Annex A controls A.5.15 (Access Control), A.5.16 (Identity Management), A.5.17 (Authentication Information), and A.5.18 (Access Rights) collectively mandate centralized identity with auditable provisioning and deprovisioning. A mature SSO integration is effectively the primary evidence artifact auditors want to see.
HIPAA — 45 CFR § 164.312 — requires unique user identification, audit controls, and authentication. The 2025 HIPAA Security Rule NPRM converts "addressable" specifications (including MFA and encryption) to required. HIPAA-regulated customers who buy your SaaS will increasingly demand MFA enforcement at the IdP plus SCIM deprovisioning with a documented 1-hour revocation SLA.
NIST SP 800-63-4 (final, July 31, 2025) strongly promotes phishing-resistant MFA and requires FIPS 140 Level 1+ key storage at FAL2 for federal agency IdPs. If you sell into federal-adjacent buyers, the practical implication is: support SAML with a customer-managed IdP that already meets the FAL2 bar.
GDPR Article 32 obligates risk-based security measures; SSO is a proportionate control for most B2B SaaS, and Schrems II considerations make a European customer's own IdP the cleanest data-flow boundary.
Auditable access and centralized deprovisioning
Every modern audit reviewer wants to know two things: who had access, and when was that access revoked? SAML plus SCIM answers both questions in a single protocol stack. The IdP owns the identity lifecycle, your app receives signed assertions at sign-in and SCIM events when membership changes, and both sides produce an audit trail that satisfies SOC 2 CC7 and ISO A.8.15 logging controls. Compared to per-application offboarding scripts, this is genuinely less work.
Security benefits of centralized identity
Enforcing MFA at the identity provider
Microsoft's Alex Weinert published the benchmark most cited in security conversations: "Your account is more than 99.9% less likely to be compromised if you use MFA." The Verizon DBIR 2024 reported that stolen credentials were the initial action in 24% of breaches, and the 2025 DBIR found that 88% of Basic Web Application Attacks used stolen credentials. Adoption still skews hard by organization size — JumpCloud's compilation of LastPass Global Password Security Report data shows roughly 87% MFA adoption at organizations with 10,000+ employees but only 27% at organizations under 25 employees. Pushing MFA enforcement to the IdP — rather than building it yourself — means every customer's MFA posture is as strong as their IT admin can make it, without your team shipping per-customer policy code.
Instant offboarding when employees leave
The Snowflake breach (UNC5537, mid-2024) is the textbook cautionary tale: approximately 165 customer tenants lacked mandatory MFA, and hundreds of millions of records were stolen from accounts with valid credentials and no federation. Snowflake's response was to make MFA mandatory by October 2024. A SAML plus SCIM integration gets you 90% of the way there automatically — when the employee is deactivated in the customer's IdP, a SCIM event fires to your app within seconds and the user's session is invalidated without your support team getting a ticket.
The cost of saying "we don't support SSO"
The SSO-tax debate has two sides, and both are worth engaging. On one side, sso.tax has catalogued markups that procurement teams find offensive: GitHub Enterprise's historical 425–525% markup for SSO, HubSpot's 5,000%+ SSO tier cliff, Appsmith's 16,567% markup, Webflow's 13,058%. On the other side, Tuple CEO Ben Orenstein put the counter-position plainly: "SSO costs close to nothing after a little automation." Tailscale reversed their SSO-tier pricing in April 2024 with the founder's note: "The SSO tax felt like a mistake." 1Password's business tier has included SSO since 2023.
The right answer for most B2B SaaS in 2026 is to bundle a reasonable number of SSO connections into the mid-tier plan and charge for scale, not for the feature itself. Charging extra for "business-critical security posture" is a procurement red flag that will cost you deals. Not charging enough means the economics don't work when a single customer onboards fifteen subsidiary organizations with fifteen IdPs. The pricing is a product decision, but the feature is not optional.
SSO and SAML fundamentals for OAuth developers
SSO, SAML, OIDC, and OAuth: how they relate
SSO is the goal; SAML and OIDC are two ways to get there. SSO means one login session unlocks many applications. SAML 2.0 is the 2005 XML-over-HTTP standard ratified by OASIS on March 14, 2005. OIDC 1.0 is the 2014 JSON-over-HTTP standard built on OAuth 2.0, finalized by the OpenID Foundation on February 26, 2014. OAuth itself is an authorization protocol, not an authentication protocol — it's the substrate that OIDC rides on top of. When marketing pages say "OAuth login," they usually mean "OIDC-on-top-of-OAuth," but it's worth keeping the distinction straight because it shapes what each token is allowed to do.
Here's the fuller three-column comparison, with the multi-tenant OIDC variant (EASIE) added. EASIE is what differentiates this from every other SAML-vs-OIDC article on the web — multi-tenant OIDC is an option most readers don't yet know exists.
Where OAuth knowledge translates (and where it doesn't)
The mental model you already have for OAuth maps pretty cleanly onto SAML.
Where it breaks down: SAML signatures are XML signatures (XML-DSIG), not JWT signatures — the algorithms and wire formats are different and the class of signature-wrapping vulnerabilities (XSW) is entirely SAML-specific. There is no PKCE in SAML because the browser POST binding that delivers the assertion is not subject to the same code-injection attack surface as OAuth's redirect-based flows. SAML supports an IdP-initiated flow (the IdP starts the exchange and POSTs an unsolicited assertion to your ACS) that has no OAuth analogue; treat it as a distinct attack surface. And finally, Single Logout exists in both protocols and is equally hard to get right in both — most production deployments simply don't implement it.
Why SAML still dominates enterprise IT
Legacy installed base plus IT procurement familiarity. Okta holds roughly 41% of the tracked standalone IAM market on 6sense's public tech-graph dashboard, and Microsoft disclosed more than 610 million Entra ID monthly active users on its FY2023 Q4 earnings call (July 25, 2023) (secondary coverage). Both IdPs support SAML and OIDC first-class. New enterprise IT apps increasingly ship OIDC; inherited apps stay on SAML. If your buyer has a ten-year-old IdP deployment with SAML parsers plugged into Splunk, don't fight the procurement language — ship SAML.
When to choose SAML vs OIDC for your B2B SaaS
The procurement reality: most enterprise workforce RFPs still name SAML by protocol. SoftwareFinder's 2025 SaaS Security Report finds 68% of enterprise RFPs require MFA plus SSO in base plans (not premium add-ons) and 43% of buyers have disqualified vendors for failing to provide verifiable credentials. Okta's public market position and Microsoft's Entra ID scale — discussed below — mean that when your customer IdP is Okta, Entra ID, Ping, JumpCloud, OneLogin, or ADFS, your customer's IT admins will expect SAML. If you ship one protocol first, ship SAML.
Definitively choose SAML when — any one of the following is sufficient:
- The customer's procurement or security questionnaire names "SAML 2.0 SSO" explicitly. This is near-universal in enterprise RFPs for workforce identity.
- The customer's IdP is Okta Workforce, Ping, ADFS, JumpCloud, or OneLogin. ADFS is OIDC-incapable in most deployed configurations — treat an ADFS customer as SAML-only.
- The customer requires IdP-initiated SSO from a corporate tile — Okta dashboard, Microsoft MyApps, Google Cloud Identity portal. Microsoft Entra does not expose
RelayStatefor OIDC connections, and Okta OIDC apps do not populate the corporate tile launcher the way SAML apps do. - The customer's SIEM, log shipper, or audit pipeline only parses SAML assertions. Mature enterprises have years of investment in Splunk SAML parsers and custom XML IOC rules; switching them to OIDC silently breaks their audit trail.
- The customer requires deep-linking via
RelayState— post-login redirect to a specific intranet URL is a first-class SAML feature and brittle-to-nonexistent in OIDC. - You're selling into US federal or defense (FedRAMP), healthcare (HIPAA), financial services, or any procurement template that pre-names SAML. NIST SP 800-63C (final, July 31, 2025) treats assertion formats as equivalent in terms of Federation Assurance Level protections — but procurement language isn't something you should fight on day one.
Definitively choose OIDC (or EASIE) when — any one is sufficient:
- The customer's IdP is Google Workspace or Microsoft Entra and tenant isolation is not a hard requirement → EASIE. One connection in your dashboard handles every Google and Microsoft customer tenant.
- You ship native mobile, SPA, or CLI clients — Authorization Code + PKCE is the native flow; SAML's browser-POST round trip is a poor fit.
- You're authenticating service-to-service or machine-to-machine workloads. Okta's developer guidance is explicit: "Okta does not support service-to-service authentication scenarios with SAML." Use OAuth 2.0 client credentials.
- You want
.well-knownautodiscovery plus JWKS rotation instead of emailed X.509 certs that silently rotate over weekends (Scalekit's #1 cited cause of production SSO outages). - The customer is a modern SMB on a developer-friendly OIDC-only IdP — Authentik, Keycloak, custom OIDC.
- Your product is API-first and every downstream service expects a JWT access token. SAML's XML assertion is a dead end for API authorization.
- Auto-deprovisioning matters and you can't ship SCIM yet. EASIE polls the IdP for membership, so removing a user from the Google or Microsoft tenant deprovisions them in your app without a separate protocol.
EASIE has a real architectural trade-off. Per Clerk's enterprise-connections docs: "The primary security difference between EASIE SSO and SAML SSO is that EASIE depends on a multi-tenant identity provider, while SAML depends on a single-tenant identity provider." Multi-tenant means one Google or Microsoft connection trusts every customer tenant — domain-claim verification (via Google's hd and Microsoft's xms_edov claims) is the boundary. NIST SP 800-63C applies the same FAL framework regardless of tenancy model, but FAL2 requires pre-established, per-RP trust agreements and injection-protected assertions, and FAL3 requires holder-of-key (or bound-authenticator) binding on top of that. Those requirements are straightforwardly satisfied by a dedicated single-tenant SAML deployment and harder to satisfy with a shared multi-tenant IdP like a Google-Workspace-wide EASIE connection — so customers who require per-tenant cryptographic isolation (financial-services audits, FedRAMP Moderate/High with dedicated signing keys, "no shared trust roots") should use SAML. This is analytical inference about tenancy and FAL, not a tenancy-specific statement in 800-63C.
The decision tree, in order:
- Does the buyer's RFP literally say "SAML 2.0," or name an IdP whose admins only speak SAML (Okta Workforce, Ping, ADFS, JumpCloud, OneLogin)? → SAML.
- Does the customer require IdP-initiated SSO from a corporate tile (Okta dashboard, Microsoft MyApps, Google Cloud Identity portal)? → SAML.
- Selling into US federal, defense, healthcare, financial services, or any environment requiring NIST 800-63-4 FAL2 or FAL3? → SAML.
- Is the buyer's IdP exclusively Google Workspace or Microsoft Entra and tenant isolation is not a hard requirement? → EASIE.
- Are you authenticating native mobile, SPA, CLI, or service-to-service workloads? → OIDC.
- None of the above? → SAML by default, because that's what most enterprise workforce RFPs name.
Why most mature B2B SaaS supports both. Workforce SSO is SAML-first; SMB and developer tenants prefer OIDC. Clerk's enterprise_sso strategy abstracts the protocol choice behind a single API — the app code is identical regardless of whether the connection underneath is SAML, OIDC, or EASIE. Plan for supporting both once you move past the first enterprise deal.
How SAML works in practice
The Service Provider (SP) and Identity Provider (IdP)
Your SaaS is the Service Provider (SP). The customer's Okta, Entra ID, or Google Workspace tenant is the Identity Provider (IdP). The SP generates a metadata document announcing its Entity ID and ACS URL. The IdP generates a metadata document announcing its SSO URL and signing certificate. Both sides exchange metadata once, and from then on the protocol runs as HTTP POSTs carrying signed XML.
SAML assertions and the Assertion Consumer Service (ACS)
A SAML assertion is a signed XML document containing a NameID (the user's stable identifier), AttributeStatement elements (email, first name, last name, and any custom claims), and Conditions elements that bound the time window and intended audience. The signature uses XML-DSIG, not JWT — the algorithms and canonicalization rules are different, and every CVE you'll read about in this article exploits a difference between what the signature covers and what the rest of your code reads.
The ACS is an HTTP endpoint on your SP that accepts POST requests containing base64-encoded SAML responses. A managed auth service owns that endpoint for you. A DIY implementation has to build it.
SP-initiated vs IdP-initiated flows
In an SP-initiated flow, the user lands on your app first, enters an email, you route them to their customer IdP, the IdP authenticates them, and the IdP POSTs a signed assertion back to your ACS. This is the safer default because your app controls the flow — there's a login CSRF cookie, a RelayState round-trip, and a request ID (InResponseTo) to match the response against.
In an IdP-initiated flow, the user clicks a tile in the Okta dashboard or Microsoft MyApps, and the IdP POSTs an unsolicited assertion to your ACS without any prior request from you. This has no login CSRF protection, no InResponseTo to validate, and is the vector behind several of the 2025–2026 CVEs discussed later. Scott Brady, IdentityServer, and Teleport all recommend disabling IdP-initiated unless a specific customer use case requires it. NIST SP 800-63C (final, July 31, 2025) requires RP-initiated federation transactions at FAL2 and FAL3 (a SHALL), which effectively rules out unsolicited IdP-initiated SSO at those assurance levels; at FAL1 the requirement is a SHOULD.
Metadata, certificates, and trust establishment
The IdP metadata XML declares the IdP's EntityID, its SingleSignOnService URL, and one or more X.509 signing certificates. Certificate key requirements: RSA 2048+ or ECC 256+, SHA-256 or stronger for signing. The tripwire is rotation: IdPs rotate signing certs on schedules you cannot control, typically with 14–30 days of notice, sometimes on a Friday afternoon with no notice at all. Scalekit's analysis of production SSO outages identifies certificate rotation mismatches as the single most common cause of production failures. Design for rotation from day one: accept a metadata URL (not a pasted certificate), cache it with a TTL, and support two active signing certificates during rotation windows.
SCIM: the other half of enterprise identity
Automated provisioning and deprovisioning
SAML creates a user on first login via Just-in-Time (JIT) provisioning. SCIM creates, updates, and — most importantly — deletes users proactively via a REST API. RFC 7643 defines the core schema; RFC 7644 defines the protocol. The IdP (Okta, Entra ID, Google Workspace) is the SCIM client; your SaaS is the SCIM service provider. When an employee is deactivated in the IdP's directory, a PATCH /Users/{id} with active: false fires to your SCIM endpoint within seconds.
When you need SCIM (and when you don't)
You need SCIM the moment a customer asks about deprovisioning. SAML alone leaves orphaned accounts when an employee is offboarded mid-session — the session cookie keeps working until it expires, and audit logs will show access events after the HR system says the employee left. Enterprise procurement increasingly mandates SCIM alongside SAML. HIPAA-aligned best practice is a 1-hour revocation SLA from IdP deactivation to session termination in your app.
You can defer SCIM for smaller customers. If your first enterprise customer has 15–50 seats, JIT-only is usually acceptable. Ask: "What's your deprovisioning SLA, and is SAML session-timeout acceptable for now?" If the answer is "24 hours is fine," you can ship JIT first and enable SCIM in the next sprint. Most enterprise customers will eventually want SCIM, though.
Multi-tenancy: SSO is per-organization, not per-app
Every customer tenant has its own IdP connection. The idiom is per-organization, not per-app: your app has one deployment; each customer has their own organization in your data model and their own SAML connection inside that organization. The routing is typically email-domain-based Home Realm Discovery — when a user enters alice@acme.com, your app looks up which organization owns the acme.com domain and routes the sign-in flow to that organization's SAML connection.
In Clerk, the Organization primitive is literally this model. In Auth0, it's called Organizations. In WorkOS, it's called Organizations too. The primitive is so universal across managed auth services that architecting without it is a tell that you're going to replatform later.
Implementation options
You have three realistic paths.
Option 1: build it yourself with open-source libraries
This is the "we'll ship it in a sprint" option that almost always turns into a 3–9 month timeline once admin UI, certificate-rotation handling, and the 2025–2026 CVE patch load are factored in. Here's why.
Common Node.js / TypeScript libraries (with CVE-safe minimum versions)
Not recommended in 2026: Clever saml2-js (CoffeeScript, maintenance mode) and any samlify below 2.10.0.
Across all four libraries, the pin-to-minimum-version discipline is non-negotiable. Each of the five critical CVEs cited below was disclosed, exploited in limited scope, and patched within the last twelve months. Running a library three minor versions behind is not "production" — it's a pending incident.
Security pitfalls you must handle yourself
XML Signature Wrapping (XSW) attacks are the defining SAML vulnerability class. The original Somorovsky et al. paper "On Breaking SAML: Be Whoever You Want to Be" (USENIX Security, 2012) showed that 11 of 14 major SAML frameworks were exploitable at the time. GitHub Security Lab's 2025 "Sign in as Anyone" research (CVE-2025-25291 and CVE-2025-25292 in ruby-saml) demonstrated the class of bug is not solved thirteen years later — it just mutates. The attack: inject a forged assertion outside the signed element, rely on parser differentials so the signature validator sees the real (signed) node and the attribute extractor sees the forged (unsigned) node. Result: alice@acme.com signs in as admin@target.com.
Signature and certificate validation. Always schema-validate before signature-verify — CVE-2025-54369 and CVE-2025-54419 in node-saml exploit exactly this ordering mistake. Use absolute XPath, not relative XPath. Never extract attributes from the original document — only from signed and canonicalized content.
Replay and clock-skew attacks. Validate InResponseTo against the request ID you stored at flow initiation (it's disabled by default in node-saml — flag this during review). Enforce NotBefore and NotOnOrAfter with 1–5 minutes of clock-skew tolerance. If you run more than one SP instance, share an InResponseTo cache in Redis or similar so an assertion used against one instance can't be replayed against another.
XML External Entity (XXE) injection. Disable DTDs entirely. Cap payload size. The OWASP XXE Prevention Cheat Sheet has the rules; xml-crypto ≥ 6.0.1 and @node-saml/node-saml ≥ 5.1.0 default to safe parsing, but older or wrapping code can still be vulnerable.
Parser differentials. CVE-2025-25291 and CVE-2025-25292: one parser for signature validation, a different parser for attribute extraction, both present on the same request. The signature parser sees the signed node and approves; the attribute extractor sees a forged sibling node with a different email. Mitigation: one parser for both stages, reject malformed XML strictly, treat any disagreement as an error.
Ongoing maintenance burden
Libraries handle the protocol. Everything else is on you.
- IdP metadata rotation is the #1 cause of SSO outages. Build monitoring for certificate expiry at 60, 30, and 7 days. Support two active signing certificates during rotation windows.
- Admin UI is not optional. You need metadata URL or XML upload, entity ID and ACS URL display, attribute mapping configuration, verified-domain management, per-connection enable/disable, cert expiry dashboards, a test-login flow, structured redact-on-PII logs, and a fallback non-SSO admin path for when the IdP is down.
- Support load is non-trivial. First-time customer IT admins routinely paste the wrong field into the wrong form, copy the IdP cert into the SP cert slot, and then file a P1 saying "SSO is broken." You will become a part-time SAML support engineer.
Cost estimate — SSOJet's illustrative DIY SSO breakdown (vendor analysis, not primary research): $27,000 year-one engineering build + $10,000/year maintenance + $40,000 opportunity cost + $20,000 security and compliance reserve + $15,000 sales and support drag = approximately $112,000 for year one. Slipping timelines push it to $150,000+. Expected timeline when teams kick off: 1–3 months. Typical actual: 3–9 months once admin UI, certificate rotation, and CVE patching land.
When DIY makes sense. Regulated industries with on-premises-only identity infrastructure. Teams with existing identity specialists. Products with open-source philosophical commitments. For everyone else: buy. The math doesn't close on DIY unless the non-financial requirements are non-negotiable.
Option 2: managed authentication services
There are roughly a dozen viable managed auth services for B2B SaaS in 2026. This article covers the three most procurement-friendly options at length and names the others briefly.
Clerk
B2B-first auth platform. SAML, OIDC, and EASIE multi-tenant OIDC as first-class primitives. The Organizations primitive is built in. Prebuilt React components (<OrganizationSwitcher />, <OrganizationProfile />, <SignIn />). Dashboard-driven connection setup plus a unified /enterprise_connections Backend API (unified March 2026). Directory Sync (SCIM 2.0) went GA on April 16, 2026. Native Next.js 16 proxy.ts support via @clerk/nextjs v7.x. Pricing after the February 2026 restructure: Pro $20/month annual ($25/month monthly), 1 enterprise connection included, $75/month per additional connection (scaling down to $15/month at 500+ connections). 50,000 free monthly active users.
Auth0
Okta-owned general-purpose CIAM. SAML plus OIDC enterprise connections. Organizations are on all plans. Next.js 16 is supported via @auth0/nextjs-auth0 v4.18.0 which honors proxy.ts. Pricing (post the February 12, 2026 B2B plan upgrade): 25,000 free MAU with 1 Enterprise Connection, Self-Service SSO, and Inbound SCIM included on the Free tier; B2B Essentials $150/month includes 3 SSO connections; B2B Professional $800/month includes 5 SSO connections; $100/month per additional connection. Self-Service SSO on Auth0 is a ticket-URL-driven guided assistant the customer admin completes in a one-time flow, not a standing admin portal the way WorkOS Admin Portal works.
WorkOS
SSO-first "drop on top of existing auth." The Admin Portal — a hosted, white-labeled customer-facing UI for self-serve SSO configuration — is the differentiator. SSO is $125 per connection per month (tiered down with scale); Directory Sync is $125 per connection per month separately. AuthKit, WorkOS's newer full-auth product, is free to 1M MAU. Next.js 16 support via @workos-inc/authkit-nextjs v3 using authkitProxy.
Also consider (one line each)
- Stytch — B2B; acquired by Twilio in November 2025.
- Descope — Drag-and-drop Flows; self-serve SCIM portal; good for B2C-adjacent B2B.
- PropelAuth — B2B-only; unique Bring-Your-Own-Auth sidecar pattern.
- Frontegg — Mature admin portal; more established mid-market focus.
- Ory Polis (formerly BoxyHQ SAML Jackson) — Open-source SAML bridge; Apache 2.0.
- Scalekit — Recently pivoting toward AI-agent-first authentication.
- SuperTokens / FusionAuth — Self-hostable options; more engineering effort to operate.
Not considered (with reason)
- Okta / OneLogin / JumpCloud / Entra ID — these are IdPs, not SPs. Your SaaS consumes them; you don't replace them with them.
- Kinde — no SCIM GA at time of writing.
- SSOJet / Userfront — too small for most enterprise procurement checks.
How to choose: a decision framework
Speed to production
Multi-tenant complexity
All three managed services support per-organization SSO connections. Clerk's Organizations primitive is the most ergonomic for teams that haven't built an org model yet — <OrganizationSwitcher /> and <OrganizationProfile /> are drop-in React components. Auth0 Organizations and WorkOS Organizations are more API-first; you'll spend more time wiring UI.
Pricing model and the "SSO tax" debate
Clerk no longer charges a per-connection premium over base subscription after the November 2024 EASIE launch and February 2026 restructure; the $75/month per-additional-connection price is straightforward. WorkOS is transparent at $125/month per connection. Auth0 has the steepest tier cliff in the market: 3 → 5 SSO connections forces a move from Essentials ($150/month) to Professional ($800/month) — a 5.3× price jump for two additional connections. Procurement readers will flag Auth0 pricing explicitly.
Developer experience and existing stack fit
Clerk leads for Next.js App Router plus React plus B2B. The @clerk/nextjs v7 package is Next.js 16 proxy.ts-native and ships prebuilt components for every SSO surface. Auth0 leads for broadest language and framework coverage — if your stack is Ruby, PHP, Go, Java, or .NET, Auth0's SDK catalog is deepest. WorkOS leads for "we already have auth, we just need SSO on top" — @workos-inc/authkit-nextjs and the Admin Portal are designed to bolt onto an existing user model rather than replace it.
Implementing SAML SSO with Clerk
This is the meaty section. It's a complete walkthrough on Next.js 16 with @clerk/nextjs v7.x, not a set of snippets — every step ends with a verification hook so you can confirm you're where you're supposed to be before moving on.
Prerequisites and architecture overview
Before you start, have:
The request flow at a high level:
[User in Next.js app]
→ (identifier-first sign-in, "alice@acme.com")
→ [Clerk Frontend API routes to Acme's IdP based on verified domain]
→ [Okta authenticates alice@acme.com]
→ [Okta POSTs signed SAML assertion to Clerk's ACS]
→ [Clerk Backend verifies signature, creates or updates User, mints session]
→ [User lands on /dashboard with orgId set and orgRole populated]You don't build most of the middle. You configure metadata on both sides, wire up the Next.js app, and let Clerk's Frontend + Backend APIs handle the protocol.
Step 1: Enable Enterprise SSO on your Clerk application
In the Clerk Dashboard: Authentication → SSO connections → Add connection → For specific domains or organizations. Pick whether you're configuring at the instance level (all organizations share the connection, rare) or at the organization level (per-customer, the default for B2B).
Pricing to know: Development instances (pk_test_* / sk_test_* keys) include all paid functionality for free — you can stage enterprise SSO for every customer before production cutover without paying per connection. Production instances on Pro include 1 enterprise connection; additional connections are $75/month each, scaling down to $60 at 16–100, $30 at 101–500, and $15/month at 500+.
An alternative first step: EASIE. If your customer uses Google Workspace or Microsoft Entra ID as their IdP, EASIE lets you skip SAML metadata exchange entirely. One EASIE connection in your Clerk dashboard trusts every Google and Microsoft tenant and uses domain verification as the tenant boundary. It's multi-tenant OIDC under the hood; setup is roughly one form submission. EASIE is the "0–30-day-to-first-enterprise-deal" option; full SAML is the "we're committed to enterprise long-term" option. Many teams ship EASIE first and add per-customer SAML connections as contracts demand it.
Step 2: Model customers as Organizations
Enterprise SSO in Clerk is organization-scoped. That means the orgId is part of the session token, role assignment derives from organization membership, and SAML connections attach to a single organization (or a set, if you're using Clerk's multi-domain feature from June 2025). Before you can create a SAML connection, the customer needs to exist as an Organization.
Mapping tenants to Clerk Organizations
If your app already has a multi-tenant data model (workspaces, teams, accounts), map each tenant to a Clerk Organization. Use <OrganizationSwitcher hidePersonal={true} /> in the app shell to let users move between organizations, and <CreateOrganization /> on onboarding to let the first user from a new tenant create the Organization record.
On the server, the Organization context is available via await auth():
// app/dashboard/page.tsx
import { auth } from '@clerk/nextjs/server'
import NoOrganizationCTA from './_components/no-org-cta'
export default async function Dashboard() {
const { userId, orgId, orgRole } = await auth()
if (!orgId) {
return <NoOrganizationCTA />
}
// Your org-scoped data fetch goes here.
// orgId is your multi-tenant foreign key.
return <DashboardContent orgId={orgId} orgRole={orgRole} />
}On the client, useOrganization() and useOrganizationList() give you the same data reactively. The Organization primitive is the anchor for everything that follows — plan your database schema so organizationId is a foreign key on every tenant-owned resource.
Verified domains and automatic organization routing
Clerk's Verified Domains feature is how users end up in the right Organization without manual invitation. An Organization admin verifies acme.com once (via an email OTP sent to admin@acme.com or a DNS TXT record). After that, any user signing up with an @acme.com email is routed to the Acme Corp organization automatically.
Three enrollment modes, configured per organization:
manual_invitation— default; admins must explicitly invite users.automatic_invitation— users from verified domains are added as members on first sign-in without needing to be invited.automatic_suggestion— users from verified domains see a "Join Acme Corp" prompt on first sign-in but aren't added automatically.
For B2B SaaS, automatic_invitation combined with SAML JIT provisioning is the most common pattern — the first time an Acme employee signs in via SAML, they land in the Acme organization with the default role applied.
Default limits to be aware of: 5 members per organization by default (configurable per plan), 100 organizations per user create-limit. The maxAllowedMemberships property on the Organization object controls the member cap (0 = unlimited). One important constraint: the same domain cannot be claimed by more than one organization at a time, and domain ownership cannot overlap between an Enterprise SSO connection and a plain verified-domain enrollment.
Step 3: Create a SAML connection
Generating your Service Provider metadata
In the Clerk Dashboard, inside the Organization you're configuring, go to SSO connections → Add SAML. Clerk auto-generates the ACS URL and the Entity ID per connection. Copy both — you'll paste them into the IdP configuration in Step 4.
Supported IdPs
Clerk supports Okta, Microsoft Entra ID (formerly Azure AD), Google Workspace, and generic SAML (saml_custom). The generic option covers OneLogin, PingFederate, JumpCloud, ADFS, and anything else that speaks SAML 2.0. For Okta and Entra ID, Clerk has guided walkthroughs that surface IdP-specific quirks inline.
Backend API: unified /enterprise_connections (March 2026)
If you're building the admin-facing form for your own customer IT admins, you'll want to create SAML connections programmatically. Clerk unified the /saml_connections and /oidc_connections endpoints into a single /enterprise_connections endpoint on March 9, 2026. The new shape groups protocol-specific parameters under a nested saml: {} or oidc: {} object, and the domain (singular string) became domains: string[] (plural array).
Here's a complete server route that creates a SAML connection on behalf of an Organization admin:
// app/api/sso/create/route.ts
import { auth, clerkClient } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'
export async function POST(req: Request) {
const { userId, orgId, has } = await auth()
if (!userId || !orgId) {
return NextResponse.json({ error: 'unauthenticated' }, { status: 401 })
}
if (!has({ role: 'org:admin' })) {
return NextResponse.json({ error: 'forbidden' }, { status: 403 })
}
const { name, domain, idpMetadataUrl } = await req.json()
const client = await clerkClient()
const connection = await client.enterpriseConnections.createEnterpriseConnection({
name,
domains: [domain],
organizationId: orgId,
active: true,
syncUserAttributes: true,
saml: {
idpMetadataUrl,
allowIdpInitiated: false,
allowSubdomains: false,
forceAuthn: false,
attributeMapping: {
emailAddress: 'user.email',
firstName: 'user.firstName',
lastName: 'user.lastName',
},
},
})
return NextResponse.json(connection)
}Note what's intentionally set here. allowIdpInitiated: false disables unsolicited IdP-initiated assertions by default — this matches Section 9 (Common Pitfalls) and what NIST SP 800-63C (final, July 31, 2025) requires for RP-initiated federation at FAL2 and FAL3. allowSubdomains: false prevents tenant-crossover if a customer adds a subsidiary with a different subdomain later. forceAuthn: false is the sensible default; setting it to true forces re-authentication on every sign-in, which is useful for high-security environments but hostile for typical workforce SSO.
The full set of parameters on EnterpriseConnectionSamlParams — per packages/backend/src/api/endpoints/EnterpriseConnectionApi.ts in clerk/javascript: allowIdpInitiated, allowSubdomains, attributeMapping, forceAuthn, idpCertificate, idpEntityId, idpMetadata, idpMetadataUrl, idpSsoUrl. There is no provider field on create — Clerk auto-detects the IdP slug (Okta, Entra, etc.) from the metadata document. The provider field only appears on the update endpoint when overriding the inferred value.
Step 4: Configure the Identity Provider side
Each major IdP has a slightly different configuration dance. The underlying fields are the same — ACS URL, Entity ID, attribute mapping, signing certificate — but the UI and claim naming conventions vary.
Okta walkthrough
- In the Okta admin console, go to Applications → Applications → Create App Integration → SAML 2.0.
- On the General Settings page, give the app a name ("Your SaaS") and optionally upload a logo.
- On the Configure SAML page, paste the ACS URL and Entity ID from Clerk's Dashboard. Leave
Name ID formatatEmailAddressandApplication usernameatEmail. - Under Attribute Statements, add:
email→user.emailfirstName→user.firstNamelastName→user.lastName
- On the Feedback page, select I'm an Okta customer adding an internal app.
- After the app is created, open the Sign On tab, click View Setup Instructions, and copy the Identity Provider metadata URL.
- Back in Clerk's Dashboard, paste the metadata URL into the SAML connection's IdP metadata URL field and save.
Verification: click Test Connection in Clerk's dashboard. A successful test ends with a "Connection verified" banner.
Entra ID (formerly Azure AD) walkthrough
- In the Azure portal, go to Microsoft Entra ID → Enterprise applications → New application → Create your own application → Non-gallery.
- Inside the new app, go to Single sign-on → SAML.
- Under Basic SAML Configuration, paste the Reply URL (this is Clerk's ACS URL) and the Identifier (this is Clerk's Entity ID).
- Under Attributes & Claims, add the following claims. Entra ID uses long WS-* URIs by default; these are XML namespace identifiers, not reachable HTTP URLs. You can leave them at the defaults or rename them:
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress → user.mail
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname → user.givenname
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname → user.surname- Under SAML Certificates, copy the App Federation Metadata Url and paste it into Clerk's IdP metadata URL field.
Google Workspace walkthrough
- In the Google Admin console, go to Apps → Web and mobile apps → Add app → Add custom SAML app. You need to be signed in as a Super Admin — delegated admins cannot create custom SAML apps.
- Give the app a name. On the Google IdP details page, copy the SSO URL, Entity ID, and download the X.509 certificate.
- On the Service provider details page, paste Clerk's ACS URL and Clerk's Entity ID. Set Name ID format to
EMAILand Name ID toBasic Information > Primary email. - Under Attribute mapping, map:
Primary email→mailFirst name→firstNameLast name→lastName
- Back in Clerk's Dashboard, paste the Google SSO URL and Entity ID, and upload the X.509 certificate (Google does not expose a metadata URL for custom SAML apps — you configure it statically).
If the customer's IdP is Google Workspace and they haven't explicitly required per-tenant SAML isolation, consider EASIE instead. EASIE skips all five of these steps and replaces them with a domain verification. Many customers prefer it once they see the delta; others insist on SAML for compliance reasons. Ask.
Step 5: Attribute mapping and custom claims
Clerk's default SAML attribute mapping covers the universal fields: email_address, first_name, last_name. Most integrations stop there and derive everything else from SCIM or from the user's first in-app interaction.
For custom claims — say, the customer wants the user's department from Okta to appear in your app's role logic — Clerk has a public_metadata bridge. Prepend the IdP claim name with public_metadata_ and Clerk will surface it on the user object:
// In Okta: add an attribute statement
// Name: public_metadata_department
// Value: user.department
// In your Next.js server component
import { auth, currentUser } from '@clerk/nextjs/server'
export default async function Dashboard() {
const user = await currentUser()
const department = user?.publicMetadata?.department as string | undefined
return <DashboardFor department={department} />
}Custom claim mapping runs on every sign-in (assuming syncUserAttributes: true on the connection), so updates in the IdP propagate to your app on the user's next session. Note that public_metadata is readable client-side — don't use it for authorization decisions that require tamper-resistance. For that, use SCIM group mappings or server-side role assignment.
Step 6: Just-in-Time (JIT) provisioning
JIT provisioning is on by default. On first SAML sign-in, Clerk creates the Clerk User, adds them to the Organization that owns the matching verified domain, and assigns the default role. The user's email, first name, and last name come from the SAML assertion — no pre-provisioning required.
The "Sync user attributes during sign-in" toggle on the connection controls whether subsequent sign-ins update the user attributes from the assertion. Default is on. Turn it off if your app is the source of truth for user display names and you don't want them overwritten by IdP changes.
JIT alone is not enough for enterprise procurement. It creates users; it doesn't deprovision them. For deprovisioning, use SCIM (the next step).
Step 7: Automated provisioning with SCIM (Directory Sync)
Clerk's core Directory Sync went GA on April 16, 2026 — see the Directory Sync GA changelog entry. Two sub-features (custom attribute mapping into publicMetadata, and groups-to-role mapping with precedence ordering) remain in public beta. Both are documented in the Directory Sync docs page, which carries scoped beta callouts on those two sections only; the rest of Directory Sync is GA and enabled for all users.
Supported IdPs
- Okta — officially supported with a dedicated OIN app catalog listing.
- Microsoft Entra ID — officially supported.
- Other SCIM 2.0 providers (JumpCloud, OneLogin, PingOne) generally work with the generic SCIM endpoint configuration but are best-effort.
Configuration
In the Clerk Dashboard, inside the Organization, go to Directory Sync → Enable Directory Sync. Clerk generates a SCIM token scoped to that Organization. Copy it.
In Okta, go to the SAML app you created in Step 4 → Provisioning → Configure API Integration → paste the SCIM token and Clerk's SCIM endpoint URL (shown in the dashboard). Enable Create Users, Update User Attributes, and Deactivate Users.
In Entra ID, go to the enterprise application → Provisioning → Provisioning Mode: Automatic → paste the SCIM URL and token.
Synced attributes
userName→ Clerkusernameemail.value→ Clerk primary emailname.givenName→ Clerkfirst_namename.familyName→ Clerklast_nameactive→ Clerk user enabled/disabled flag
Events
SCIM events fire as Clerk user.created and user.updated webhooks, plus dedicated SCIM webhook events:
scim.user.createdscim.user.patchedscim.user.deletedscim.provisioning_failed
Wire the webhooks to whatever downstream system needs to know about user lifecycle — your audit log, your billing system, your customer success tooling.
Two public-beta sub-features to note
Both are production-usable for most teams but may still see API tweaks before they are declared GA.
- Custom attribute mapping into
publicMetadata. Sync IdP attributes likedepartment,employee_id, orcost_centerinto the user'spublicMetadata. While Directory Sync is enabled, these fields become read-only in Clerk — the IdP is the system of record. - Groups-to-role mapping with precedence ordering. When Okta pushes the user's group memberships via SCIM, Clerk can map group names to Organization roles. Precedence ordering matters — if a user is in both the
adminsandviewersgroups, which role wins? You configure the precedence list in the dashboard.
Pricing
Directory Sync is included with each enterprise connection at no extra charge — consistent with Clerk's pricing-page and changelog messaging. That differs from Auth0, where Inbound SCIM is scoped per plan (and is included on the February 2026 Free tier), and from WorkOS, which charges $125/month per Directory Sync connection separately from SSO.
Step 8: Wire up your Next.js 16 app
Install
pnpm add @clerk/nextjs@^7Environment variables
Clerk reads the publishable and secret keys from environment variables. Put them in .env.local for local development and use vercel env add (or your platform's equivalent) for staging and production:
# .env.local
CLERK_PUBLISHABLE_KEY=pk_test_xxxxxxxxxxxxxxxxxxxx
CLERK_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxUse pk_test_* / sk_test_* for Development instances, pk_live_* / sk_live_* for Production. Vercel: vercel env add CLERK_SECRET_KEY for each environment. Never commit either key.
proxy.ts replaces middleware.ts
Next.js 16 renamed middleware.ts to proxy.ts and ships a codemod to migrate existing projects: npx @next/codemod@canary middleware-to-proxy .. The Next.js 16 file-convention reference (/docs/app/api-reference/file-conventions/proxy) states verbatim: "The file must export a single function, either as a default export or named proxy."
Clerk's Next.js quickstart uses export default clerkMiddleware(...) — the value returned by clerkMiddleware() is exported directly, which is the shorter idiomatic choice. Teams that prefer the named form can write export const proxy = clerkMiddleware(...) with no wrapper change.
// proxy.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
const isProtected = createRouteMatcher(['/dashboard(.*)', '/api/private(.*)'])
const isEnterpriseRoute = createRouteMatcher(['/enterprise(.*)'])
export default clerkMiddleware(async (auth, req) => {
if (isEnterpriseRoute(req)) {
await auth.protect({
unauthenticatedUrl: new URL('/sign-in', req.url).toString(),
unauthorizedUrl: new URL('/not-authorized', req.url).toString(),
})
return
}
if (isProtected(req)) {
await auth.protect()
}
})
export const config = {
matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
}Error handling for unauthorized users
auth.protect() has a response matrix that surprises first-time users. Specifically, an authenticated-but-unauthorized user gets a thrown 404, not a 403 — and there's no JSON body. This is intentional from a security-through-obscurity angle (don't leak which roles exist) but it produces bad UX on a SAML-protected enterprise route, so you should override it explicitly.
Mitigations for SAML-protected enterprise routes:
- Pass
unauthenticatedUrlandunauthorizedUrlexplicitly so the user lands on a branded page —/sign-in,/not-authorized,/upgrade-sso. The official Clerk docs list both parameters in the type signature but currently have no end-to-end code example using them. The snippet above fills that gap. - For signed-in users who lack an Organization (
orgIdis null), redirect to an org-selection CTA like/select-orgor render an inline<OrganizationSwitcher />. A thrown 404 is the wrong UX for a user who just hasn't picked their workspace yet. - For signed-in users whose Organization is valid but whose email domain hasn't been configured for SSO yet, route to a "request access" page (
/request-sso) that explains the step their IT admin must take. UseunauthorizedUrl: '/request-sso'whenhas()is applied with an SSO-specific check. - For API route handlers, prefer
has()manually when you need structured JSON error bodies.auth.protect()technically works in Route Handlers (the Clerk docs show one example), but it throws a bare 404/401 with no JSON body. Clerk's/docs/guides/secure/authorization-checksguide contrasts the two:auth.protect()"doesn't offer control over the response" whilehas()offers "flexibility and control."
<ClerkProvider> placement
The <ClerkProvider> wrapper must live inside <body> for Next.js 16 cache components to work correctly. Putting it outside <body> is a common mistake migrating from older Next.js versions.
// app/layout.tsx
import { ClerkProvider } from '@clerk/nextjs'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<ClerkProvider>{children}</ClerkProvider>
</body>
</html>
)
}Custom sign-in with the enterprise_sso strategy
For identity-first flows where the user enters their email and your app routes them to the right IdP, use authenticateWithRedirect with strategy: 'enterprise_sso'. Clerk resolves the email domain to the correct SAML connection automatically:
// app/sign-in/[[...sign-in]]/page.tsx
'use client'
import { useSignIn } from '@clerk/nextjs'
import { useState } from 'react'
export default function SignInPage() {
const { signIn, isLoaded } = useSignIn()
const [email, setEmail] = useState('')
if (!isLoaded) return null
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
await signIn.authenticateWithRedirect({
identifier: email,
strategy: 'enterprise_sso',
redirectUrl: '/sign-in/sso-callback',
redirectUrlComplete: '/',
})
}
return (
<form onSubmit={handleSubmit}>
<label>
Work email
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
</label>
<button type="submit">Continue with SSO</button>
</form>
)
}Wire up the callback page:
// app/sign-in/sso-callback/page.tsx
import { AuthenticateWithRedirectCallback } from '@clerk/nextjs'
export default function SSOCallback() {
return <AuthenticateWithRedirectCallback />
}When the user returns from their IdP, Clerk's callback component finishes the flow and redirects to redirectUrlComplete.
Role-scoped route protection
Inside a server component or route handler, use has() for authorization checks:
// app/api/enterprise/route.ts
import { auth } from '@clerk/nextjs/server'
export async function GET() {
const { isAuthenticated, orgId, has } = await auth()
if (!isAuthenticated) {
return Response.json({ error: 'unauthenticated' }, { status: 401 })
}
if (!orgId) {
return Response.json({ error: 'org_required', code: 'NO_ORG' }, { status: 403 })
}
if (!has({ role: 'org:admin' })) {
return Response.json({ error: 'forbidden', code: 'ROLE_REQUIRED' }, { status: 403 })
}
return Response.json({ data: 'your enterprise data here' })
}This pattern gives you structured JSON errors that your SPA or native client can parse, rather than relying on thrown 404s from auth.protect().
Step 9: Enable self-serve SSO configuration for customer admins
Here's the honest truth: Clerk does not ship a white-labeled, customer-facing admin portal the way WorkOS's Admin Portal works. <OrganizationProfile /> has tabs for General, Members, and Billing, but there's no built-in SSO tab as of April 2026. This is a real gap if "hand configuration to customer IT without giving them Dashboard access" is a hard requirement.
The workaround is to build it yourself using Clerk's custom pages pattern. It's not a weekend project, but it's straightforward.
- Add a custom
<OrganizationProfile.Page label="SSO" url="sso">tab to the<OrganizationProfile />component for organization admins. - Inside that page, render a React form that collects 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 — which is the
app/api/sso/create/route.tsexample from Step 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 component collects name, domain, and idpMetadataUrl, and POSTs to your API route. For the full component implementation pattern, see Clerk's custom-flows and custom-pages documentation.
Step 10: Test end-to-end
Staging IdPs
For team testing without involving a real corporate IdP, use:
- MockSAML.com (BoxyHQ / Ory) — hosted mock IdP, free, public.
- SAMLTest.id (Shibboleth project) — hosted mock IdP, public.
- DummyIDP.com — minimal hosted mock, fast round-trip.
boxyhq/mock-samlDocker image — self-hosted mock for CI and air-gapped testing.- Keycloak Docker image — heavier, fuller-fidelity; good for SCIM too.
For staging that mirrors production, use the free Okta Developer Integrator Plan (developer.okta.com) or a free Microsoft Entra tenant.
Isolating mock IdPs from production
A mock IdP with publicly known signing keys, trusted against a production Clerk instance, is functionally an authentication bypass for any email claim the connection accepts. Isolate strictly:
- Use a Development Clerk instance for all mock IdP testing. Clerk's Development (
pk_test_*/sk_test_*) and Production (pk_live_*/sk_live_*) instances are fully separated; SSO connections do not copy between dev and prod automatically. - Before adding a mock connection, verify: (1) the publishable key starts with
pk_test_, notpk_live_; (2) the Clerk Dashboard shows the Development banner; (3) the IdP metadata URL does not resolve to a production or customer-reachable domain; (4) the claimed email domain is a disposable test domain you own (e.g.,acme-test.example) and will never be verified in production; (5) the mock IdP container is not publicly exposed with default admin credentials — a classic Keycloak Docker leak vector. - Never paste a mock IdP's X.509 cert into a production tenant. OWASP's SAML Security Cheat Sheet is explicit: signing keys are a top security asset; trust should only be established via verified metadata URLs, not hand-pasted certs of unknown provenance.
- Rotate out before promotion. Before any Organization or tenant moves to production use, delete every connection whose IdP entityID points to
mocksaml.com,localhost,*.ngrok.io, or an internal Docker hostname. - Environment variable hygiene.
CLERK_SECRET_KEY, IdP signing keys, and anyboxyhq/mock-samlPRIVATE_KEYmust be per-environment and managed through a secrets manager (Vercel env, Doppler, Infisical). Never commit.envfiles containing mock IdP keys.
Real-world framing: the March 2026 CVE-2026-3055 Citrix NetScaler SAML IdP exposure, where crafted SAMLRequest payloads leaked session data via the NSC_TASS cookie and landed in CISA's KEV catalog, demonstrates the broader class of "test-grade IdP left reachable from production" risk. Mock IdPs that stay alive after their usefulness ends become production attack surface.
Pre-launch mock-IdP cleanup checklist:
- No connection in your production Clerk instance has an entityID containing
mocksaml.com,samltest.id,dummyidp.com,localhost,*.ngrok.io,*.ngrok-free.app, or any internal Docker or Kubernetes hostname. - No production connection trusts an X.509 certificate that was copy-pasted from a mock IdP or staging IdP.
- No production connection's claimed email domain resolves to a test or disposable domain you do not own (
*.example,*.test,*.localhost). - Every mock-IdP container spun up during testing has been torn down, and the hostnames no longer resolve.
- All
boxyhq/mock-samlPRIVATE_KEYand signing-key env vars have been rotated out of production secrets managers and no longer appear in any running deployment. - Customer-facing
/sign-in, organization onboarding, and admin-portal pages do not reference mock IdP documentation or testing URLs.
Validating the SAML response
Three tools to have in your toolkit:
- SAML-tracer browser extension (Chrome and Firefox). Captures the raw SAML request and response as they traverse the browser. Decode-on-hover makes debugging "why did this fail" tractable.
- SAMLTool.com online validator. Paste a base64-encoded SAML response and get the decoded XML with signature validation status.
- Structured logging on your own side. Log
Issuer,Audience,Destination,NotBefore,NotOnOrAfter, and the signing cert fingerprint as structured fields. Redact PII (emails, names) from logs unless you have a legal basis to retain them.
Troubleshooting common errors
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 in Section 5 Step 9).
- 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.
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.