better-auth-saml-idp · feature comparison

How a Better Auth SAML IdP compares with eleven others

Shibboleth, SimpleSAMLphp and Keycloak as references; Zitadel, authentik, Logto and Ory Polis as modern open-source peers; Entra ID, Okta, Google Workspace and Auth0 as the commercial IdPs SPs are tested against. Every entry comes from the product's own documentation or source code.

Checked 2026-10-1511 products · 24 features“Not documented” ≠ “no”

Where we lead

  • Replay protection you can checkAuthnRequest replay is rejected by a database unique key and tested under concurrency across separate instances. Most peers don't document replay handling for inbound requests.
  • Strict identity by defaultOnly verified email addresses get assertions; admin-impersonation sessions and anonymous users are refused. The user and session are re-read from the database right before signing.
  • Honest protocol answersIsPassive, RequestedAuthnContext, Subject and NameIDPolicy are honoured, and failures go back as signed SAML status Responses. authentik, Logto and Ory Polis show HTML errors instead.
  • Runs where your app runsA Better Auth plugin that runs on Cloudflare Workers and Node, verified live against Microsoft 365, Google Workspace, Okta, Auth0, Salesforce, AWS IAM Identity Center and Cloudflare Access. Every other self-hosted option here is a separate server.
Our column: 22 yes0 partial1 no

Where we trail

  • SP onboardingNo admin UI in the plugin (Better Auth plugins are headless): hosts build one on the registry API, starting from the example's reference page.

Each gap is on the roadmap below, in the order SPs and admins are most likely to hit it.

Feature matrix

