new·Earn with mozg — 20% of every monthSend somebody here and take a fifth of every plan payment they make, for as long as they keep paying — not a bounty on the first invoice. Your handle is the link, the window is thirty days, and the commission lands on your balance the second they pay. Free to join: if you have signed in, you already have the link. mozg.sh/earnall news →
mozg.beta
Sign in

Better Auth · all subjects

better auth/upgrade guides

28 notes, read out of this brain and free to use. Each one was extracted from a source and is re-checked against its exam.

next-auth v4 update instructions

next-auth v4 is the current LTS line on the latest tag. Update with: pnpm add next-auth@latest. This resolves to version 4.24.15 as of the July 2026 security update.

next-auth v5 update instructions

next-auth v5 is in beta on the beta tag. A plain pnpm add next-auth@latest resolves to the v4 line, so v5 projects must install with the beta tag: pnpm add next-auth@beta. This resolves to version 5.0.0-beta.32 and pins @auth/core@0.41.3.

1.7 upgrade entry point command

Run `npx auth upgrade` to start upgrading Better Auth from 1.6 to 1.7. This command handles package updates for `better-auth` and all `@better-auth/*` packages together.

Old oidcProvider plugin is removed

The `oidcProvider` plugin from `better-auth/plugins` is removed in 1.7. Replace it with `oauthProvider` from `@better-auth/oauth-provider` and move the configuration across.

MCP moves to @better-auth/mcp package

The MCP plugin moves from `better-auth` to `@better-auth/mcp` and uses `@better-auth/oauth-provider`. Install `@better-auth/mcp`, `@better-auth/cimd`, and the official version 2 MCP client or server package. Import MCP authorization and protected-request helpers from `@better-auth/mcp`. Replace removed in-core client and adapters with `@modelcontextprotocol/client` or `@modelcontextprotocol/server`.

MCP configuration changes in 1.7

For MCP: (1) Add the required `jwt()` plugin. (2) Compose `mcp()` with `cimd({ fetchClientMetadataResource, metadataProfile: "mcp-2026-07-28" })` for client registration. (3) Move options from `oidcConfig` to the top level of `mcp({ ... })`. (4) Set one canonical HTTPS `resource`, such as `https://api.example.com/mcp`, and replace `resourceMetadataMappings` with that value. (5) Rename `withMcpAuth` to `requireMcpAuth` and `mcpHandler` to `createMcpProtectedRequestHandler`. (6) Apply protected-resource scope option changes. (7) Use version 2 `createMcpHandler` with `legacy: "reject"`, wrap it with `requireMcpAuth`, and expose only `POST`. (8) Move registered clients through OAuth client records migration.

