better-auth-saml-idp
Turn your Better Auth server into a SAML 2.0 Identity Provider, and sign your users in to Microsoft 365, Google Workspace, Okta, Salesforce, AWS and other SAML 2.0 service providers. It's a Better Auth plugin, not a separate server: it runs on Cloudflare Workers and Node 22+, with no Java and no native binaries.


Try it: the example IdP (Workers + Hono + D1) signs users in to Microsoft 365, AWS IAM Identity Center, Cloudflare Access, Okta, Auth0 and Salesforce, verified live.

📖 Read the guide for everything this page leaves out: every option, every error code, the security controls, Single Logout, Cloudflare Workers, the CLI and troubleshooting.
Unofficial community plugin. Not affiliated with or endorsed by Better Auth. Stable 1.x on npm, following semantic versioning; the CHANGELOG says what changed in each release and why.
✨ Features
- 🔐 SAML 2.0 single sign-on: SP-initiated over HTTP-Redirect and HTTP-POST, and IdP-initiated as an opt-in per SP.
- ✍️ Signed and encrypted: RSA-SHA256 signatures, AES-256-GCM encryption per SP, zero-downtime key rotation, or a key the plugin makes, stores and rotates itself (
signing: { managed: true }).
- 🛡️ Hardened input: every message checked against the OASIS schemas (libxml2 in WebAssembly, on Workers too), defences against XML signature wrapping, and replay protection by a database unique key. Fuzzed on every CI run.
- 👤 Strict identity: only verified emails get assertions, impersonated and anonymous sessions are refused, and the user and session are re-read right before signing.
- 🪜 Step-up MFA and your own rules: SPs that ask for more send the user back to sign in again (flows), and a per-SP
authorize hook can deny with a reason.
- 🚪 Single Logout in both directions, and
onSessionEnded names the SPs still signed in when a session ends without it (guide).
- 🗂️ SPs at runtime: an admin API that logs every change, SPs from their metadata, and certificates kept current from a metadata URL (guide).
- 🏢 Better Auth native: organization-scoped SPs, an IdP identity per organization, admin-plugin permissions, a typed client plugin, and tables from
npx auth generate. With @better-auth/sso in the same app it's an identity broker.
- 🎟️ Assertions for OAuth tokens: an agent can trade an assertion for tokens once, the flow MCP's Enterprise-Managed Authorization uses (guide).
- 📈 Observability: event callbacks for audit trails and SIEM, and an optional audit-log table (guide).
- 🧰 A CLI that inspects metadata, decodes captured messages, smoke-tests a deployment and generates keys (guide).
- 🗄️ Your database and runtime: PostgreSQL, MySQL, MongoDB, SQLite and Cloudflare D1, through Kysely, Drizzle and Prisma, all in CI. Workers and Node run the whole suite; Bun and Deno a smoke test. Benchmarked on Workers and D1.
⚡ Quick Start
npm install better-auth-saml-idp
npx better-auth-saml-idp keygen --cert-out idp.crt --key-out idp.key
Add the plugin to your Better Auth config, with the key and certificate loaded from your secret store:
import { betterAuth } from "better-auth";
import { samlIdp } from "better-auth-saml-idp";
export const auth = betterAuth({
plugins: [
samlIdp({
entityId: "https://auth.example.com/api/auth/saml2/idp",
baseURL: "https://auth.example.com",
loginPage: "/sign-in",
signing: {
privateKey: process.env.SAML_IDP_PRIVATE_KEY!,
certificate: process.env.SAML_IDP_CERT!,
},
serviceProviders: [
{
id: "my-app",
entityId: "<the SP's entity ID>",
acsUrls: ["<the SP's ACS URL>"],
attributes: { email: "email" },
},
],
}),
],
});
npx auth migrate
npx better-auth-saml-idp inspect https://auth.example.com
npx better-auth-saml-idp sp-from-metadata https://sp.example.com/saml/metadata --id my-app
npx better-auth-saml-idp smoke https://auth.example.com --sp <the SP's entity ID>
Then give the SP your metadata URL, https://auth.example.com/api/auth/saml2/idp/metadata. The getting started guide walks through each step, including the sign-in page and the defaults to know about.
On Cloudflare Workers, put samlIdp() inside withCloudflare's second argument: a plugins: [...] next to ...withCloudflare(...) replaces the Cloudflare plugin and silently disables its storage validation. Or skip the key step with signing: { managed: true }. See Cloudflare Workers.
Examples: Workers + Hono + D1 (a complete, deployable IdP, the one running the demo), Next.js on Node, and Workers + Neon on Postgres through Hyperdrive.
Requirements
better-auth and @better-auth/core >=1.7.8 <1.8.0 (peer dependencies). Better Auth 1.7.7 fixed three security advisories, a critical Magic Link account takeover among them, and 1.7.8 a SAML replay check in @better-auth/sso; see versioning.
- A real database. Replay protection depends on UNIQUE constraints, which Better Auth's in-memory adapter doesn't enforce.
- With secondary storage (for example KV):
verification: { storeInDatabase: true }. On Workers also rateLimit: { storage: "database" }, better-auth-cloudflare and the nodejs_compat flag.
- Next.js:
serverExternalPackages: ["better-auth-saml-idp"] in next.config. The plugin reads its XSD validator from its own package, which a bundled copy can't.
✅ Verified with
Live tests are run by hand; each has its date and evidence in DECISIONS.md.
| Microsoft 365 (Entra ID federation) | Sign-in to My Apps with a persistent NameID; Microsoft took the IdP's MFA claim; signing out at Microsoft ended the IdP session | guide |
| Google Workspace | Sign-in to Google, with users created and suspended by better-auth-scim-provisioning | guide |
| Okta | Signed requests, plain and encrypted assertions, SP stored in the registry | guide |
| Auth0 | Signed requests over Redirect and POST, encrypted assertions | guide |
| Salesforce | Signed requests required, NameID matched to the Federation ID, Single Logout both ways | guide |
| AWS IAM Identity Center | The access portal and the AWS console in a permission set | guide |
| Cloudflare Access | Signed requests, encrypted assertions, metadata refresh, key rotation | guide |
| HubSpot | Guide written, not yet run live | guide |
In CI: Keycloak 26.8 and SimpleSAMLphp 2.5 in real Chromium over HTTPS; node-saml, samlify and @better-auth/sso on Node and workerd, including an identity broker; PostgreSQL 17, MySQL 8.4, MongoDB 8.2, SQLite and D1, through Kysely, Drizzle and Prisma. Testing with other SPs covers the rest.
📚 Documentation
| Set it up step by step | Getting started |
| See every option | Options |
| Add SPs: in code, from metadata, at runtime | Service providers |
| Map attributes and NameIDs, limit SPs to organizations, write access rules | Users and access |
| Choose what's signed, encrypt assertions, rotate keys | Signing and encryption, key rotation |
| Understand the endpoints and flows, IdP-initiated SSO, step-up | Flows |
| Turn on Single Logout | Single Logout |
| Give each organization its own IdP | Multi-tenant |
| Provision accounts at the SPs too | Sign-in and provisioning |
| Fix an error | Errors, troubleshooting |
| Check the security model | Security controls, threat model |
The guide's index lists every page, including databases, schema, observability, the CLI and @better-auth/sso interop.
📊 How it compares
A feature-by-feature comparison with Shibboleth, SimpleSAMLphp, Keycloak, Zitadel, authentik, Logto, Ory Polis, Entra ID, Okta, Google Workspace and Auth0, with sources: interactive comparison (or docs/comparison.md).

