Skip to content

Releases: mmcintosh/better-auth-saml-idp

v1.7.0

Choose a tag to compare

@github-actions github-actions released this 16 Oct 01:47
b1890cf

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/sso accepting a reused SAML assertion under generateId: "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: add serverExternalPackages: ["better-auth-saml-idp"] to next.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-keys routes, 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-cloudflare 0.4 unreleased. CONTRIBUTING says when releases are cut (every week or two, and at once for a security fix), and pnpm release now 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 v3 for 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 beta tag, where Better Auth publishes its prereleases (1.8.0-beta.0 on 2026-10-15). It had tested only latest and next, and next still points at 0.8.7-beta.5, so it would never have tried 1.8 before its release. A prerelease tag is skipped once latest has 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/utils override removed (it held the tests on 0.4.2 while every supported Better Auth requires 0.5.0) and the sharp override 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's beta prereleases too, and the decision log's index lists every decision again (it stopped at D-062).

v1.6.0

Choose a tag to compare

@github-actions github-actions released this 14 Oct 05:26
1fb00ea

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 in samlIdpTenantKey, under Better Auth's secret or signing.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, /activate and /retire (with signing.minPublishedSeconds, default 24 hours, before a next key can sign). These need a host manager, on the new samlSigningKey statement with permissions. Every step, and the first generation, is a signing-key.changed event (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: without signing.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). Set signing.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_FOUND and SIGNING_KEY_TOO_NEW, for the managed key's routes.

Changed

  • Types: SamlIdpOptions["signing"] is now SigningConfig | ManagedSigningConfig (narrow on managed), SamlIdpEvent includes SigningKeyChangedEvent, and samlIdpStatements includes samlSigningKey. 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, onPremisesImmutableId set 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 onPremisesImmutableId set 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

Choose a tag to compare

@github-actions github-actions released this 12 Oct 11:38
699954b

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 tenants and the per-SP tenant.
  • 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.storeInDatabase and rateLimit.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 pg pool (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 under wrangler dev against Postgres.

v1.5.0

Choose a tag to compare

@github-actions github-actions released this 11 Oct 22:36
48dc24d

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 exactly PasswordProtectedTransport can get that class from an MFA session, instead of a sign-in round that can't help and then NoAuthnContext. 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.keys is "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: sessionIndex on samlIdpSessionParticipant gains an index, so the lookup isn't a table scan. D1: the example's migration 0014. Without it, logout still works, more slowly on large tables. On MySQL or SQL Server, Better Auth's migrate won't index an existing text column: change it first (MySQL: ALTER TABLE samlIdpSessionParticipant MODIFY sessionIndex varchar(255) NOT NULL;, using your table and column names).

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 on rejectMfaByFederatedIdp, and what AADSTS51004 usually means in a test.

Examples

  • The Workers example's provisioning is on better-auth-scim-provisioning 1.0 (#73), with migration 0015 for its new columns (group links' kind and subjectId, links' adopted). A deployment that ran provisioning on 0.3 applies 0015 before 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 passkey plugin, migration 0013): 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

Choose a tag to compare

@github-actions github-actions released this 09 Oct 02:52
37d846c

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.issued event records the sign-in level sent (authnContextClassRef, #65, D-075): the class authnContext.current() reported, or authnContextClassRef when 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 the multipleauthn class 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, migration 0012), reported to SPs through authnContext. A password is PasswordProtectedTransport, a password plus a code is Microsoft's multipleauthn, which a Microsoft 365 domain federated with acceptIfMfaDoneByFederatedIdp is 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

Choose a tag to compare

@github-actions github-actions released this 08 Oct 18:25
62f12ba

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, xpath 0.0.34, xml, xml-escape, asn1, safer-buffer and escape-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

Choose a tag to compare

@github-actions github-actions released this 08 Oct 12:19
d2c7d88

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.Element becomes ReactElement, since React 19's types have no global JSX). 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-js 1.2.2 (GHSA-68fv-2mgg-jv7q). osv-scanner.toml files 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), and http-cache-semantics through 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.
  • main requires 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.mjs never replaces a .env.local that appeared after its check, and sso.mjs decodes 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 AssertionExchangeError code 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 release opens.
  • 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: false when Better Auth is built per request; cf as 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

Choose a tag to compare

@github-actions github-actions released this 06 Oct 21:38
f7226fa

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 option tokenExchange: { 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 with getSamlIdpExchange(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 throw AssertionExchangeError with a code. A new assertion.exchanged event (events.onAssertionExchanged, audit log) records each exchange. A tenant's administrator can't set or change tokenExchange. Nothing changes for SPs that don't opt in. See Exchanging assertions for OAuth tokens.

Project

  • The workerd tests run on @cloudflare/vitest-plugin 1.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-only undici override is gone. Vitest 5 waits for the plugin to support it.
  • sharp 0.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

Choose a tag to compare

@github-actions github-actions released this 06 Oct 00:43
9a6d771

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-pg and the prisma-client generator, as npx auth generate writes 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.ts workaround 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

Choose a tag to compare

@github-actions github-actions released this 05 Oct 18:37
cf44865

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-For entry without checking it, which any client can set. Now a valid IP only, and a multi-value header only through advanced.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/init answers 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 missing sp is still UNKNOWN_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 callbackURL on 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-only RATE_LIMIT=off.

Project

  • Releases are one merge: pnpm release patch|minor|major opens 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.