MCP endpoints move from /mcp/* to /oauth2/*

OAuth endpoints move from `/mcp/*` to `/oauth2/*`. Discovery-based clients find the new endpoints automatically. The MCP refresh-token reuse interval defaults to 30 seconds for every client; set `refreshTokenReuseInterval: 0` on `mcp()` to require strict replay handling.

MCP no longer enables unauthenticated registration by default

`mcp()` no longer enables unauthenticated Dynamic Client Registration. If deliberately supporting it, set both `allowDynamicClientRegistration` and `allowUnauthenticatedClientRegistration`.

OIDC SSO uses verified sub claim as account subject

OIDC SSO now uses the verified `sub` claim as the account subject. Profile mappings can still select email, name, image, and additional fields, but `oidcConfig.mapping.id` is removed. Remove `id` from every OIDC mapping, then confirm that each OIDC provider returns a stable `sub`. If a previous mapping used `mapping.id`, prepare its replacement before the maintenance window. Build a trusted mapping from every old account subject to the protocol-defined identity, then rewrite those account rows with the verified `sub`. Obtain this mapping from the identity provider, not email or a mutable profile attribute.

SAML uses signed NameID as account subject

SAML now uses the signed `NameID` as the account subject. `samlConfig.mapping.id` is removed. Manual SAML configurations that do not provide `idpMetadata.metadata` must set `idpMetadata.entityID`; `samlConfig.issuer` identifies the service provider and no longer acts as an IdP fallback. Remove `id` from every SAML mapping, then confirm each SAML provider returns a stable, signed `NameID`. If a previous mapping used `mapping.id`, prepare its replacement before maintenance. Build a trusted mapping from every old account subject to the signed `NameID`, then rewrite those account rows.

IdP-initiated SAML disabled by default

Unsolicited logins started by the identity provider are now disabled by default. A login response is validated for `InResponseTo`, so it cannot be replayed against a request it was not issued for. Single Logout requests are matched to their session by `SessionIndex`. Set `saml.allowIdpInitiated: true` to restore the old behavior if required.

SAML signing certificates can be a list

SAML signing certificates now accept a single value or a list, which lets you rotate certificates without downtime. The management endpoints return the certificate as a list, or omit it when the certificates live inside an `idpMetadata` document. Update any code reading the certificate from those endpoints to expect a list or its absence. Make sure every SAML config supplies a signing-cert source: an explicit certificate or an `idpMetadata` document, or registration fails.

SAML callback URL and metadata automatically generated

The SAML callback URL is derived automatically and service-provider metadata is generated. Remove the empty `spMetadata` and removed fields from the config. Register the ACS URL with the IdP as the base URL plus `/sso/saml2/sp/acs/:providerId`; with default base path that is `https://yourapp.com/api/auth/sso/saml2/sp/acs/:providerId`. For SP-initiated logins, set the post-login redirect with `callbackURL` in `signIn.sso()`. The `callbackUrl` config field is no longer the ACS URL and is now optional, but it remains the post-login redirect for IdP-initiated logins. Keep it if `allowIdpInitiated` is enabled and a specific landing page is needed.

SAML signature enforcement matches wantAssertionsSigned configuration

In 1.6, `wantAssertionsSigned` enforced a signature on the SAML response message instead of the assertion. 1.7 verifies the assertion element itself and applies the configured requirement to it. Sign-ins from an IdP that signs only the response message fail when the service provider requires signed assertions. A callback that carries RelayState validates it unconditionally. Provider registration rejects service-provider metadata that weakens a signed-assertion policy, exceeds the metadata size limit, or declares an ACS location containing a URL fragment. Confirm each IdP signs assertions, or set `wantAssertionsSigned: false` deliberately for an IdP that cannot. Fix or re-register stored SP metadata that no longer validates. If a custom integration supplies its own RelayState on SAML callbacks, ensure it round-trips the value Better Auth issued.

/sso/update-provider rejects partial OIDC and SAML mappings

Updating an SSO provider now rejects a partial OIDC or SAML mapping object. Send a complete mapping object when calling `/sso/update-provider`.

OIDC SSO works on Cloudflare Workers

OIDC SSO with discovery now works on Cloudflare Workers. A discovery or token endpoint that redirects is rejected with a clear configuration error instead of failing in a runtime-specific way. No action needed. If a provider endpoint redirects, point the config at the final URL.

SCIM supports three connection modes

SCIM now supports three connection modes: Static code-defined (declare connections and bearer credentials in `scim({ connections })`); Application-owned runtime (verify bearer token and return its connection atomically from `authentication.verifyBearerToken`); Plugin-managed runtime (configure `managedConnections`, then call its trusted server APIs from an application-authorized administrator workflow). The plugin no longer depends on Organization or SSO plugins. Applications can choose one connection mode. Legacy runtime connection-management endpoints, SCIM client plugin, CLI scaffolding, `defaultSCIM`, `staticProviders`, `trustedDomains`, `providerOwnership`, and organization-scoped provider configuration are removed. Provisioned identities no longer create authentication accounts.

SCIM requires full reprovisioning with maintenance window

The new SCIM models do not read or convert 1.6 SCIM tables. Use a maintenance window, pause provisioning, and stop every application instance running the old SCIM plugin before changing the schema. This change cannot use an in-place generated migration.

SCIM reprovisioning workflow

SCIM reprovisioning workflow: (1) Back up every legacy SCIM table and build a reviewed inventory of exact `scimProvider` rows, SCIM-created `account` rows, their linked `user` rows, and any organization or team state. (2) Decide how each legacy User and account row will be handled: retain by copying a stable mapping to application-owned storage and configuring `identity.resolveUser`; recreate by removing SCIM account and deleting User after proving no other sign-in method depends on it. (3) Choose static, application-owned runtime, or plugin-managed connection mode and prepare new opaque identifiers. Never import legacy token hash, reuse raw token, or add compatibility path accepting legacy bearer syntax. (4) While old plugin is stopped, clear SCIM-owned resources and confirm legacy tables are empty and every inventoried SCIM account row has a reviewed disposition. Drop or rename incompatible legacy physical tables only after reviewed backup and cleanup. (5) Enable native interactive transactions and apply 1.7 schema. (6) Deploy selected mode; for managed mode, create connection and issue first credential through trusted server API. (7) Verify linking, Group state, lifecycle, and projections, then resume provisioning.

SCIM new plugin never links by email

The new SCIM plugin never links by email. Retained legacy Users will cause `409 Conflict` during reprovisioning unless `identity.resolveUser` returns an explicit link decision for them.

Stripe organization subscriptions require organization.enabled

`referenceMiddleware` now rejects organization-scoped subscriptions unless `organization: { enabled: true }` is set in the Stripe plugin config. Set `organization: { enabled: true }` in `stripe()` plugin options for organization-scoped subscriptions. The organization plugin is still needed separately.

Stripe onSubscriptionCancel event parameter is required

The `event` parameter on the `onSubscriptionCancel` callback is now required. Update `onSubscriptionCancel` callbacks to expect a non-optional `event`.

Device codes use unique indexes and bounded values

The stable 1.6 schema does not enforce uniqueness for device lookup values. The 1.7 schema creates unique indexes on `deviceCode` and `userCode`, so resolve duplicate values in both columns on every adapter before applying migration. MySQL and SQL Server must convert both columns to bounded strings and clean values longer than 191 characters. Custom `generateDeviceCode` and `generateUserCode` functions must stay within the 191-character limit.

OAuth device grants are opt-in for RFC 8707 resources

In 1.6, `deviceAuthorization()` signed a device into the same Better Auth application and returned a Better Auth session token from `/device/token`. That standalone flow remains available and does not accept RFC 8707 resource indicators or add OAuth fields to `deviceCode`. To let registered CLI, TV app, or other limited-input client obtain OAuth tokens, add OAuth Device Authorization integration alongside OAuth Provider with `oauthDeviceAuthorization()` and `oauthProvider()`. This adds nullable `oauthClientId` and `resources` fields to `deviceCode`, validates OAuth client, scopes, and resources at code creation, advertises the device authorization endpoint in discovery, and exchanges approved codes at `/oauth2/token`. Regenerate and apply schema when enabling it. Existing 1.6 rows need no backfill and remain on session-token path even if their `clientId` later matches a registered OAuth client. Allow pending 1.6 clients to finish polling `/device/token` or let codes expire before moving clients to OAuth flow.

Device authorization client uses RFC 8707 resource field with OAuth

The client type only exposes the RFC 8707 `resource` request field when the server uses the OAuth device authorization grant, not the standalone session token path.

enableTwoFactor returns discriminated response with method

`enableTwoFactor` now accepts a `method` of `"otp"` or `"totp"` (default `"totp"`) and returns that method in the response. `totpURI` and backup codes are present only for `"totp"`. The `skipVerificationOnEnable` option still works. Update callers reading `totpURI` or backup codes to branch on the returned `method`.

Magic-link and email-OTP sign-in clear unproven credentials

Magic-link and email-OTP sign-in now treat proven mailbox control as the source of truth for an account whose email had never been confirmed. If that account had an unproven password or other linked accounts, Better Auth removes all of them and revokes existing sessions before signing the user in. If a user signed up with email and password but first signs in through magic link or email OTP instead of confirming the verification email, ask them to set a new password through password reset.

Post-1.7 upgrade verification checklist

After deploying 1.7, verify: Sign in with email and password, each OAuth or SSO provider, and Google One Tap when configured. Link, list, refresh, and unlink external accounts. Exercise OAuth authorization, refresh, revocation, discovery, and protected-resource checks when running an identity provider. Connect an MCP client and complete one protected request. Confirm SAML SP-initiated login and IdP-initiated login when enabled. Confirm SCIM User linking, Group membership, lifecycle changes, and application projections before resuming provisioning. Confirm proxy deployments generate public callback, issuer, discovery, and JWKS URLs.

Give your agent this brain