YesPartialNoNot documentedn/aScroll sideways on small screens. Our column is shaded.
This pluginReferenceOpen sourceCommercial
Featurebetter-auth-saml-idp1.7Shibboleth IdP5.2.3SimpleSAMLphp2.5.3.1Keycloak26.8.1Zitadel4.19.5authentik2026.8.4Logto1.45, SAML appsOry PolisSAML Federation (enterprise)Microsoft Entra IDnon-gallery appsOktacustom SAML appsGoogle Workspacecustom SAML appsAuth0SAML2 Web App addon
Flows
SP-initiated SSOYesHTTP-Redirect and HTTP-POST in, POST outYesYesYesYesYesYesYesbrokered to an upstream IdPYesYesYesYes
IdP-initiated SSOYesopt-in per SP; RelayState allow-listYesYesYesper clientNoYesNoPartialOIDC-initiated onlyYesYesYesYes
Single LogoutYesSP- and IdP-initiated, front-channel propagation to every participating SP; node-saml interop; verified live with Microsoft 365Yesdocs call it best-effortPartialfront-channel onlyYesfront and back channelPartialresponds, ends nothingYesfront and back channelNoNoYesRedirect onlyYesback-channel is Early AccessNot documentedPartialno multi-app logout
Bindings
AuthnRequest over Redirect and POSTYesnode-saml's DEFLATEd POST accepted tooYesYesYesYesYesYesYesYesNot documentedNot documentedNot documented
Artifact bindingNonot plannedPartialresponses onlyPartialresponses, needs memcacheYesNoNoNoNoNoresponses always POSTNot documentedNot documentedNot documented
Signing and encryption
Response / Assertion signing choiceYesper SP, with a global default; both signed by defaultYesper SPYesper SPYesper clientNoalways bothYesper SPNoresponse onlyNoalways bothYesYesPartialone checkboxPartialone or the other
Encrypted assertionsYesAES-256-GCM + RSA-OAEP, per SP; verified live with Cloudflare Access, Okta and Auth0, and with node-saml and samlifyYeson by defaultYesper SPYesAES-256-GCM defaultNoYesYesNoYesneeds P1YesNot documentedYesvia Actions
Verifies and can require signed AuthnRequestsYesHTTP-Redirect and HTTP-POST (XSW-hardened), per SP; verified live with Cloudflare Access, Okta, Auth0 (POST) and Salesforce; node-saml POST; multiple SP certsYesYesYesPartialinstance-wide switchYesYesper app, since 1.44Nochecks the key embedded in the requestYesYesNot documentedPartialcan verify; requiring not documented
SHA-256 default, SHA-1 only by opt-inYesopt-in logs a warningYesYesYesPartialaccepts SHA-1 inboundYesPartialfixed SHA-256Yesfixed SHA-256YesYesNot documentedNoSHA-1 is the default
Signing key rotationYesa managed key rotated through the API: publish the next key, switch after 24 hours, retire, with expiry warnings; or a PEM key by hand. Zero-downtime procedure verified livePartialmanualYestwo-key rolloverYesactive and passive keysYesautomaticPartialmanualPartialone active at a timePartialsingle keyYesexpiry emailsYesYestwo certificatesPartialtenant-wide key
Signed IdP metadataYesoptional (signMetadata)Partialleft to federationsYesoptionalNot documentedYesYesNot documentedNot documentedNot documentedNot documentedNot documentedNot documented
Replay and protocol fidelity
Rejects replayed AuthnRequest IDsYesDB unique key; concurrency and cross-instance testedYesreplay cacheNot documentedNot documentedPartialstores ID, no rejection foundNot documentedPartialstores ID, no rejection foundNot documentedNot documentedNot documentedNot documentedNot documented
ForceAuthn, IsPassive, RequestedAuthnContextYesForceAuthn verified live with Cloudflare Access; RequestedAuthnContext exact, or all four comparisons on step-up levelsYesPartialYesstep-up via LoANoPartialForceAuthn onlyPartialForceAuthn onlyNoYesRequestedAuthnContext exact onlyPartialForceAuthn settingNot documentedPartialno IsPassive
SAML error status ResponsesYesNoPassive, NoAuthnContext, UnknownPrincipal, InvalidNameIDPolicyYesYesYesYesNoSSO errors are HTML pagesNoNoYesNot documentedPartialmostly error pagesNot documented
Identity and access
NameID formats per SPYesemail, persistent (per-SP HMAC), transient, unspecifiedYesYesYesNofixed; labelled email, carries usernameYesYesPartialpasses upstream NameIDYesYesPartialYes
Attribute mappingYesper SP: declarative map (JSON-friendly) or a functionYesdeclarative + scriptsYesYesmappers + scriptsPartialfixed set + ActionsYesPython mappingsYesdeclarativePartialYesYesYesYes
Per-SP access controlYesauthorize() hook, deny = no assertionYesPartialvia authproc filtersPartialvia conditional flowsYesper projectYespolicy bindingsNot documentedPartialrouting onlyYesapp assignmentYesapp assignmentYesper OU or groupNot documented
MFA and step-up for SAMLYeshost's Better Auth 2FA or passkeys; step-up via authnContext levels (prompt=login + acr_values); Microsoft 365 relying on it verified liveYesPartialvia modulesYesPartialMFA, no step-upPartialForceAuthn step-upPartialMFA, no step-upNodelegated upstreamYesConditional Access (P1)YespoliciesNot documentedPartial
Operations
Register an SP from its metadata XML or URLYesXML import (helper and CLI, also from a URL); certificates refreshed from the metadata URL, entity ID and ACS URLs pinnedYesfile, URL, MDQPartialconverter, refresh add-onYesXML import, URL for certificatesYesthe only wayPartialfile importNoNoPartialfills URLs, not certificatesNomanual fieldsNoNo
Admin UI or management API for SPsYesmanagement API over a database registry (admin-gated, audited); a reference admin page in the example, no UI in the pluginNoNoYesYesYesYesYesYesYesYesYes
Outbound SCIM provisioningYescompanion plugin better-auth-scim-provisioning: SCIM 2.0, Google Workspace and signed webhooks; verified live with Cloudflare Access and Google WorkspaceNoNoPartialpreviewNoinbound onlyYesNot documentedNoinbound onlyYesP1YesPartialcatalog apps onlyNo
Platform
Runs on serverless / edge (Cloudflare Workers)Yesverified on a live WorkerNoJava serverNot documentedNoJava serverNot documentedNot documentedNot documentedNot documentedn/aSaaSn/aSaaSn/aSaaSn/aSaaS
Embeds in an existing appYesa Better Auth pluginNoNoNoPartialGo library zitadel/samlNoNoYesnpm library (licence needed)n/aSaaSn/aSaaSn/aSaaSn/aSaaS
Licence and costMITApache-2.0LGPL-2.1Apache-2.0AGPL-3.0MIT coreMPL-2.0Enterprise licenceFree tier covers SAML SSOPaid, from $6/user/monthFree (Cloud Identity Free)Free tier, 25k MAU

Roadmap

What the comparison said to build, and what is still open.

1.0

First npm release

