SAML SSO usually enters the roadmap through one innocent enterprise prospect asking, "Do you support Okta?" Everyone smiles. Someone says, "Probably easy." Then you meet metadata XML, ACS URLs, certificate rotation, tenant-specific domains, and a checkbox in an IdP admin screen that somehow controls your quarterly revenue.
This guide is about building SAML SSO for a B2B SaaS product without turning authentication into a haunted basement. It pairs naturally with Designing Multi-Tenant SaaS Isolation: Data, Controls, and Cost Guardrails, Implementing Role-Based Access Control (RBAC) in Next.js App Router, Designing Audit Logs for SaaS: Evidence, Security, and Product Trust, and Secrets Management in Modern Infrastructure Using Vault or SSM.
TL;DR
SAML SSO is a tenant-scoped trust relationship. Treat it like security infrastructure, not a login button with extra XML.
- Store SAML configuration per tenant, never globally.
- Let a battle-tested library or identity broker validate XML signatures.
- Bind login state to tenant, redirect target, and short expiration.
- Map identities and IdP groups explicitly.
- Log setup, login failures, enforcement, and certificate rotation.
What SAML SSO Actually Solves in a SaaS App
SAML SSO lets a company use its own identity provider, such as Okta, Microsoft Entra ID, or Google Workspace, to authenticate users into your product. The customer controls employee lifecycle. You control product access. That split is the whole point.
Without SSO, your app owns password resets, offboarding risk, and "I left the company six months ago but still have access" incidents. With SSO, the customer disables the user in their IdP and the next login fails. Not glamorous, but very enterprise.
SAML does not replace the tenant model from Designing Multi-Tenant SaaS Isolation: Data, Controls, and Cost Guardrails. It sits on top of it. A SAML assertion should land inside exactly one tenant context, then your app enforces permissions using the same discipline described in Implementing Role-Based Access Control (RBAC) in Next.js App Router.
Short version: SAML says "this person is Alice from Acme." Your authorization layer says "Alice can manage billing, but cannot delete production data because we enjoy sleeping."
SP-Initiated vs IdP-Initiated Login
SP-initiated login starts in your app. IdP-initiated login starts in the customer's identity provider.
Prefer SP-initiated login as your default. It gives you cleaner state handling, a known tenant, and a redirect path you generated. IdP-initiated login is useful for customers who live inside Okta dashboards all day, but it is easier to misconfigure because the request did not start with your app.
| Flow | Best for | Main risk |
|---|---|---|
| SP-initiated | Normal app login, tenant-aware redirects | Broken state validation |
| IdP-initiated | IdP dashboards and bookmarks | Tenant guessing and loose RelayState handling |
For both flows, validate the final assertion against tenant-specific configuration. Do not accept a valid SAML response from Tenant A and log the user into Tenant B. That is not SSO. That is a cross-tenant escape room with legal consequences.
Store SAML Configuration Per Tenant
Every enterprise customer has its own IdP metadata, certificate, entity ID, SSO URL, and attribute mapping. Put those settings behind a tenant boundary.
A practical data shape looks like this:
// lib/sso/types.ts
export type SamlProviderConfig = {
tenantId: string;
entityId: string;
ssoUrl: string;
x509Certificate: string;
emailAttribute: string;
groupsAttribute?: string;
isEnabled: boolean;
};
export type VerifiedSamlProfile = {
tenantId: string;
email: string;
displayName?: string;
externalId: string;
groups: string[];
};Store the certificate as sensitive configuration. It is public-ish in the SAML sense, but operationally it belongs near integration credentials. The rotation and access patterns from Secrets Management in Modern Infrastructure Using Vault or SSM apply well here, especially if customers upload metadata through an admin screen.
Also audit configuration changes. A new SSO URL, disabled enforcement, or replaced certificate should create events like sso.config_updated and sso.certificate_rotated. The product evidence model from Designing Audit Logs for SaaS: Evidence, Security, and Product Trust is perfect for this.
Validate SAML Responses, Do Not Parse XML Like a Cowboy
The direct answer: use a maintained SAML library or an identity broker. Do not hand-roll XML signature validation.
SAML has sharp edges: canonicalization, signed assertions, audience restrictions, clock skew, replay protection, and certificate rollover. If you parse XML with a hopeful regex, production will eventually send you a calendar invite titled "Incident review."
Your Next.js ACS route should be boring. Receive the POST, load tenant config, delegate validation, then create a session.
// app/api/sso/saml/acs/route.ts
import { NextRequest, NextResponse } from "next/server";
import { createSessionForSamlUser } from "@/lib/auth/session";
import { resolveTenantFromRelayState, verifyRelayState } from "@/lib/sso/state";
import { loadSamlConfig, verifySamlResponse } from "@/lib/sso/saml";
export const POST = async (request: NextRequest) => {
const formData = await request.formData();
const samlResponse = formData.get("SAMLResponse");
const relayState = formData.get("RelayState");
if (typeof samlResponse !== "string" || typeof relayState !== "string") {
return NextResponse.json({ error: "Invalid SAML callback" }, { status: 400 });
}
const state = verifyRelayState({ relayState });
const tenantId = resolveTenantFromRelayState({ state });
const config = await loadSamlConfig({ tenantId });
if (!config?.isEnabled) {
return NextResponse.json({ error: "SSO is not enabled" }, { status: 403 });
}
const profile = await verifySamlResponse({ samlResponse, config });
const session = await createSessionForSamlUser({ profile, redirectTo: state.redirectTo });
return NextResponse.redirect(session.redirectUrl);
};The order matters: validate state, resolve tenant, load tenant config, verify assertion, create session. Change that casually and you invite bugs wearing a fake badge.
RelayState Needs More Respect Than It Gets
RelayState carries the context from the login request to the callback. Treat it like a signed, short-lived login intent, not a friendly suggestion from the browser.
At minimum, RelayState should contain a tenant ID, an internal redirect path, and an expiration timestamp. Sign it with HMAC, reject expired values, and normalize redirects to paths like /dashboard. This is the same tenant-first instinct as Designing Multi-Tenant SaaS Isolation: Data, Controls, and Cost Guardrails. Tenant identity is not decorative metadata. It is part of the security boundary.
Identity Mapping: Email Is Useful, But Not Magic
After validation, you still need to map the SAML profile to a user in your database. Email is the common identifier, but it is not sacred scripture.
Use a stable external ID when the IdP provides one. Keep email for display and fallback matching during first setup. If a user changes their email after marriage, acquisition, or a heroic attempt to escape firstname.lastname.7, you do not want a duplicate account.
Good mapping rules:
- Match existing linked identities by
providerUserId. - On first login, match by verified email inside the tenant.
- Require an allowed domain or invitation before auto-provisioning.
- Never let SAML attributes bypass product authorization.
This is where Implementing Role-Based Access Control (RBAC) in Next.js App Router matters again. IdP groups can suggest roles, but your app should still translate them into explicit permissions.
Should SAML Groups Become Product Roles?
The short answer: maybe, but only through an admin-controlled mapping table. Letting an IdP group named Admins automatically become your app's owner role sounds convenient until two departments both have an Admins group.
Store mappings as tenant-owned rules: samlGroup, appRole, createdBy, and updatedAt. Apply the highest privilege match only after the SAML response is verified, then run normal permission checks from Implementing Role-Based Access Control (RBAC) in Next.js App Router. Keep this mapping visible in tenant admin settings and record changes in audit logs. Role mapping mistakes are "why can finance delete engineers" errors, which tend to age poorly.
Common SAML Production Pitfalls
Most SAML incidents are configuration problems disguised as authentication failures.
- Clock skew: allow a small tolerance, but do not accept ancient assertions.
- Certificate rollover: support two active certificates during rotation.
- Wrong audience: validate the assertion audience against your service provider entity ID.
- Open redirects: restrict RelayState redirects to internal paths.
- Replay attacks: store assertion IDs briefly and reject duplicates.
- Tenant confusion: bind every login attempt to one tenant and one provider.
- SSO enforcement: require SSO for tenant members only after a grace period, and keep a platform-only break-glass path protected by MFA and audit logs.
Certificate rollover deserves extra attention. Customers will rotate IdP certificates on a Friday because enterprise portals apparently know when engineers are tired. Keep old and new certificates active during a planned window, then expire the old one with an audit event.
Observability for SAML Without Leaking Sensitive Data
Log the outcome, tenant, provider, request ID, and failure category. Do not log raw assertions. XML blobs full of identity attributes do not belong in your log pipeline, Slack, or the screenshot someone attaches to a ticket named "pls help."
Useful events:
sso.login_startedsso.login_succeededsso.login_failedsso.config_updatedsso.certificate_rotatedsso.enforcement_enabled
This complements Designing Audit Logs for SaaS: Evidence, Security, and Product Trust without turning application logs into a second identity database. For certificate storage and metadata uploads, keep applying the restraint from Secrets Management in Modern Infrastructure Using Vault or SSM.
SAML SSO Implementation Checklist
- Create tenant-scoped SAML settings with entity ID, SSO URL, certificate, and attribute mapping.
- Build SP-initiated login with signed RelayState and short expiration.
- Validate SAML responses through a maintained library or identity broker.
- Map identities and groups through explicit tenant-owned rules.
- Add audit events and alerts for setup, enforcement, failures, and certificate changes.
FAQ: Questions Developers Ask After the First Customer Setup Call
Do we need SCIM with SAML?
Not immediately, but probably soon. SAML handles login. SCIM handles user provisioning and deprovisioning before login happens.
Should we build SAML ourselves or use a provider?
Use a provider if SSO is not your core business. WorkOS, Auth0, Clerk, Stytch, BoxyHQ, and similar tools can save months. Build directly only when you need deep control or strict data residency.
Conclusion
SAML SSO is not just an enterprise checkbox. It is a trust boundary between your SaaS product and the customer's identity system.
The implementation succeeds when tenant isolation is strict, assertion validation is delegated to proven tooling, identity mapping is explicit, role mapping is controlled, and every important change leaves evidence. The same production habits behind Designing Multi-Tenant SaaS Isolation: Data, Controls, and Cost Guardrails, Implementing Role-Based Access Control (RBAC) in Next.js App Router, Designing Audit Logs for SaaS: Evidence, Security, and Product Trust, and Secrets Management in Modern Infrastructure Using Vault or SSM apply here.
Build it carefully once, and SSO becomes boring. In authentication, boring is a compliment.
