# 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](https://clerk.com/glossary.md#security-assertion-markup-language-saml) as a first-class primitive, model every customer tenant as a per-organization [SSO](https://clerk.com/glossary/single-sign-on-sso.md) 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](https://clerk.com/glossary.md#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](https://clerk.com/glossary/openid-connect.md) 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](https://clerk.com/glossary/openid-connect.md)) is the fastest route. If you're authenticating mobile, SPA, CLI, or service-to-service workloads, OIDC is the native fit.

| Attribute                                                     | SAML 2.0                                                                                  | OIDC 1.0                                                                  |
| ------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| Best fit for B2B SaaS                                         | Workforce SSO into enterprise tenants — "Okta required" RFPs, IdP-initiated tile launches | Mobile / SPA / CLI, modern IdPs, service-to-service                       |
| Data format                                                   | Signed XML assertions                                                                     | JSON Web Tokens (ID Token)                                                |
| IdP-initiated SSO from corporate tile (Okta / Entra / Google) | Native via `RelayState`                                                                   | No native equivalent — Entra does not expose `RelayState` for OIDC        |
| Mobile / native fit                                           | Poor (browser POST round-trip)                                                            | Strong (Authorization Code + PKCE)                                        |
| Service-to-service / M2M                                      | Not supported                                                                             | OAuth 2.0 client credentials                                              |
| Enterprise IdP coverage                                       | \~100% of workforce IdPs                                                                  | \~95% (Okta, Entra, Ping, JumpCloud, Google all ship OIDC)                |
| 2025–2026 critical CVE count (Node.js ecosystem)              | 5+ critical (SAMLStorm, ruby-saml parser diff, samlify, node-saml ×2)                     | Far fewer; OIDC advisories caught pre-exploitation                        |
| NIST SP 800-63-4 FAL equivalence                              | Valid at FAL1 / FAL2 / FAL3                                                               | Valid at FAL1 / FAL2 / FAL3 — NIST treats assertion formats as equivalent |
| IdP marketplace coverage                                      | Deepest (Okta OIN, Entra Gallery)                                                         | Growing; smaller catalog today                                            |
| Typical procurement RFP language                              | "SAML 2.0 SSO" named explicitly                                                           | Rare; RFPs rarely require OIDC by name                                    |
| Direct answer for "which first?"                              | Ship SAML first — named in most enterprise workforce RFPs                                 | Ship OIDC second, or first when the customer IdP is modern-only           |

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](#sso-and-saml-fundamentals-for-oauth-developers); 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](https://softwarefinder.com/resources/saas-security-report-2025) 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](https://clerk.com/glossary.md#b2b-sso) to a Next.js 16 app using Clerk, covering [organizations](https://clerk.com/glossary.md#organizations), per-tenant SAML connections, [attribute mapping](https://clerk.com/glossary.md#attribute-mapping), [JIT](https://clerk.com/glossary.md#identity-provider-sso-idp-sso) provisioning, and [SCIM](https://clerk.com/glossary.md#directory-sync)-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](https://clerk.com/glossary.md#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."](https://techcommunity.microsoft.com/blog/microsoft-entra-blog/your-paword-doesnt-matter/731984) The [Verizon DBIR 2024](https://www.verizon.com/business/resources/reports/2024-dbir-data-breach-investigations-report.pdf) reported that stolen credentials were the initial action in 24% of breaches, and the [2025 DBIR](https://www.verizon.com/business/resources/reports/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](https://jumpcloud.com/blog/multi-factor-authentication-statistics) shows roughly 87% MFA adoption at organizations with 10,000+ employees but only 27% at organizations under 25 employees. Pushing [MFA](https://clerk.com/glossary.md#multi-factor-authentication-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.

| Attribute                          | SAML 2.0                                                          | OIDC 1.0                                                                             | EASIE (multi-tenant OIDC)                                                                                                                                                                                                                                                                                                                                            |
| ---------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Standard / ratified                | OASIS Standard, 2005                                              | OpenID Foundation final, 2014                                                        | Open spec maintained at easie.dev (Clerk-led, Nov 2024)                                                                                                                                                                                                                                                                                                              |
| Data format                        | Signed XML assertions                                             | JWT (ID Token) + JSON                                                                | JWT (ID Token) + JSON                                                                                                                                                                                                                                                                                                                                                |
| Metadata discovery                 | Static XML exchange + X.509 cert pinning                          | `.well-known/openid-configuration` + JWKS auto-rotation                              | `.well-known` + tenant `hd` claim                                                                                                                                                                                                                                                                                                                                    |
| Typical flows                      | SP-initiated, IdP-initiated                                       | Authorization Code + PKCE                                                            | Authorization Code + PKCE; hosted-domain matching                                                                                                                                                                                                                                                                                                                    |
| IdP-initiated SSO (corporate tile) | Yes (via `RelayState`)                                            | No native equivalent                                                                 | No (always SP-initiated)                                                                                                                                                                                                                                                                                                                                             |
| Deep-linking / post-login redirect | First-class via `RelayState`                                      | Approximated via `state` + provider-specific `target_link_uri`                       | Provider-specific                                                                                                                                                                                                                                                                                                                                                    |
| Service-to-service / M2M auth      | Not supported                                                     | OAuth 2.0 client credentials                                                         | Not in scope                                                                                                                                                                                                                                                                                                                                                         |
| Tenant isolation model             | Single-tenant per connection                                      | Single-tenant per connection                                                         | Multi-tenant (one connection, many customer tenants)                                                                                                                                                                                                                                                                                                                 |
| Mobile / SPA / CLI fit             | Poor (XML, browser POST)                                          | Strong (PKCE)                                                                        | Strong (PKCE)                                                                                                                                                                                                                                                                                                                                                        |
| Auto-deprovisioning                | Requires SCIM (separate protocol)                                 | Built-in via token revocation                                                        | Built-in via IdP polling                                                                                                                                                                                                                                                                                                                                             |
| Enterprise IdP coverage            | \~100% workforce IdPs                                             | \~95% (Okta, Entra, Ping, JumpCloud, Google)                                         | Google Workspace + Microsoft Entra only                                                                                                                                                                                                                                                                                                                              |
| 2025–2026 critical CVEs (Node.js)  | 5+ (SAMLStorm, ruby-saml, samlify, node-saml ×2)                  | Far fewer; OIDC advisories caught pre-exploitation                                   | Inherits OIDC CVE profile                                                                                                                                                                                                                                                                                                                                            |
| NIST SP 800-63-4 FAL fit           | Deployable at FAL1 / FAL2 / FAL3 with the required protections    | Deployable at FAL1 / FAL2 / FAL3 (NIST treats assertion formats as protocol-neutral) | Protocol-neutral per NIST; in practice, FAL2's pre-established per-RP trust and injection-protected assertions — and FAL3's holder-of-key binding — are harder to satisfy with a shared multi-tenant IdP than with a dedicated single-tenant SAML deployment (this is analytical inference about tenancy models, not a tenancy-specific statement in 800-63C itself) |
| FedRAMP / federal acceptance       | Supported (Login.gov integrates both)                             | Supported                                                                            | Customers requiring per-tenant cryptographic isolation should use SAML                                                                                                                                                                                                                                                                                               |
| Typical procurement RFP language   | "SAML 2.0 SSO" named explicitly in most enterprise workforce RFPs | Rarely required by name                                                              | Not referenced in RFPs                                                                                                                                                                                                                                                                                                                                               |
| Developer ergonomics               | XML signatures, manual trust establishment                        | JSON, standard discovery endpoints                                                   | Zero-config; verify email domain only                                                                                                                                                                                                                                                                                                                                |
| Primary B2B use case               | Workforce SSO — "Okta required" RFP                               | Modern IdPs, mobile, custom OIDC tenants                                             | Fastest path to "any Google Workspace customer signs in"                                                                                                                                                                                                                                                                                                             |

> NIST SP 800-63C (final, July 31, 2025) treats SAML and OIDC assertion formats as protocol-neutral — the FAL level you qualify for is determined by the assertion protections (audience restriction, injection protection, encrypted assertions, and — at FAL3 — holder-of-key binding), not by the format. The long-running industry folklore that "SAML is more secure for compliance" is not reflected in current NIST guidance. Procurement templates still often name SAML explicitly, but the cryptographic bar is set by the FAL trust-model requirements, not by the choice between XML and JSON.

#### Where OAuth knowledge translates (and where it doesn't)

The mental model you already have for OAuth maps pretty cleanly onto SAML.

| OAuth / OIDC concept               | SAML equivalent                                                                                      |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------- |
| Authorization Code flow            | SP-initiated SAML flow                                                                               |
| Redirect URI                       | [Assertion Consumer Service (ACS) URL](https://clerk.com/glossary.md#assertion-consumer-service-acs) |
| Client ID                          | Entity ID                                                                                            |
| `state` parameter                  | `RelayState`                                                                                         |
| ID Token (JWT)                     | SAML assertion (signed XML)                                                                          |
| `.well-known/openid-configuration` | IdP metadata XML document                                                                            |
| JWKS rotation                      | X.509 certificate rotation (manual)                                                                  |

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](https://6sense.com/tech/identity-access-management/okta-market-share), and Microsoft disclosed more than 610 million Entra ID monthly active users on its [FY2023 Q4 earnings call (July 25, 2023)](https://www.microsoft.com/en-us/Investor/earnings/FY-2023-Q4/press-release-webcast) ([secondary coverage](https://www.bigtechwire.com/2023/07/26/microsoft-entra-id-maus-linkedin-members/)). 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 `RelayState` for 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-known` autodiscovery 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:**

1. 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.**
2. Does the customer require IdP-initiated SSO from a corporate tile (Okta dashboard, Microsoft MyApps, Google Cloud Identity portal)? → **SAML.**
3. Selling into US federal, defense, healthcare, financial services, or any environment requiring NIST 800-63-4 FAL2 or FAL3? → **SAML.**
4. Is the buyer's IdP exclusively Google Workspace or Microsoft Entra **and** tenant isolation is not a hard requirement? → **EASIE.**
5. Are you authenticating native mobile, SPA, CLI, or service-to-service workloads? → **OIDC.**
6. 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.

> The OpenID Foundation is publishing **OIDC Federation 1.0** as a final spec in Q1–Q2 2026 (final review closed February 2026). This is OIDC's answer to SAML's federation model, but real-world IdP adoption is years out. Don't time your 2026 roadmap to it.

### How SAML works in practice

#### The Service Provider (SP) and Identity Provider (IdP)

Your SaaS is the [Service Provider](https://clerk.com/glossary.md#service-provider) (SP). The customer's Okta, Entra ID, or Google Workspace tenant is the [Identity Provider](https://clerk.com/glossary.md#identity-provider-sso-idp-sso) (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](https://clerk.com/glossary.md#directory-sync) 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](https://clerk.com/glossary.md#organizations) 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)

| Library                    | Minimum safe version (April 2026) | Notes                                                                                                                                          |
| -------------------------- | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `@node-saml/passport-saml` | ≥ 5.1.0                           | Passport.js strategy; \~423k weekly downloads                                                                                                  |
| `@node-saml/node-saml`     | ≥ 5.1.0                           | Framework-agnostic; \~564k weekly                                                                                                              |
| `samlify`                  | ≥ 2.10.0                          | TypeScript; \~488k weekly. 2.10.0 patches CVE-2025-47949 (CVSS 9.9 signature wrapping).                                                        |
| `xml-crypto`               | ≥ 6.0.1                           | Foundational XML signature layer; \~2.6M weekly. 6.0.1 fixes [SAMLStorm](https://workos.com/blog/samlstorm) (CVE-2025-29774 / CVE-2025-29775). |

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](https://github.blog/security/sign-in-as-anyone-bypassing-saml-sso-authentication-with-parser-differentials/) (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](https://ssojet.com/blog/the-hidden-150-000-cost-of-a-simple-sso-feature) (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](https://clerk.com/glossary.md#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](https://clerk.com/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](https://auth0.com/pricing) (post the [February 12, 2026 B2B plan upgrade](https://auth0.com/blog/auth0-b2b-plans-upgraded/)): 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](https://workos.com/pricing) (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

| Option                             | Time to first production SAML login           |
| ---------------------------------- | --------------------------------------------- |
| Clerk (Dashboard + IdP round-trip) | 1–3 hours with the API + existing tests       |
| WorkOS (Admin Portal)              | "Days" per marketing; 1 day realistic         |
| Auth0 (with Actions customization) | 1–2 weeks                                     |
| DIY with libraries                 | 4–8 weeks MVP; 3–9 months production-hardened |

#### 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:

- [ ] A Next.js 16 app using the App Router, React 19, `@clerk/nextjs` v7.x, and Node 22+.
- [ ] A customer to test against — for our walkthrough, we'll call them "Acme Corp" and assume they use Okta. If you don't have a customer yet, spin up a free Okta Developer Integrator Plan at `developer.okta.com`, or create a free Microsoft Entra tenant.
- [ ] A Clerk application with Organizations enabled. Organizations are not on by default — you flip the toggle in **Configure** → **Organizations** once. If your development instance is new, Clerk also prompts you to enable Organizations the first time you use an organization component or hook (the in-dashboard prompt shipped November 24, 2025), which is typically the fastest way to discover the toggle.

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.

> A customer's first question when you propose EASIE will usually be: "Does this mean you trust every Google tenant?" The honest answer is yes — with domain verification as the boundary. If they require per-tenant cryptographic isolation (FAL2+, financial-services audits, "no shared trust roots"), go straight to SAML.

### Step 2: Model customers as Organizations

Enterprise SSO in Clerk is organization-scoped. That means the [`orgId`](https://clerk.com/glossary.md#organizations) 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()`:

```ts
// 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).

> `clerkClient` in `@clerk/nextjs` v7 / Core 3 is an **async factory**. You must await it. Direct property access like `clerkClient.enterpriseConnections.createEnterpriseConnection(...)` will fail at runtime with `TypeError: clerkClient.enterpriseConnections is undefined`. Always invoke the factory: `(await clerkClient()).enterpriseConnections...`. Teams migrating from `@clerk/nextjs` v5 get bitten by this because TypeScript may not flag the legacy shape.

Here's a complete server route that creates a SAML connection on behalf of an Organization admin:

```ts
// 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.

> `clerkClient.samlConnections` is now the legacy surface as of the March 9, 2026 unification. The methods still function and continue to hit `/saml_connections`, but new code should target `enterpriseConnections`. The two create signatures are incompatible — the legacy `CreateSamlConnectionParams` shape is flat with SAML fields at the top level and requires `provider: SamlIdpSlug` and a singular `domain: string`, while the new `CreateEnterpriseConnectionParams` nests protocol-specific fields under `saml: { ... }` or `oidc: { ... }` and uses `domains: string[]`. This is a migration, not a drop-in rename. See the [March 9, 2026 Backend API changelog entry](https://clerk.com/changelog/2026-03-09-bapi-enterprise-connections.md) for the full migration matrix.

### 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

1. In the Okta admin console, go to **Applications** → **Applications** → **Create App Integration** → **SAML 2.0**.
2. On the **General Settings** page, give the app a name ("Your SaaS") and optionally upload a logo.
3. On the **Configure SAML** page, paste the ACS URL and Entity ID from Clerk's Dashboard. Leave `Name ID format` at `EmailAddress` and `Application username` at `Email`.
4. Under **Attribute Statements**, add:
   - `email` → `user.email`
   - `firstName` → `user.firstName`
   - `lastName` → `user.lastName`
5. On the **Feedback** page, select **I'm an Okta customer adding an internal app**.
6. After the app is created, open the **Sign On** tab, click **View Setup Instructions**, and copy the Identity Provider metadata URL.
7. 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

1. In the Azure portal, go to **Microsoft Entra ID** → **Enterprise applications** → **New application** → **Create your own application** → **Non-gallery**.
2. Inside the new app, go to **Single sign-on** → **SAML**.
3. Under **Basic SAML Configuration**, paste the Reply URL (this is Clerk's ACS URL) and the Identifier (this is Clerk's Entity ID).
4. 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:

```text
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
```

5. Under **SAML Certificates**, copy the **App Federation Metadata Url** and paste it into Clerk's **IdP metadata URL** field.

> Entra ID's "add custom claims" UI has a subtle quirk: if you paste a custom attribute (like a department code) and Entra stores it as a typed value instead of a string, Clerk's SAML response parser will reject it. Cast custom attributes to strings explicitly in the Entra claim transformation.

#### Google Workspace walkthrough

1. 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.
2. Give the app a name. On the Google IdP details page, copy the **SSO URL**, **Entity ID**, and download the X.509 certificate.
3. On the **Service provider details** page, paste Clerk's ACS URL and Clerk's Entity ID. Set **Name ID format** to `EMAIL` and **Name ID** to `Basic Information > Primary email`.
4. Under **Attribute mapping**, map:
   - `Primary email` → `mail`
   - `First name` → `firstName`
   - `Last name` → `lastName`
5. 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:

```ts
// 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](https://clerk.com/changelog/2026-04-16-directory-sync.md). 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](https://clerk.com/docs/guides/configure/auth-strategies/enterprise-connections/directory-sync.md), 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` → Clerk `username`
- `email.value` → Clerk primary email
- `name.givenName` → Clerk `first_name`
- `name.familyName` → Clerk `last_name`
- `active` → Clerk user enabled/disabled flag

#### Events

SCIM events fire as Clerk `user.created` and `user.updated` webhooks, plus dedicated SCIM webhook events:

- `scim.user.created`
- `scim.user.patched`
- `scim.user.deleted`
- `scim.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.

1. **Custom attribute mapping into `publicMetadata`.** Sync IdP attributes like `department`, `employee_id`, or `cost_center` into the user's `publicMetadata`. While Directory Sync is enabled, these fields become read-only in Clerk — the IdP is the system of record.
2. **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 `admins` and `viewers` groups, 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

```sh
pnpm add @clerk/nextjs@^7
```

#### Environment 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:

```sh
# .env.local
CLERK_PUBLISHABLE_KEY=pk_test_xxxxxxxxxxxxxxxxxxxx
CLERK_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxx
```

Use `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.

```ts
// 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.

| State                                                         | `auth.protect()` response                |
| ------------------------------------------------------------- | ---------------------------------------- |
| Authenticated + authorized                                    | Returns `Auth` object; request continues |
| Authenticated + unauthorized (wrong role, permission, or org) | Thrown 404 (not 403, not JSON)           |
| Unauthenticated, document request                             | Redirect to sign-in URL                  |
| Unauthenticated, session-token API request                    | Thrown 404                               |
| Unauthenticated, machine-token API request                    | Thrown 401                               |

Mitigations for SAML-protected enterprise routes:

- Pass `unauthenticatedUrl` and `unauthorizedUrl` explicitly 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 (`orgId` is null), redirect to an org-selection CTA like `/select-org` or 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. Use `unauthorizedUrl: '/request-sso'` when `has()` 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-checks`](https://clerk.com/docs/guides/secure/authorization-checks.md) guide contrasts the two: `auth.protect()` "doesn't offer control over the response" while `has()` 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.

```tsx
// 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:

```tsx
// 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:

```tsx
// 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:

```ts
// 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.

1. Add a custom `<OrganizationProfile.Page label="SSO" url="sso">` tab to the `<OrganizationProfile />` component for organization admins.
2. 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).
3. On submit, POST to your own API route — which is the `app/api/sso/create/route.ts` example from Step 3.
4. The API route calls `(await clerkClient()).enterpriseConnections.createEnterpriseConnection({ saml: { ... } })` on behalf of the Organization admin.
5. 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:

```tsx
// 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.

> If a turnkey white-labeled admin portal is genuinely a hard requirement — enterprise customer IT teams who will never accept a vendor's React form — the right architectural answer is WorkOS Admin Portal alongside Clerk for that narrow flow. Use Clerk for the user, session, and org model; use WorkOS Admin Portal for the customer-IT-facing SSO configuration step. Few teams need this, but it's worth naming the option.

### 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-saml`** Docker 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_`, not `pk_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 any `boxyhq/mock-saml` `PRIVATE_KEY` must be per-environment and managed through a secrets manager (Vercel env, Doppler, Infisical). Never commit `.env` files 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:**

1. 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.
2. No production connection trusts an X.509 certificate that was copy-pasted from a mock IdP or staging IdP.
3. No production connection's claimed email domain resolves to a test or disposable domain you do not own (`*.example`, `*.test`, `*.localhost`).
4. Every mock-IdP container spun up during testing has been torn down, and the hostnames no longer resolve.
5. All `boxyhq/mock-saml` `PRIVATE_KEY` and signing-key env vars have been rotated out of production secrets managers and no longer appear in any running deployment.
6. 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

| Error                                  | Likely cause                                                   | Fix                                                                            |
| -------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `Invalid Signature`                    | Certificate rotation mismatch; cert in Clerk is stale          | Re-download IdP metadata; Clerk auto-pulls if you used metadata URL            |
| `Audience mismatch`                    | Entity ID wrong in IdP config                                  | Compare Clerk's Entity ID to IdP's `audienceRestriction` element               |
| `NotOnOrAfter exceeded`                | Clock skew between SP and IdP, or assertion replay             | Sync server clocks via NTP; check for replay in logs                           |
| `AssertionConsumerServiceURL mismatch` | ACS URL wrong in IdP config                                    | Compare Clerk's ACS URL (copy-paste) to IdP's Reply URL                        |
| `Signature element not found`          | IdP is signing the Response, not the Assertion (or vice versa) | In Clerk dashboard, flip "Expect signed response" vs "Expect signed assertion" |

## 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:

```ts
// 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:

```ts
// 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:

```ts
// 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`:

```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).*)'],
}
```

> Auth0's Next.js v4 SDK does **not** support SAML IdP-initiated flows as of v4.18.0 (published April 17, 2026). The library implements OpenID Connect, which has no native IdP-initiated concept (see the long-standing thread at [`auth0/nextjs-auth0#261`](https://github.com/auth0/nextjs-auth0/issues/261), closed February 2021 and unresolved in-SDK since). An Auth0 maintainer recommends in that thread a silent-authentication portal app using `prompt=none` — pointing at Auth0's silent-authentication docs — rather than native IdP-initiated SAML. Auth0's SAML platform itself supports IdP-initiated SSO (see the [Auth0 IdP-initiated docs](https://auth0.com/docs/authenticate/protocols/saml/saml-sso-integrations/identity-provider-initiated-single-sign-on), configurable at Dashboard → Authentication → Enterprise → SAMLP by setting "IdP-Initiated SSO Behavior" to "Accept Requests"; Auth0's own docs flag IdP-initiated as "not recommended" due to the Login CSRF exposure). The gap is specifically the Next.js SDK. Clerk and WorkOS handle IdP-initiated natively (with the security caveats from Section 9).

### 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.

```ts
// 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 authorizationUrl
```

The callback handler exchanges a code for a profile:

```ts
// 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.idpId
```

WorkOS'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

```ts
// 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:

```ts
// 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

| Feature                                      |         Clerk        |                   Auth0                   |           WorkOS           |      DIY (Node libs)     |
| -------------------------------------------- | :------------------: | :---------------------------------------: | :------------------------: | :----------------------: |
| Free MAU                                     |        50,000        |                   25,000                  |        1M (AuthKit)        |            N/A           |
| SAML connections on free tier                |           0          |            1 (Feb 2026 update)            |              0             |            N/A           |
| SAML connections on paid tier                |        1 (Pro)       | 3 (B2B Essentials) / 5 (B2B Professional) | 0 (per-connection billing) |   Unlimited (you host)   |
| Per-additional-connection price              |        $75/mo        |        $100/mo past Essentials cap        |           $125/mo          |       $0 (you host)      |
| SCIM / Directory Sync                        |   Yes GA April 2026  |          Yes Free tier (Feb 2026)         | Yes $125/mo per connection |         You build        |
| Self-serve SSO configuration for customer IT |     Custom pages     |            Ticket-URL assistant           |      Yes Admin Portal      |         You build        |
| Next.js 16 `proxy.ts` native                 |          Yes         |                    Yes                    |             Yes            |         You build        |
| IdP-initiated SSO supported in SDK           | Yes (off by default) |              No (Next.js SDK)             |             Yes            |         You build        |
| Multi-tenant (per-org) SSO model             |          Yes         |                    Yes                    |             Yes            |         You build        |
| Time to first production SAML                |       1–3 hours      |                 1–2 weeks                 |   1 day with Admin Portal  |       4–8 weeks MVP      |
| 2026 CVE exposure                            |    Vendor-managed    |               Vendor-managed              |       Vendor-managed       | 5+ critical in 12 months |

### 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_connections` API + 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-saml` and 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](https://clerk.com/glossary.md#account-linking) 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:

1. **A customer tenant spans multiple email domains** (e.g. `acme.com` primary plus `acme.co.uk` UK subsidiary). Use Clerk's June 2025 "multiple domains per SSO connection" feature to attach all domains to one Organization's single connection.
2. **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 `RelayState` round-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

## FAQ

### What is the difference between SSO and SAML?

SSO (Single Sign-On) is the goal: one login session unlocks multiple applications. SAML (Security Assertion Markup Language) is one of the protocols that implements SSO — specifically the 2005 XML-over-HTTP standard that dominates workforce enterprise IT. The other main SSO protocol is OIDC (OpenID Connect), a 2014 JSON-over-HTTP standard. You can have SSO without SAML (using OIDC), but when enterprise procurement says "we need SSO," they typically mean SAML 2.0 specifically because most enterprise workforce RFPs name it by protocol.

### Can I use OAuth or OIDC instead of SAML for enterprise SSO?

Technically yes, but practically it depends on the customer. If the customer's IdP is Okta Workforce, Ping, ADFS, JumpCloud, or OneLogin and their admins only speak SAML, OIDC is not a real option — their procurement will ask for SAML by name. If the customer is on Google Workspace or Microsoft Entra and has not specified a per-tenant isolation requirement, multi-tenant OIDC (EASIE) is the fastest path. NIST SP 800-63C (final, July 31, 2025) treats SAML and OIDC as equivalent at every Federation Assurance Level — the FAL you qualify for depends on assertion protections (injection protection, audience restriction, encrypted assertions, and at FAL3 holder-of-key binding), not the format. The "SAML is more secure" folklore is not supported by current guidance — the choice is driven by what the customer asks for.

### SAML vs OIDC for SaaS: which should I support first?

Ship SAML first. Enterprise workforce procurement overwhelmingly names SAML by protocol, and customer IdPs like Okta Workforce, Ping, ADFS, JumpCloud, and OneLogin expect SAML as the primary federation option. Adding OIDC or EASIE second is straightforward once SAML is in place. If your first enterprise deal is specifically a Google Workspace tenant, EASIE can be faster than SAML — but plan on supporting both within the first year because workforce SSO is SAML-first and SMB or mobile deals skew OIDC.

### How long does it take to add SAML SSO to a SaaS product?

With a managed auth service like Clerk: 1–2 days for the first connection including customer IdP round-tripping, and 1–3 hours via the /enterprise\_connections API once you have the pattern down. With Auth0: 1–2 weeks including Actions customization. With WorkOS and Admin Portal: 1 day (customer admin does most of the work via the magic link). Building SAML from scratch with @node-saml/passport-saml or samlify: 4–8 weeks for an MVP and 3–9 months for production-hardened code, because admin UI, cert-rotation handling, and the 2025–2026 CVE patch load dominate the timeline.

### Should I build SAML from scratch or use an authentication service?

For most B2B SaaS, buy. SSOJet's illustrative DIY SSO cost breakdown puts year-one cost at roughly $27,000 in engineering plus $10,000 per year in maintenance plus $40,000 in opportunity cost plus $20,000 in security and compliance reserve plus $15,000 in sales and support drag — about $112,000 for year one, scaling to $150,000+ when timelines slip. That breakdown is vendor math, but the shape is right: a production-hardened SAML stack is several quarters of sustained work once admin UI, cert rotation, and the 2025–2026 CVE patch load are factored in. The cases where DIY makes sense are regulated industries with on-premises-only identity requirements, teams with existing identity specialists, and open-source products with philosophical commitments. Everyone else: managed service.

### What is SCIM provisioning and do I need it alongside SAML?

SCIM (System for Cross-domain Identity Management) is the REST protocol IdPs use to create, update, and delete users in your app proactively. SAML creates users on first sign-in (JIT provisioning); SCIM handles everything after — especially deprovisioning when an employee leaves. Enterprise procurement increasingly mandates SCIM alongside SAML because SAML alone leaves orphaned accounts when someone is offboarded mid-session. Smaller customers (fewer than 25 seats) often accept JIT-only; ask your first enterprise customer what their deprovisioning SLA is. HIPAA-aligned best practice is a 1-hour revocation SLA from IdP deactivation to session termination.

### Which identity providers should I support first?

Okta and Microsoft Entra ID (formerly Azure AD) cover the vast majority of enterprise deals. Okta reports around 41% of the public IAM market on 6sense's tech-graph dashboard, and Microsoft disclosed 610M+ Entra ID monthly active users in its FY23 Q4 earnings. Google Workspace is a close third and is often where you'll see EASIE adopted instead of full SAML. If you can only ship three IdP integrations: Okta, Entra ID, Google Workspace. Generic SAML (via an `saml_custom` or equivalent connection type) covers the long tail — Ping, OneLogin, JumpCloud, ADFS — with the same code path.

### How do I handle multi-tenant SAML SSO for different customer organizations?

The universal pattern is per-organization SAML connections with email-domain-based Home Realm Discovery. Each customer tenant has its own SSO connection scoped to their Organization record; when a user enters their work email on your sign-in page, you look up which Organization owns the email domain and route the flow to that Organization's connection. Clerk, Auth0, and WorkOS all model this with an Organizations primitive. Avoid building per-customer dedicated SP metadata or per-customer subdomain-based routing — both are brittle and break when a customer onboards a subsidiary.

### What is Just-in-Time (JIT) provisioning and how is it different from SCIM?

JIT provisioning creates a user in your app the first time they successfully sign in via SAML — no pre-provisioning required. SCIM (Directory Sync) pushes user creates, updates, and deletes proactively from the IdP as they happen in the customer's HR system. JIT covers the "new employee onboarded" case; SCIM covers the "employee offboarded" case. Most mature B2B SaaS implementations use both: JIT for sign-in-driven creation, SCIM for lifecycle management including deprovisioning.

### How do I test a SAML integration without a real identity provider?

Use a hosted mock IdP — MockSAML.com (BoxyHQ/Ory), SAMLTest.id (Shibboleth), or DummyIDP.com — for quick manual tests. For CI and air-gapped testing, run `boxyhq/mock-saml` or Keycloak in Docker. Keep mock IdPs strictly isolated from production: only test against Development Clerk instances (pk\_test\_\* keys), never paste a mock IdP's X.509 cert into a production tenant, and run a pre-launch cleanup checklist to ensure no production connection points at a mock entityID before go-live. A mock IdP reachable from production is effectively an authentication bypass.

### What are the most common SAML security pitfalls to avoid in 2026?

XML Signature Wrapping (XSW), parser differentials, missing InResponseTo validation, clock skew on NotBefore/NotOnOrAfter, XXE injection via unsafe XML parsers, silent certificate rotation, and accepting unsolicited IdP-initiated assertions. The Node.js SAML ecosystem had five critical-severity CVEs in the last twelve months: SAMLStorm ([CVE-2025-29774](https://nvd.nist.gov/vuln/detail/CVE-2025-29774) / [CVE-2025-29775](https://nvd.nist.gov/vuln/detail/CVE-2025-29775)), ruby-saml parser differentials ([CVE-2025-25291](https://nvd.nist.gov/vuln/detail/CVE-2025-25291) / [CVE-2025-25292](https://nvd.nist.gov/vuln/detail/CVE-2025-25292)), samlify ([CVE-2025-47949](https://nvd.nist.gov/vuln/detail/CVE-2025-47949), CVSS 9.9), and two node-saml CVEs ([CVE-2025-54369](https://nvd.nist.gov/vuln/detail/CVE-2025-54369) and [CVE-2025-54419](https://nvd.nist.gov/vuln/detail/CVE-2025-54419)). If you DIY, pin to known-safe minimum versions — xml-crypto ≥6.0.1, @node-saml/node-saml ≥5.1.0, samlify ≥2.10.0 — and maintain a SAML CVE runbook. Managed services absorb this cycle on your behalf.

### How do I deprovision users when they leave a customer's company?

Use SCIM (Directory Sync). When the employee is deactivated in the customer's IdP (Okta, Entra ID, Google Workspace), a SCIM PATCH with `active: false` fires to your app within seconds. Your app invalidates the user's sessions and revokes any org memberships. Without SCIM, you rely on session timeouts — a working session cookie can outlive the employee's HR record by hours or days, which is an audit finding for SOC 2 CC6.2 and a hard blocker for HIPAA-regulated customers who expect 1-hour revocation SLAs. Clerk bundles Directory Sync with every enterprise connection at no extra charge; Auth0 includes Inbound SCIM across every plan (including Free after the February 12, 2026 upgrade); WorkOS charges $125/month per Directory Sync connection separately.

### Should I charge extra for SSO, or include it in every plan?

The procurement-friendly answer is to bundle a reasonable number of SSO connections into your mid-tier plan and charge for scale or per-connection overages beyond that — not for the feature itself. The "SSO tax" pattern (charging 5–50× premium for SSO-equipped tiers) is on the sso.tax catalog and will cost you deals. Counter-example: Tailscale walked back their SSO tier pricing in April 2024 with their CEO's public note "The SSO tax felt like a mistake." Tuple CEO Ben Orenstein: "SSO costs close to nothing after a little automation." Include SSO in the plan that your enterprise buyers are already on; charge overages only when a customer is onboarding 10+ subsidiary IdPs.

### How do I configure the SAML Assertion Consumer Service (ACS) URL?

Your managed auth service (Clerk, Auth0, WorkOS) generates the ACS URL automatically and displays it in their dashboard when you create a SAML connection. You copy it into the IdP's SAML app configuration — Okta calls it the "Single sign on URL," Entra ID calls it the "Reply URL," Google Workspace calls it the "ACS URL." The ACS is where the IdP POSTs signed SAML assertions after the user authenticates. Common misconfiguration: pasting the Entity ID into the ACS field or vice versa — this produces an "AssertionConsumerServiceURL mismatch" error that's easy to diagnose once you know to compare the two values in the IdP config against the managed service's dashboard.

### Can enterprise customers self-serve their SAML configuration?

WorkOS Admin Portal is the most mature self-serve option — a hosted, white-labeled UI that customer IT admins access via a 5-minute magic link. Auth0 shipped Self-Service SSO in the February 12, 2026 B2B plan upgrade: a ticket-URL-driven guided flow that the Auth0 customer sends to their enterprise customer's admin, who then picks an IdP, configures the connection, and tests it before activation. It is a one-time guided assistant rather than a standing white-labeled portal. Clerk does not ship an equivalent turnkey portal as of April 2026; the workaround is a custom OrganizationProfile.Page that collects IdP metadata and POSTs to a server route calling `(await clerkClient()).enterpriseConnections.createEnterpriseConnection(...)`. If turnkey self-serve is a hard customer requirement, WorkOS still wins this comparison; Auth0 has closed a real gap; and Clerk remains custom-implementation territory.

### How does B2B SaaS SSO implementation differ from consumer SSO?

Consumer SSO (Login with Google, Login with Apple, Login with GitHub) is typically a single IdP connection shared across all users. B2B SSO is per-organization: each customer tenant has its own SAML or OIDC connection pointing to that customer's IdP, routed by email domain via Home Realm Discovery. The data model is different too — B2B SaaS models customers as Organizations with members and roles, where consumer apps model users as individuals. SCIM provisioning is a B2B-specific concern that rarely applies to consumer. And procurement checklists (RFPs, security questionnaires, DPAs, audit evidence) are B2B-only and drive most of the protocol and feature choices.

### What happens if an identity provider is down?

Sign-ins fail for that customer tenant until the IdP recovers. This is a visible outage your customer will file tickets about. Mitigations: (1) keep a break-glass admin account in your app that bypasses SSO — a non-federated account with credentials in a physical safe, used only when the IdP is down AND no SSO admin is currently signed in; (2) if your app caches sessions, users who are already signed in stay signed in until their session expires; (3) have a status page that calls out "SSO depends on the customer IdP" so support knows to route IdP-outage tickets back to the customer's IT team. PagerDuty's published break-glass pattern is the reference implementation. Most managed auth services (Clerk included) do not and cannot act as a fallback IdP — the auth layer can't mint federation assertions on behalf of a down IdP.

### What does the April 2026 Clerk Directory Sync GA mean for my existing SAML implementation?

Directory Sync went GA on April 16, 2026, after a public beta period. If you already have SAML connections live with Clerk and JIT provisioning, you can enable Directory Sync on each Organization without disrupting existing users — the SCIM token is generated per-organization in the dashboard, pasted into the customer's IdP provisioning settings, and starts syncing on the IdP's next provisioning cycle. Directory Sync is included with each enterprise connection at no additional charge. Two public-beta sub-features to be aware of: custom attribute mapping into publicMetadata, and groups-to-role mapping with precedence ordering. Both are stable enough for production for most teams but may still see API tweaks.

## 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](https://clerk.com/docs/guides/configure/auth-strategies/enterprise-connections/overview.md) — product-level overview of SAML, OIDC, and EASIE in Clerk.
- [Clerk Directory Sync docs](https://clerk.com/docs/guides/configure/auth-strategies/enterprise-connections/directory-sync.md) — configuration, attribute mapping, and role mapping.
- [Clerk Directory Sync GA changelog, April 16, 2026](https://clerk.com/changelog/2026-04-16-directory-sync.md) — GA announcement and public-beta sub-feature notes.
- [Clerk Backend API unification changelog, March 9, 2026](https://clerk.com/changelog/2026-03-09-bapi-enterprise-connections.md) — `/enterprise_connections` endpoint migration.
- [Clerk authorization checks guide](https://clerk.com/docs/guides/secure/authorization-checks.md) — `auth.protect()` vs `has()` trade-offs.
- [OWASP SAML Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/SAML_Security_Cheat_Sheet.html) — the single best DIY reference.
- [NIST SP 800-63-4](https://pages.nist.gov/800-63-4/) (final, July 31, 2025) — federation assurance levels, phishing-resistant MFA guidance.
- [OASIS SAML 2.0 specifications](http://docs.oasis-open.org/security/saml/v2.0/) — Core, Bindings, Profiles, Metadata.
- [IETF RFC 7643](https://datatracker.ietf.org/doc/html/rfc7643) / [RFC 7644](https://datatracker.ietf.org/doc/html/rfc7644) — SCIM Core Schema and Protocol.