Ship what is built, safely.

  • Release engineering (done)dist build with type declarations, Biome lint, gitleaks, publint and Are the Types Wrong, changelog; a CI-only release workflow publishing with npm provenance and a CycloneDX SBOM.
  • Supply chain and project files (done)Every GitHub Action pinned by SHA with least-privilege tokens; CodeQL, a blocking runtime dependency audit, dependency review, OSV-Scanner, OpenSSF Scorecard and Dependabot. SECURITY.md, CONTRIBUTING.md, issue forms, and a versioning and support policy.
  • Fuzzing (done)Property-based tests (fast-check) of the signature verifier, every inbound parser and issuance. The verifier held; issuance had five kinds of characters that produced invalid or unverifiable assertions, all fixed (D-036).
  • Live SP verification (done)Cloudflare Access, Okta, Auth0, Salesforce, AWS IAM Identity Center, Google Workspace and Microsoft 365, live, with signed requests (Redirect and POST), encrypted assertions and Single Logout (D-034, D-035, D-042, D-054, D-063, D-075).
  • Observability hooks (done)onAssertionIssued, onDenied, onLogout and onSessionEnded callbacks, run in the background (waitUntil on Workers) and isolated from the flow, plus an optional audit-log table with retention that doesn't store anonymous noise (D-038).
  • better-auth-cloudflare 0.4.0 in the example (done)The plugin doesn't depend on it. The example moved from a vendored build of its main branch to 0.4.0 from npm (released 2026-10-12). The verification and rate-limit settings stay, since 0.4.0 requires them when KV is configured.
  • Key rotation guide (done)docs/key-rotation.md: add next certificate, switch, retire. Rehearsed live with Cloudflare Access with zero downtime.
  • Database adapter test matrix (done)Postgres, MySQL and MongoDB, and Drizzle on Postgres and MySQL and Prisma on Postgres, proven in CI next to SQLite and D1 (MongoDB found a real gap, fixed: D-033), with a Databases page in the guide.
  • Second adversarial review (done)Fresh-eyes review plus an independent review by a different model (D-029, D-030); every finding fixed with a regression test. Everything since the first round: POST re-entry, error Responses, account policy, NameID, encryption, IdP-initiated SSO, POST signatures, metadata refresh, the registry and the CLI.
1.0

Close the expected-feature gaps

What admins and SPs assume every IdP has.

  • IdP-initiated SSO (done)Opt-in per SP, off by default; RelayState only from a per-SP allow-list. Supported by 8 of the 11 products compared (partially by Ory Polis), including all four commercial IdPs.
  • Encrypted assertions (done)AES-256-GCM with RSA-OAEP, per SP, sign-then-encrypt. Decrypted and validated by node-saml and samlify. Shibboleth encrypts by default; Keycloak, authentik, Logto, Entra and Okta offer it per SP.
  • Register SPs from metadata XML (done)serviceProviderFromMetadata(): a helper that turns an SP's metadata into a serviceProviders entry, including certificates and ACS URLs.
  • Per-SP signing choice (done)sign: both, response or assertion, per SP, as Shibboleth, Keycloak and authentik offer.
  • Signed IdP metadata (done)Optional (signMetadata), for SPs and federations that verify metadata signatures.
1.0

Operations at scale

For hosts with many SPs or changing SPs.

  • Single Logout (done)SP- and IdP-initiated, front-channel, propagated to every SP in the session; PartialLogout when one fails. Best effort by nature, as Shibboleth says.
  • Database-backed SP registry and API (done)Add, change, disable and remove SPs at runtime without a redeploy; admin-gated API, every change logged with the acting user.
  • SP metadata URL with refresh (done)SP certificate rotation picked up automatically; certificates only, optional signature pinning.
  • Signed AuthnRequests over HTTP-POST (done)Enveloped XML signatures with XSW defences, pinned to the SP's certificates; node-saml interop; each defence mutation-tested.
Next

Reach, trust and B2B

