Repository navigation
Releases: mmcintosh/better-auth-saml-idp
Release list
v1.7.0
Better Auth 1.7.8 is now the minimum, which keeps installs off Better Auth versions with known security advisories, and Next.js apps that bundle the plugin get a clear error instead of failing every SAML request; everyone on 1.6.x should upgrade, along with Better Auth.
Changed
- Better Auth 1.7.8 is now the minimum (the peer range is
>=1.7.8 <1.8.0, was>=1.7.5). 1.7.7 fixed three Better Auth security advisories: a critical Magic Link account takeover (GHSA-965c-763c-88jm), an OAuth Proxy sign-in as another user (GHSA-r4xp-prcw-77qf), and rate limits exceeded under concurrency on Drizzle and Postgres (GHSA-44jh-23m7-hpcf). 1.7.8 stopped@better-auth/ssoaccepting a reused SAML assertion undergenerateId: "uuid". The plugin's own code isn't affected by any of them, but the Better Auth it runs in is, and npm refuses to install the plugin next to an older one. Upgrade Better Auth (and its@better-auth/*packages) to 1.7.8 or later; 1.7.10 is the latest. This is a minor release under a new rule in the versioning policy: the floor may rise for security within a Better Auth minor (D-081).
Fixed
- Next.js: a clear error when the plugin is bundled. A Next.js app (Turbopack, the default) that bundles the plugin answered 500 to every SAML request with a
TypeError("The "path" argument must be of type string … Received an instance of URL"): the bundler rewrites the path to the plugin's XSD validator (wasm/xsd.wasm) into a web path the server can't read. The error now says what to do, and getting started and troubleshooting say it up front: addserverExternalPackages: ["better-auth-saml-idp"]tonext.config. The Next.js example already did; a copy of it built without the setting, installed from the packed tarball, reproduced the failure and now shows the message.
Documentation
- A shorter README (about 170 lines, was 607), with a diagram of the sign-in at the top and a screenshot of a real Microsoft 365 sign-in handing the user to the example IdP. The README is now the overview: what it is, the features, the Quick Start, requirements, and a table of the service providers it's verified with and what was verified. The reference sections (the option tables, endpoints, attributes, signing, Single Logout, organizations, the registry, the CLI) live in the guide, which already had them, with a table of where to find what. The guide's endpoint list gains the managed key's
/saml-idp/signing-keysroutes, and getting started the defaults the README listed (verified email, NameID per format, signed error Responses). The release checklist asks for README changes only where the README's own sections change. - The comparison rechecked on 2026-10-15, with a new README image. This plugin's column is at 1.7: the managed key's rotation through the API makes key rotation a full yes, and Single Logout and MFA name Microsoft 365, verified live. Keycloak 26.8.1, Zitadel 4.19.5, authentik 2026.8.4, Logto 1.45 and SimpleSAMLphp 2.5.3.1 are the versions compared; Logto 1.44 added requiring signed AuthnRequests. The roadmap lists the managed key, Microsoft 365 and assertion exchange as done.
- The Cloudflare Access guide gives the RelayState default as 4096, as it has been since 1.1.1; it still said 1024.
- docs/security.md no longer calls
better-auth-cloudflare0.4 unreleased. CONTRIBUTING says when releases are cut (every week or two, and at once for a security fix), andpnpm releasenow requires the sentence that opens each release, saying why it exists and who should upgrade.
Examples
- The examples (Next.js, Workers + D1, Workers + Neon, and the experimental CharDB examples) and the development setup run Better Auth 1.7.10. CI tests 1.7.8, the lowest supported version, alongside the latest 1.7.x.
- The CharDB examples run the plugin's current release (1.6.0, was 1.2.0) and Better Auth 1.7.10 (was 1.7.6, below the new minimum), with CharDB's generated migration
v3for the plugin's newer index (samlIdpSessionParticipant_sessionIndex_idx). A CharDB deployment of the example applies it on upgrade. - The Workers example uses better-auth-scim-provisioning 1.3.0 and better-auth-cloudflare 0.4.1.
Project
- The upstream canary also tests Better Auth's
betatag, where Better Auth publishes its prereleases (1.8.0-beta.0 on 2026-10-15). It had tested onlylatestandnext, andnextstill points at 0.8.7-beta.5, so it would never have tried 1.8 before its release. A prerelease tag is skipped oncelatesthas caught up with it. - Up to date before the release (D-082): Vitest 5 (its hold lifted, now that @cloudflare/vitest-plugin supports it); the
@better-auth/utilsoverride removed (it held the tests on 0.4.2 while every supported Better Auth requires 0.5.0) and thesharpoverride removed (no longer needed); xml-crypto 6.4.0 in the lockfile, which new installs already get (the published range stays^6.3.3; the plugin uses none of what it deprecates); patch and minor updates for development and the examples. The upstream watch reports Better Auth'sbetaprereleases too, and the decision log's index lists every decision again (it stopped at D-062).
v1.6.0
A signing key the plugin makes, stores and rotates itself, so a deployment needs no key made by hand, and Microsoft 365 users provisioned straight from Better Auth.
Added
- A signing key the plugin makes and keeps:
signing: { managed: true }(guide, D-080). Instead of a PEM key and certificate in the options, the plugin generates the IdP's key the first time it's needed (RSA 3072, a two-year certificate) and stores it encrypted insamlIdpTenantKey, under Better Auth's secret orsigning.keyEncryptionSecret. Concurrent first requests end up with one key. It's rotated in the same three steps as tenants' keys, through/saml-idp/signing-keys,/rotate,/activateand/retire(withsigning.minPublishedSeconds, default 24 hours, before a next key can sign). These need a host manager, on the newsamlSigningKeystatement withpermissions. Every step, and the first generation, is asigning-key.changedevent (events.onSigningKeyChanged). A stored key that can't be used refuses sign-ins and metadata; nothing else ever signs instead. Tenants on the shared key sign with the managed key. A deployment can now start with no key made by hand, which a one-click install needs. Keys given as PEM work as before. Switching an existing deployment: withoutsigning.initialKey(the current key, stored as the first one) the plugin makes a new key and SPs that trust the old certificate refuse its signatures (how to switch). Setsigning.keyEncryptionSecret: with Better Auth's secret, rotating it without keeping the old version makes the stored key unreadable and stops the IdP signing (recovering). - Error codes
INVALID_SIGNING_KEY,SIGNING_KEY_EXISTS,SIGNING_KEY_NOT_FOUNDandSIGNING_KEY_TOO_NEW, for the managed key's routes.
Changed
- Types:
SamlIdpOptions["signing"]is nowSigningConfig | ManagedSigningConfig(narrow onmanaged),SamlIdpEventincludesSigningKeyChangedEvent, andsamlIdpStatementsincludessamlSigningKey. Code that switches exhaustively over events or statements gets a new case.
Documentation
- Microsoft 365: adding users after federating (guide, D-079). A user created on the federated domain with Graph
POST /users,onPremisesImmutableIdset to their Better Auth id and no password, signed in at My Apps through the IdP on the first try. The guide had said this works without showing how. It now has the request, and a row in the Graph API table. - Microsoft 365: provisioning, who may sign in, and two Graph surprises (guide). Step 3 points to better-auth-scim-provisioning 1.3.0's Entra ID target, which creates users with
onPremisesImmutableIdset and no password. Step 2 says how to let in only some users. "When something goes wrong" covers a deleted user blocking a new one with the same immutableId (the recycle bin, and how to restore), and a new user not yet found by id.
v1.5.1
A README for newcomers on any runtime, npm keywords for the verified SPs, and the Workers example on better-auth-cloudflare 0.4.0.
Documentation
- A README for newcomers on any runtime. The Quick Start now works on Node as well as Workers (install, a key, a minimal config, the tables, then the CLI checks), and the Workers-only warning moved into the Workers setup. The examples use a generic SP instead of HubSpot, which isn't verified live yet. Setup step 2 starts from Better Auth's CLI and the example's migrations, with the hand-written tables in Schema. The verification lists sit after the Quick Start, and the roadmap became a short "What's next" (the full roadmap is on the comparison page). Fixed: Keycloak 26.8 (not 26.7) in CI, the CI matrix sentence, the better-auth-cloudflare settings (0.4.0, released 2026-10-12, requires them with KV; the README had said 0.4 wouldn't need them), and the guide links in step 5 (Okta, Auth0 and Salesforce added). The options tables gained
tenantsand the per-SPtenant. - npm keywords name the verified SPs (Microsoft 365, Entra ID, Google Workspace, Okta, Salesforce) instead of HubSpot.
Examples
- The Workers example uses better-auth-cloudflare 0.4.0 from npm (released 2026-10-12) instead of a vendored build of its main branch; 0.4.0 is that build plus the release commit. Its
verification.storeInDatabaseandrateLimit.storage: "database"settings stay: with KV configured, 0.4.0 fails every request without them, naming the fix. The plugin itself doesn't depend on better-auth-cloudflare. - Workers + Neon example (examples/workers-neon, D-078): the plugin on Cloudflare Workers with Postgres on Neon through Hyperdrive, its query cache off. Better Auth is built once per isolate and each request gets its own
pgpool (connections can't cross requests on Workers); building it per request cost about 25 ms of CPU on every request. Verified live (smoke checks, a full sign-in, a session deleted in Neon refused on the next request), and CI runs the same scripts underwrangler devagainst Postgres.
v1.5.0
Signing out at Microsoft 365 now ends the IdP session, and authnContext.current() also gets the SP and its request.
Added
authnContext.current()also gets the SP and its request (serviceProvider,requested; D-077), so it can answer per SP. For example, an SP that asks for exactlyPasswordProtectedTransportcan get that class from an MFA session, instead of a sign-in round that can't help and thenNoAuthnContext. Callbacks that ignore the new fields behave as before.
Changed
- The registry's overlap warning (the same entity ID and ACS URL in two tenants) no longer says tenants share one signing key when
tenants.keysis"per-tenant". It now says each copy of the SP must trust only its own tenant's certificate (D-077).
Fixed
- Signing out at Microsoft 365 now ends the IdP session (D-076). Microsoft sends its LogoutRequest from a hidden iframe on its sign-out page, and browsers never send the session cookie to a cross-site iframe, so the IdP found no session, answered
Success, and the next Microsoft sign-in went through without asking. A LogoutRequest without the session's cookie (or with another session's) now ends the session its SessionIndex was issued for, found on the server. This browser's own session and cookie are left alone. The SessionIndex is unguessable and given only to that SP for that one session; whoever holds it can end that one session and nothing else.- Database:
sessionIndexonsamlIdpSessionParticipantgains an index, so the lookup isn't a table scan. D1: the example's migration0014. Without it, logout still works, more slowly on large tables. On MySQL or SQL Server, Better Auth'smigratewon't index an existingtextcolumn: change it first (MySQL:ALTER TABLE samlIdpSessionParticipant MODIFY sessionIndex varchar(255) NOT NULL;, using your table and column names).
- Database:
Documentation
- Microsoft 365 guide, from a second live test (D-076, D-077): Microsoft relying on your IdP's MFA is verified with per-user MFA and with Conditional Access (security defaults off), and a password-only sign-in gets Microsoft's own MFA. New: sign-out, one Microsoft tenant per IdP entity ID (use a tenant of your IdP for a second one), the Graph API (
az rest) route beside PowerShell, why the federation starts onrejectMfaByFederatedIdp, and whatAADSTS51004usually means in a test.
Examples
- The Workers example's provisioning is on better-auth-scim-provisioning 1.0 (#73), with migration
0015for its new columns (group links'kindandsubjectId, links'adopted). A deployment that ran provisioning on 0.3 applies0015before deploying. Checked locally on D1 against a SCIM server: a user and an organization's group delivered, and the new columns written. - Passkeys in the Workers example (Better Auth's
passkeyplugin, migration0013): add them on the home page, then "Sign in with a passkey", using the browser's own WebAuthn JSON helpers with no client library. - The sign-in level now comes from how the session was signed in, not from the user's two-step setting. A session field (
auth_method), set when the session is created, records password, authenticator code, backup code, passkey or impersonation, and the MFA class is sent only for the code and passkey ones. The Microsoft 365 guide's MFA snippet follows it.
v1.4.0
The audit log records the sign-in level sent to each service provider, and a new guide covers Microsoft 365 (Entra ID domain federation), verified live.
Added
- The
assertion.issuedevent records the sign-in level sent (authnContextClassRef, #65, D-075): the classauthnContext.current()reported, orauthnContextClassRefwhen step-up isn't configured. It's in the audit log's details too, so the log shows, for example, whether Microsoft 365 was told the user did MFA.
Documentation
- Microsoft 365 guide (docs/sp-microsoft-365.md, D-075): Entra ID domain federation, verified live. It covers adding Microsoft by configuration (its metadata isn't schema-valid), linking users through
onPremisesImmutableId, federating with Graph PowerShell, sending themultipleauthnclass for MFA done at the IdP, and the security-defaults catch.
Examples
- Two-step sign-in in the Workers example (#64): authenticator-app codes (Better Auth's
twoFactor, migration0012), reported to SPs throughauthnContext. A password isPasswordProtectedTransport, a password plus a code is Microsoft'smultipleauthn, which a Microsoft 365 domain federated withacceptIfMfaDoneByFederatedIdpis set to accept as MFA (Microsoft relying on it isn't verified yet; see the guide). - A QR code for two-step sign-in setup in the Workers example, drawn by the Worker (
uqr), so the key never goes to a third-party QR service.
v1.3.0
samlify is no longer a dependency: the plugin writes its metadata itself, byte for byte the same, so a user installs 6 packages instead of 13 and the bundled code roughly halves. No API or behaviour changes.
Changed
- samlify is no longer a dependency (D-074). It was only used to build the IdP's metadata document, which the plugin now writes itself, byte for byte the same (tested against samlify's output for every option that changes it). A user now installs 6 packages instead of 13: gone are samlify,
node-rsa,@authenio/xml-encryption,xpath0.0.34,xml,xml-escape,asn1,safer-bufferandescape-html. The plugin's bundled code roughly halves (minified 993 KB → 523 KB, gzip 269 KB → 143 KB). No API or behaviour changes.
v1.2.1
Documentation release: no code changes. The package's README and docs catch up with 1.2.0's assertion exchange, the security notes gain a Hyperdrive warning, and two Workers-guide statements are corrected.
Examples
- The CharDB examples run on React 19 (react, react-dom and their types together;
JSX.ElementbecomesReactElement, since React 19's types have no globalJSX). Dependabot now proposes React's packages as one update, and leaves Vitest's next major alone until the Workers test plugin supports it.
Project
- OSV-Scanner and Scorecard report no open vulnerabilities. The CharDB examples force
source-map-js1.2.2 (GHSA-68fv-2mgg-jv7q).osv-scanner.tomlfiles record, with reasons and a re-check date, the advisories that have no update: npm's bundled dependencies in the release job (.github/npm-cli), andhttp-cache-semanticsthrough Astro in the CharDB Astro example. None reach the published package. - Every dependency install in CI goes through Socket Firewall, which blocks confirmed malware before it downloads (the release job included). pnpm 10.34.6 won't resolve a version less than a day old (
minimumReleaseAge, Better Auth exempt), and Dependabot waits 3 days (D-073). The README shows Socket's package badge. mainrequires a pull request and 19 passing checks (the CI, CodeQL and dependency jobs) before anything merges, with no bypass (D-072).- Next.js example scripts:
dev-keys.mjsnever replaces a.env.localthat appeared after its check, andsso.mjsdecodes HTML entities in one pass.
Documentation
- Assertion exchange (1.2.0) in the README (features, options, a usage section), the security page, the errors page (each
AssertionExchangeErrorcode and whether it uses the assertion up), the schema page, and the threat model. - The README shows the OpenSSF Best Practices badge (passing, project 15268). Its answers are kept in
.bestpractices.json, which the badge site reads to pre-fill them. - Each release now checks the docs against the changelog: a step in CONTRIBUTING.md's release list, and a checklist in the release pull request
pnpm releaseopens. - Cloudflare Workers guide and security notes: turn off Hyperdrive's query cache for an auth database (
--caching-disabled): it kept a revoked session valid for 62.5 s. Also:validateSchema: falsewhen Better Auth is built per request;cfas a function needs better-auth-cloudflare 0.4 (0.3.1 stores no geolocation then); and 0.4's storage check fires when Better Auth initializes, on the first request, not at deploy time.
v1.2.0
Adds assertion exchange: an SP can let one OAuth client trade its assertions for tokens, once each (draft ID-JAG §4.5, MCP's Enterprise-Managed Authorization). Nothing changes for SPs that don't opt in.
Added
- Assertion exchange (D-071): an SP can let one OAuth client exchange its assertions for tokens, once each. This is RFC 8693 token exchange with a SAML 2.0 subject token, the flow in draft-ietf-oauth-identity-assertion-authz-grant §4.5 that MCP's Enterprise-Managed Authorization uses. Set
tokenExchange: { clientId }on the SP; stored SPs also need the new server optiontokenExchange: { enabled: true }. Each assertion to such an SP is recorded at sign-in. An authorization server on the same Better Auth instance (an ID-JAG issuer) checks it withgetSamlIdpExchange(ctx).verifyIssuedAssertion(ctx, assertionXml, { clientId }). That checks the signature (XSW-hardened, the identity's own key), the issuer and tenant, the validity window, the SP's client, single use across instances, and the user, session and membership as they are now. Failures throwAssertionExchangeErrorwith a code. A newassertion.exchangedevent (events.onAssertionExchanged, audit log) records each exchange. A tenant's administrator can't set or changetokenExchange. Nothing changes for SPs that don't opt in. See Exchanging assertions for OAuth tokens.
Project
- The workerd tests run on
@cloudflare/vitest-plugin1.3.6, which replaces@cloudflare/vitest-pool-workers(0.22.0, last published 2026-09-18). It brings the current miniflare and wrangler, so the development-onlyundicioverride is gone. Vitest 5 waits for the plugin to support it. sharp0.35.5 is forced (GHSA-wq5f-xc86-pv6w, high), at the root and in the CharDB examples. It comes with Miniflare, so it affects development and the examples only; nothing in the published package uses it.- The release job stages with npm 12.2.0 (was 11.20.0).
v1.1.4
Requires xml-crypto 6.3.3, which fixes several canonicalization cases in signed XML; upgrade to pick it up if your lockfile still has 6.3.2. No API changes.
Changed
- xml-crypto 6.3.3 or later. It fixes several canonicalization cases in signed XML (inherited namespace context, namespace values, processing instructions). Fresh installs already got it through
^6.3.2; the minimum now makes sure. Every test, including the SP interop suite, passes on it.
Project
- A weekly Upstream watch issue lists what Dependabot doesn't cover: Better Auth against the peer range, the vendored builds of unreleased upstream code, and updates held back on purpose (
upstream-watch.yml). - The adapter matrix runs Prisma 7 (7.10, with
@prisma/adapter-pgand theprisma-clientgenerator, asnpx auth generatewrites it for Prisma 7) instead of Prisma 6. Prisma 7's CLI pins mysql2 3.15.3 (GHSA-3f6p-5ww8-9rcr, GHSA-rgwj-5xj2-c3m3), so a development-only override lifts it to 3.24.5. The Databases guide now says Prisma 7. - The Upstream watch also lists each pnpm override with whether the package that needs it still does.
Examples
- The CharDB examples run on CharDB's main branch (
e6cf5c9) and better-auth-saml-idp 1.1.3. CharDB now keeps Better Auth's background work alive itself, so the examples'src/background.tsworkaround is gone, and a replayed request no longer logs an uncaught SQLite error. Checked on a database created with the previous build: sessions, tenants, keys, SPs, used request IDs and audit rows carry over.
v1.1.3
Fixes from an external review of 1.1.2: nothing critical or high, but upgrade if you log client IPs, use tenant key rotation, or pass RelayState with line breaks. No API changes.
Fixed
From an external review of 1.1.2 (nothing critical or high; D-065):
- Events and the audit log read the client IP the way Better Auth does. They took the left-most
X-Forwarded-Forentry without checking it, which any client can set. Now a valid IP only, and a multi-value header only throughadvanced.ipAddress.trustedProxies. - A retried tenant key activation finishes the activation. Cut short after the active key moved to "previous", a retry retired the tenant's own old key and published the shared root certificate for that tenant.
/saml2/idp/initanswers an unknown SP id and one that hasn't opted in alike (IDP_INITIATED_NOT_ALLOWED), so SP ids and tenant keys can't be listed from outside. A missingspis stillUNKNOWN_SERVICE_PROVIDER.- RelayState goes back to the SP exactly as it came. The form escaped it as XML, which dropped some characters and rewrote line endings. A RelayState with control characters, which a browser's form submission would change, is now refused (
INVALID_SAML_REQUEST). - SP metadata up to 1 MiB is accepted, as documented. The validator capped it at 128 KiB; protocol messages are still held to 128 KiB (
libxml2Validator({ maxMetadataBytes })).
Documentation
- The getting-started sign-in snippet only follows a
callbackURLon the IdP's own origin. As written, a signed-in user opening/sign-in?callbackURL=javascript:…would have run it, and another site's URL redirected there. - Corrected where the docs said something the code doesn't do: the replay key, the minimum better-auth-cloudflare version, when the registry API is mounted, ProtocolBinding, Redirect-bound XML signatures, InvalidNameIDPolicy's status, tenant metadata's certificate, the flow diagram's order, ForceAuthn's meaning, and resume links.
- A guide to using this plugin with better-auth-scim-provisioning, so your app both signs people in to its apps and keeps their accounts in step there (
docs/guide/provisioning.md). - A benchmark of sign-ins on Workers and D1, with the scripts to rerun it (
docs/benchmark.md,scripts/bench). The Workers example gains a benchmark-onlyRATE_LIMIT=off.
Project
- Releases are one merge:
pnpm release patch|minor|majoropens the release PR, and merging it tags the version and starts the release run (tag-release.yml). Publishing still waits for the two approvals.
Examples
- The Workers example can also provision (better-auth-scim-provisioning, optional): set a SCIM app or Google Workspace target, apply migration 0011, and a Users and apps page shows each user's account at each app, the queue and the groups, with actions (add a test user, ban, re-sync, reconcile, …). A Cron Trigger, if added, delivers retries.
- The Workers example's pages are redesigned: one layout with a sidebar (the IdP's sections for admins, your apps for everyone), separate Overview, Service providers, Tenants and Activity pages, a matching sign-in page, light and dark themes, and user emails in the activity log. The styles and code are same-origin files, so the pages' CSP allows no inline code at all.