🧭 What's next
The 1.x line covers everything on the roadmap derived from a feature comparison with eleven other SAML identity providers (checked 2026-10-15).
Not planned:
- 🚫 Artifact binding: Of the products compared, only Keycloak supports it fully (Shibboleth and SimpleSAMLphp for responses); no target SP needs it.
- 🚫 Admin UI in the plugin: Better Auth plugins are headless; hosts build their own UI on the registry API, and the example has a reference page to copy.
What shipped in each release is in the CHANGELOG.
🔒 Security

- docs/security.md: the threat model, what your host must do (don't use KV for sessions; leave
cookieCache off), and known limitations.
- DECISIONS.md: every security-relevant design decision, with its tests and mutation proofs.
- SECURITY.md: how to report a vulnerability privately (not in a public issue), and the supply chain: actions pinned by SHA, CodeQL, dependency audits, OSV-Scanner, OpenSSF Scorecard, and npm provenance and a CycloneDX SBOM with every release.
🧪 Development
pnpm test
pnpm test:wasm
pnpm e2e
pnpm lint && pnpm typecheck && pnpm pack:check
CI runs these on Node 24 with Better Auth 1.7.8 and the latest 1.7.x, and on Node 22 with 1.7.8, plus a gitleaks scan, the database matrix and the Next.js example. CONTRIBUTING.md has the rest.
License and contributing
MIT © Mark McIntosh. Third-party components (libxml2 in wasm/xsd.wasm, and the OASIS/W3C schemas) are listed in THIRD_PARTY_NOTICES.md.
Contributions are welcome: bug reports, SP interop reports (especially "it works with X"), fixes and features. For anything significant, please open an issue first; see CONTRIBUTING.md. If the plugin is useful to you, a ⭐ helps others find it.