Adoption by Better Auth's mainstream users, trust signals, and what B2B apps ask for next.

  • Next.js example (done)examples/nextjs: App Router on the Node runtime with node:sqlite, a sign-in page that honours callbackURL and prompt=login; CI builds it, starts it and signs in with node-saml.
  • Drizzle on Postgres and MySQL, Prisma on Postgres (done)The most common Better Auth adapters, proven as the query layer in the CI adapter matrix alongside Kysely, MongoDB, SQLite and D1 (D-051).
  • Bun and Deno (done)The built package smoke-tested on both in CI (test/runtimes/smoke.mjs): CLI keygen, metadata and a full sign-in with the signature verified (D-051).
  • Multi-tenant IdP, phase 1 (done)One IdP identity per organization: each tenant gets its own entity ID, metadata and SSO/SLO URLs, under the shared signing key. Host administrators create tenants and manage their SPs; organization membership is mandatory; isolation doesn't rely on SPs checking Issuer (D-052, docs/guide/multi-tenant.md).
  • Multi-tenant IdP, phase 2: a signing key per tenant (done)Keys stored encrypted in the database (sealed with Better Auth's versioned secret, bound to their tenant), generated (RSA 3072) or uploaded, rotated per tenant with the three-step rotation and a 24-hour publish guard. For SPs, moving from the shared key is an ordinary certificate rotation (D-058, docs/guide/multi-tenant.md).
  • Multi-tenant IdP, phase 3: delegated administration (done)An organization's owners and admins manage their own tenant's SPs through the registry API, membership re-read on every request. Only with per-tenant keys, which startup enforces; with IDOR checks, a user-field allow-list, a metadata-URL policy and a tenant-scoped audit view (D-059, docs/guide/multi-tenant.md).
  • A signing key the plugin makes and keeps (done)signing: { managed: true }: generated on first use, stored encrypted in the database, and rotated through the API in the same three steps as a PEM key, with expiry warnings; an existing key can be brought along (D-080, docs/guide/signing-and-encryption.md).
  • Microsoft 365 (done)Entra ID domain federation verified live: sign-in, sign-out, users linked by immutable ID, and Microsoft relying on the IdP's MFA (D-075, D-076, D-077, docs/sp-microsoft-365.md).
  • Assertion exchange (done)An SP can let one OAuth client exchange its assertions for tokens, once each: RFC 8693 token exchange with a SAML 2.0 subject token, the flow MCP's enterprise authorization uses (D-071, docs/guide/token-exchange.md).
  • SCIM provisioning, a companion plugin (done)better-auth-scim-provisioning (on npm) creates, updates and deactivates users in SPs automatically: SCIM 2.0 apps, Google Workspace and signed webhooks, with organizations, teams and roles as groups. A separate package, since provisioning is a separate concern from sign-in; verified live with Cloudflare Access and Google Workspace, and with this plugin as Workspace's IdP (D-063).
Later

Considered

Valuable, but needs design first or depends on the host.

  • Step-up authentication (done)authnContext levels: RequestedAuthnContext judged against the session's achieved class; the user is sent to sign in again (prompt=login, acr_values) when more is needed, as Keycloak does with levels of authentication (D-047).
  • Declarative attribute mapping (done)Per-SP map from attribute name to user field, constant, split list or first/last name; usable from JSON configuration.
Proposed

Under review

Ideas raised before 1.0. Each one either moves into a release or goes.

  • Salesforce as a live SP (done)Verified 2026-09-27 with a Developer Edition org: signed requests required and verified, NameID to Federation ID (D-042, docs/sp-salesforce.md).
  • Load test and benchmark (done)Sign-ins on Workers and D1 under concurrency, with the method and scripts to rerun it (docs/benchmark.md, scripts/bench): about 260 ms and 20 ms of CPU per sign-in; one D1 database tops out at about 28 sign-ins a second, a limit set by the database's query rate, not the plugin's CPU.
  • Release automation (done)`pnpm release patch|minor|major` opens the release PR (version bumped, CHANGELOG section dated); merging it tags the version and starts the release run, which still waits for the npm environment's approval and npm's staged-publish approval. Changesets was considered and not used: it would replace the hand-written changelog and bypass the tag-gated trusted publish.
  • Admin page in the example (done)/admin in the Workers example: IdP details, SPs (add from metadata, edit, enable, delete, test sign-in) and recent audit events, on the registry API. The plugin itself ships no UI.
  • NameID from a user field (done)nameId: { field } for code and stored SPs (for example an employee ID); only fields users can't set themselves (D-041).
Not planned

Out of scope

Deliberately left out.

  • Artifact bindingOf the products compared, only Keycloak supports it fully (Shibboleth and SimpleSAMLphp for responses); no target SP needs it.
  • Admin UI in the pluginBetter Auth plugins are headless; hosts build their own UI on the registry API, and the example has a reference page to copy.

HubSpot, the first target SP

  • PlanHubSpot Professional or Enterprise (Marketing, Sales, Service, Data, Content Hub, Smart CRM, Revenue Hub).
  • NameIDMust be the user's email, matching the HubSpot user. Met: emailAddress is the default format.
  • SigningSHA-256 only. Met: RSA-SHA256 is the default.
  • BindingHTTP-POST. Met.
  • IdP valuesIssuer, SSO URL and a PEM certificate. Met: all in the metadata endpoint.

Source: HubSpot SSO setup guide. A live Cloudflare Access login has been verified; HubSpot itself has not yet been tested live.

Method

Three parallel reviews of vendor documentation on 2026-10-15, falling back to the default-branch source code for the open-source products where docs were silent. Our own column is backed by tests in the repository and a live login through Cloudflare Access. Corrections are welcome as issues on the repository.

Sources