mirror of
https://github.com/better-auth/better-auth.git
synced 2026-07-15 21:18:18 -05:00
docs: add Better Auth 1.7 Release Candidate pages (#10169)
Co-authored-by: Taesu <bytaesu@gmail.com>
This commit is contained in:
@@ -28,4 +28,5 @@ razorpay
|
||||
payu
|
||||
PayU
|
||||
Mikro
|
||||
anthropics
|
||||
anthropics
|
||||
Zitadel
|
||||
|
||||
@@ -128,6 +128,27 @@ export default async function Page({
|
||||
{children}
|
||||
</Callout>
|
||||
),
|
||||
HeaderLabel: ({
|
||||
children,
|
||||
variant = "default",
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
variant?: "default" | "info" | "warning";
|
||||
}) => {
|
||||
const colors = {
|
||||
default: "text-neutral-600 dark:text-neutral-300",
|
||||
info: "text-blue-500 dark:text-blue-400",
|
||||
warning: "text-amber-600 dark:text-amber-400",
|
||||
};
|
||||
return (
|
||||
<span
|
||||
data-header-label="true"
|
||||
className={`text-[11px] font-semibold tracking-wide not-prose block select-none ${colors[variant]}`}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
iframe: (props: React.ComponentProps<"iframe">) => (
|
||||
<iframe
|
||||
title="Embedded content"
|
||||
|
||||
@@ -2489,6 +2489,18 @@ C0.7,239.6,62.1,0.5,62.2,0.4c0,0,54,13.8,119.9,30.8S302.1,62,302.2,62c0.2,0,0.2,
|
||||
href: "/docs/guides/optimizing-for-performance",
|
||||
icon: () => <Gauge className="size-4" />,
|
||||
},
|
||||
{
|
||||
title: "Upgrade",
|
||||
group: true,
|
||||
icon: () => null,
|
||||
href: "",
|
||||
},
|
||||
{
|
||||
title: "1.7 Upgrade Guide",
|
||||
href: "/docs/guides/1-7-upgrade-guide",
|
||||
icon: () => <ScrollTextIcon className="size-4" />,
|
||||
isNew: true,
|
||||
},
|
||||
{
|
||||
title: "Migration",
|
||||
group: true,
|
||||
|
||||
365
docs/content/blogs/1-7-rc.mdx
Normal file
365
docs/content/blogs/1-7-rc.mdx
Normal file
@@ -0,0 +1,365 @@
|
||||
---
|
||||
title: "Better Auth 1.7 RC"
|
||||
description: "OAuth and OpenID Connect fixes, protected resources, DPoP, MCP package migration, SAML and SCIM updates, safer defaults, and more."
|
||||
date: 2026-06-26
|
||||
author:
|
||||
name: "Gustavo Valverde"
|
||||
avatar: ""
|
||||
tags: ["1.7", "OAuth", "OpenID Connect", "MCP", "SAML", "SCIM", "security"]
|
||||
---
|
||||
|
||||
# Better Auth 1.7 RC
|
||||
|
||||
We've put a lot into Better Auth 1.7. It's a big release, and some of it means migration work. We wanted to support a wider range of login flows, make the system more robust, and ship security fixes that needed structural changes.
|
||||
|
||||
Here's what this release brings.
|
||||
|
||||
**Your app can be the login provider for other apps.** If other apps sign in through your app, Better Auth now handles more OAuth and OpenID Connect rules. This includes DPoP tokens, single sign-out, forced re-login, per-API token rules, and safer token checks.
|
||||
|
||||
**More login providers work.** Better Auth now supports login setups that did not work well before: Amazon Cognito, Microsoft Entra ID with certificate login, standard OpenID providers, and school-login providers like Clever.
|
||||
|
||||
**More secure by default.** We hardened flows that were previously unsafe. As security keeps moving to the foreground, we would rather disclose a security gap and fix it quickly than keep it in place for backward compatibility.
|
||||
|
||||
***
|
||||
|
||||
## Highlights
|
||||
|
||||
<HeaderLabel variant="warning">Breaking</HeaderLabel>
|
||||
|
||||
### Better Auth as an identity provider
|
||||
|
||||
This is the biggest area of the release. The OAuth provider grows from "it issues tokens" into a standards-based authorization server that other apps can sign in through.
|
||||
|
||||
* **Protected resources:** describe each API behind your login server as its own resource, with its own token lifetime, scopes, and claims. A token is locked to the API it was issued for and can no longer be replayed against another. This replaces the flat `validAudiences` list.
|
||||
* **DPoP-bound tokens:** an access token binds to a key held by the client, so a stolen token is not enough to call the API.
|
||||
* **Single sign-out:** OIDC Back-Channel Logout tells every app a user signed into to end its own session when they sign out of your provider.
|
||||
* **Forced re-login:** `max_age` is now honored instead of ignored.
|
||||
* **Consistent introspection:** opaque tokens return the same claims as JWTs, and a separate API service can introspect a token issued to another client.
|
||||
* **Protocol-safe ID tokens:** custom claims can no longer overwrite reserved protocol claims such as issuer, subject, and audience.
|
||||
* **Self-service and machine clients:** a backend registers without a logged-in user using a pre-shared token, and **Client ID Metadata Documents** (`@better-auth/cimd`) let a client identify itself by a hosted URL, which is how MCP clients connect without being registered ahead of time. Real MCP clients like Claude and Codex now keep the authentication method they register with, confidential by default rather than forced to public.
|
||||
* **Request specific user details:** a client can ask for individual user claims through the standard `claims.userinfo` parameter, and the request becomes part of the user's consent.
|
||||
* **An extension surface:** `extendOAuthProvider` lets a companion plugin add grant types, client-authentication methods, discovery metadata, and claims without changing core.
|
||||
* **Conformance polish:** standard `{ error, error_description }` envelopes, `no-store` on credential responses, an `at_hash` claim on ID tokens, form-encoded requests, and a refresh-retry window for native clients.
|
||||
|
||||
The old `oidcProvider` plugin, deprecated in 1.6, is removed. Move OpenID provider setups to `@better-auth/oauth-provider`.
|
||||
|
||||
***
|
||||
|
||||
<HeaderLabel variant="warning">Breaking</HeaderLabel>
|
||||
|
||||
### Connecting to more identity providers
|
||||
|
||||
The other half of the OAuth work is about Better Auth as the app receiving a login.
|
||||
|
||||
* **Generic OAuth is rebuilt** on the same path as built-in social providers, with PKCE on by default and automatic issuer validation through discovery. The API you call and the callback URL both change.
|
||||
* **Identity tokens are verified consistently** against a provider's published keys, including for mobile and single-page apps. Custom providers move from a `verifyIdToken` method to an `idToken` config.
|
||||
* **Granted scopes are preserved** across re-login and token refresh instead of being overwritten, and Google's `includeGrantedScopes` is now configurable (still on by default).
|
||||
* **Google One Tap** signs in the account owner by validating the identity token subject, not just the email.
|
||||
* **Per-request login options** (`additionalParams`) unlock Cognito upstream routing, Microsoft Entra ID `domain_hint`, and offline or incremental Google access. **Certificate and signed-assertion login** (`clientAssertion`, `tokenEndpointAuth`) add Entra ID certificate login and generic `private_key_jwt`.
|
||||
* **Provider-started login** restarts safely with `allowIdpInitiated: true`, **per-provider email verification** (`requireEmailVerification`) withholds the session until the email is verified, and **anonymous account linking** now works in Expo and other in-app browsers.
|
||||
|
||||
***
|
||||
|
||||
<HeaderLabel variant="info">New</HeaderLabel>
|
||||
|
||||
### New integrations unlocked
|
||||
|
||||
These setups were impossible, broken, or unsafe before, and work now.
|
||||
|
||||
**Logging your users in with an external provider**
|
||||
|
||||
| Provider or setup | What was blocked | How it works now |
|
||||
| --------------------------------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------ |
|
||||
| **Amazon Cognito** federated login | You could not route a user to a specific upstream provider per login. | Use typed `identityProvider` and per-request `additionalParams`. |
|
||||
| **Microsoft Entra ID** certificate login | Only shared-secret login was supported. | Use `clientAssertion` for certificate login and `domain_hint` per login. |
|
||||
| **Google** offline and incremental access | Options were global, and granted scopes were overwritten each login. | Use per-request options, preserved scopes, and `includeGrantedScopes`. |
|
||||
| **Any OpenID provider** via discovery | The provider identity token was not verified. | Point at a discovery URL, and tokens are verified automatically. |
|
||||
| **Zitadel, Auth0, and other multi-tenant OIDC providers** | No way to send extra parameters when refreshing a token. | Use `refreshTokenParams` on refresh, without a full redirect. |
|
||||
| **Providers needing signed-assertion login** | Only secret-based login was supported. | Use `tokenEndpointAuth` with a signed JWT. |
|
||||
| **Clever and similar education providers** | A login started by the provider failed. | Use `allowIdpInitiated: true` to restart the flow safely. |
|
||||
|
||||
**Other apps logging in through you**
|
||||
|
||||
| Setup | What was blocked | How it works now |
|
||||
| ---------------------------------------------- | --------------------------------------- | ---------------------------------------------------------------------------- |
|
||||
| **APIs needing theft-resistant tokens** | Only plain bearer tokens. | Use DPoP tokens bound to the client's key. |
|
||||
| **Several APIs behind one login server** | One token could be used on any API. | Use per-API resources, so tokens are locked to the API they were issued for. |
|
||||
| **Machine clients registering themselves** | Registration required a logged-in user. | Use pre-shared registration tokens. |
|
||||
| **MCP clients (Claude, Codex, Factory Droid)** | Their registration was rejected. | They are accepted automatically, with Client ID Metadata Document support. |
|
||||
| **Single sign-out across apps** | Sign-out did not notify other apps. | Use OIDC Back-Channel Logout. |
|
||||
| **Forced re-login** | The request was ignored. | `max_age` is enforced. |
|
||||
|
||||
**Enterprise SSO**
|
||||
|
||||
| Setup | What was blocked | How it works now |
|
||||
| ------------------------------------ | ------------------------------------- | --------------------------------------------------- |
|
||||
| **SAML certificate rotation** | Only one certificate was accepted. | A list of certificates is accepted during rotation. |
|
||||
| **SCIM group provisioning** | No durable group resources. | Groups have first-class lifecycle endpoints. |
|
||||
| **OpenID SSO on Cloudflare Workers** | Redirecting endpoints broke the flow. | They fail with a clear configuration error. |
|
||||
|
||||
***
|
||||
|
||||
<HeaderLabel variant="warning">Breaking</HeaderLabel>
|
||||
|
||||
### Enterprise SSO, SAML, and SCIM
|
||||
|
||||
* **Rotate SAML certificates without downtime:** signing certificates can now be a list, so an administrator publishes a new certificate next to the old one. The management endpoints return the certificate as a list, or omit it when certs live in an `idpMetadata` document.
|
||||
* **Unsolicited SAML logins are off by default:** `allowIdpInitiated` now defaults to `false`, so a provider-started login is rejected unless you opt in with `allowIdpInitiated: true`.
|
||||
* **Simpler SAML configuration:** the callback URL is derived automatically, service-provider metadata is generated, several unused fields are removed, and Single Logout ends the session correctly. One endpoint path changes.
|
||||
* **SCIM groups and organization scoping:** SCIM gains durable group resources, and every connection is now bound to an organization through a required `organizationId`, with the old per-user ownership option removed. This needs a migration and one manual reclaim of pre-1.7 connections that have no organization.
|
||||
|
||||
***
|
||||
|
||||
<HeaderLabel variant="info">New</HeaderLabel>
|
||||
|
||||
### Drizzle Relations v2
|
||||
|
||||
Drizzle ORM v1 is now in release candidate, bringing its new relations API. Better Auth supports it through a new adapter entry point, so the generated auth relations slot in alongside your app's own relations.
|
||||
|
||||
```ts title="auth.ts"
|
||||
import { betterAuth } from "better-auth/minimal";
|
||||
import { drizzleAdapter } from "@better-auth/drizzle-adapter/relations-v2"; // [!code highlight]
|
||||
import { db } from "./db";
|
||||
import * as schema from "./schema";
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: drizzleAdapter(db, { provider: "pg", schema }),
|
||||
});
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
<HeaderLabel variant="info">New</HeaderLabel>
|
||||
|
||||
### Built-in i18n for 22 languages
|
||||
|
||||
`@better-auth/i18n` ships built-in translations for 22 languages, and the English fallback is fixed. Drop it in to localize Better Auth's user-facing messages without maintaining your own translation table.
|
||||
|
||||
***
|
||||
|
||||
<HeaderLabel variant="warning">Breaking</HeaderLabel>
|
||||
|
||||
### Secure by default
|
||||
|
||||
* **Forwarded proxy headers are not trusted by default:** your app reads its own address from the `Host` header and ignores `x-forwarded-*` unless you opt in with `advanced.trustedProxyHeaders: true`. Platforms like nginx, Vercel, Cloudflare, and Netlify usually need no change.
|
||||
* **Atomic state, no more races:** "check, then write" database logic is replaced with single atomic operations, so a one-time token cannot be used twice, a team cannot exceed its limit, and a rate limit holds under load. Custom adapters and storage backends gain required methods.
|
||||
* **Electron requires modern PKCE:** the Electron flow now requires S256 PKCE and no longer trusts a custom origin header. Upgrade the client and server together.
|
||||
* **Captcha matches full paths:** replace a partial path like `/sign-in` with `/sign-in/*` or `/sign-in/**`.
|
||||
* **Stricter custom-scheme origins:** a host-bearing `trustedOrigins` entry like `myapp://callback` now matches that host exactly and no longer accepts `myapp://callback.attacker.tld`.
|
||||
* **Passwordless sign-in clears unproven credentials:** when an account email was never confirmed, proven mailbox control wins, so an unproven password and any other linked accounts are removed before sign-in.
|
||||
* **A gate for new identities:** the new `user.validateUserInfo` hook can reject an identity before a user is created or an account is linked, across every sign-up method.
|
||||
|
||||
***
|
||||
|
||||
## Important changes
|
||||
|
||||
### Breaking changes
|
||||
|
||||
| Change | What to do |
|
||||
| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Protected resources replace `validAudiences` | Move each audience into `resources`, link clients to resources, and run the schema migration. |
|
||||
| DPoP changes token verification | Rename the bearer-token helper, use the DPoP request verifier where needed, and run the schema migration. |
|
||||
| Back-channel logout revokes session-bound tokens | Run the schema migration and expect tokens tied to a signed-out session to become inactive. |
|
||||
| Generic OAuth uses the social-provider path | Update sign-in calls, link calls, callback URLs, and client plugins. |
|
||||
| Custom social providers use one identity-token verifier | Replace a provider's `verifyIdToken` method with an `idToken` config. |
|
||||
| The old `oidcProvider` plugin is removed | Move provider setups to `@better-auth/oauth-provider`. |
|
||||
| MCP moves to `@better-auth/mcp` | Update imports, endpoint paths, helper names, config shape, and schema. |
|
||||
| SAML defaults and config change | Update removed fields, update callback URLs, and review IdP-initiated flows. |
|
||||
| SCIM connections need a manual reclaim | Assign an `organizationId` to pre-1.7 connections, or delete unowned rows, before regenerating their tokens. |
|
||||
| Proxy headers are not trusted by default | Opt in with `advanced.trustedProxyHeaders: true` only when your proxy requires it. |
|
||||
| Custom adapters and storage need atomic methods | Implement `incrementOne` and `consumeOne` (adapters), `increment` and `getAndDelete` (secondary storage), or `consume` (rate-limit storage). |
|
||||
| Captcha rules match full paths | Replace partial paths like `/sign-in` with `/sign-in/*` or `/sign-in/**`. |
|
||||
| Custom-scheme trusted origins match by host | A host-bearing entry like `myapp://callback` no longer accepts `myapp://callback.attacker.tld`. Re-check native and mobile `trustedOrigins`. |
|
||||
| Electron requires S256 PKCE | Upgrade the Electron client and server together. |
|
||||
| OIDC ID tokens drop profile/email scope claims | Read profile and email claims from the UserInfo endpoint, not the ID token. |
|
||||
| Synchronous OAuth2 request builders removed | Replace `createAuthorizationCodeRequest`, `createRefreshAccessTokenRequest`, and `createClientCredentialsTokenRequest` with the async equivalents. |
|
||||
| `jwt.sign` callbacks must match `keyPairConfig.alg` | Align your custom ID-token signing alg with the configured key pair, or issuance is rejected. |
|
||||
| Stricter Dynamic Client Registration validation | Send reciprocal `response_types`/`grant_types` (a `code` response type requires the `authorization_code` grant). |
|
||||
| Unauthenticated registration keeps the client auth method | Registrations are confidential by default. Set `token_endpoint_auth_method: "none"` for clients that must stay public. |
|
||||
| `/oauth2/revoke` rejects valid JWT access tokens | Expect `400 unsupported_token_type`. Revoke refresh or opaque tokens instead. |
|
||||
| OAuth callback error code renamed | Update handling of `email_doesn't_match` to `email_does_not_match`. |
|
||||
| `generateState()` signature changed | Call it with the new options object instead of positional `(c, link, additionalData)`. |
|
||||
| SCIM connections are scoped to an organization | `organizationId` is required, `userId` is removed, `defaultSCIM` becomes `staticProviders`, `trustedDomains` is removed. Run the migration. |
|
||||
| SCIM requires the organization plugin | SCIM no longer initializes without it, and every token must be tied to an organization. |
|
||||
| SCIM account IDs are namespaced per organization | Migrate existing SCIM-linked accounts to the `scim:{organizationId}:{providerId}` provider-id form. |
|
||||
| SSO SAML config registration changes | Provide a signing-cert source and expect the full lowercased ACS error-redirect code, not the short alias. |
|
||||
| `/sso/update-provider` rejects partial mappings | Send a complete OIDC/SAML mapping object, not a partial one. |
|
||||
| auth CLI requires Node.js 22.12+ | Upgrade the Node.js used to run the CLI. |
|
||||
| New organization columns | Run the migration for `team.memberCount` and `teamMember.membershipKey`. |
|
||||
| Stripe org subscriptions need `organization.enabled` | `referenceMiddleware` rejects org-scoped subscriptions unless `organization: { enabled: true }` is set in the Stripe plugin config. |
|
||||
| Default Drizzle schema uses singular relation keys | Regenerate and review your Drizzle schema relations. |
|
||||
| Stripe `onSubscriptionCancel` event is required | Update the callback to expect a non-optional `event`. |
|
||||
| Two-factor OTP-only enablement | `enableTwoFactor` takes a new `method` param and returns a discriminated response. Update callers. |
|
||||
| Public export `getIp` renamed to `getIP` | Update imports of the IP helper. |
|
||||
|
||||
### Behavior changes
|
||||
|
||||
| Change | What changes |
|
||||
| --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `max_age` is enforced | Clients that ask for fresh login now get it instead of being ignored. |
|
||||
| Token introspection is consistent | Opaque tokens return the same claims as a JWT, and a resource server can introspect another client's token. |
|
||||
| ID-token claims stay protocol-safe | Custom claims can no longer overwrite reserved protocol claims, and ID tokens report `acr: "0"`. |
|
||||
| Granted scopes are preserved | Later logins no longer erase scopes granted earlier. |
|
||||
| Google One Tap validates the token subject | One Tap signs in the account owner instead of matching on email. |
|
||||
| Magic-link and email-OTP sign-in can clear unproven credentials | Proven mailbox control wins over an unconfirmed password on the same account. |
|
||||
| userinfo rejects bad access tokens with 401 | Invalid tokens get `401 invalid_token` with a `WWW-Authenticate` header. |
|
||||
| OAuth authorize redirects missing-`response_type` errors | The error goes to the verified client `redirect_uri` instead of a generic error. |
|
||||
| Drizzle adapter validates affected-row counts | An invalid affected-row count throws instead of returning `0`. |
|
||||
| `organization.updateTeam` ignores immutable fields | `id`, `createdAt`, and `updatedAt` are no longer accepted in the request body. |
|
||||
| `updateMemberRole` checks authorization first | Role-existence validation runs after authorization checks. |
|
||||
| CLI `generate --output` to a directory | Picks an adapter-specific default filename. |
|
||||
| Generated schema skips migration-disabled models | References to models with migrations disabled are omitted. |
|
||||
| Cookie-cache session is bound to its cookie | The cached session is tied to the `session_token` cookie. |
|
||||
| SSRF host checks cover more reserved ranges | Outbound-host classification now blocks additional reserved ranges (6to4 relay anycast, site-local IPv6, and IPv4-compatible IPv6). |
|
||||
| OAuth token-redemption errors use standard codes | Authorization-code redemption failures return `400 invalid_grant` instead of `401 invalid_client` or `invalid_request`. |
|
||||
| Sign-out runs session-delete hooks with external session stores | `session.delete` hooks now run on sign-out even with `secondaryStorage` and `preserveSessionInDatabase`. |
|
||||
| Two-factor invalidation failure has its own error code | A failed two-factor challenge cleanup now returns `FAILED_TO_INVALIDATE_TWO_FACTOR_CHALLENGE`. |
|
||||
|
||||
### New additions
|
||||
|
||||
| Change | What is available now |
|
||||
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
||||
| Refresh-token retries for native clients | Replay the same refresh response during a short reuse window. |
|
||||
| OAuth provider extension surface | Add grant types, client-authentication methods, discovery metadata, and claims. |
|
||||
| Client ID Metadata Documents | Let clients identify themselves with a hosted metadata document. |
|
||||
| Per-request login options | Pass provider-specific login options from one sign-in request. |
|
||||
| Certificate and signed-assertion login | Use certificate login for Microsoft Entra ID and signed JWTs for generic OAuth. |
|
||||
| Private-key-JWT client auth | Authenticate OAuth provider and SSO clients with a signed JWT (RFC 7523) instead of a shared secret. |
|
||||
| SCIM groups | Manage SCIM groups with durable lifecycle endpoints. |
|
||||
| Request specific user claims | Ask for individual user details with the `claims.userinfo` parameter. |
|
||||
| Drizzle Relations v2 support | A new `@better-auth/drizzle-adapter/relations-v2` entry point merges with your app's relations. |
|
||||
| Sessions and tools | Use public-key session verification, `hydrateSession`, i18n, and `create-admin`. |
|
||||
|
||||
### Deprecations and removals
|
||||
|
||||
| Change | Replacement |
|
||||
| ------------------------------------------ | ------------------------------------ |
|
||||
| `oidcProvider` plugin removed | Use `@better-auth/oauth-provider`. |
|
||||
| MCP plugin path moved out of `better-auth` | Use `@better-auth/mcp`. |
|
||||
| Generic OAuth client APIs changed | Use the standard social client APIs. |
|
||||
|
||||
***
|
||||
|
||||
## Migrating to v1.7
|
||||
|
||||
Because this release carries large changes, we're shipping it as a release candidate first, so there's a window to migrate. We'll keep watching feedback and release the stable 1.7 from there.
|
||||
|
||||
Install it from the `rc` tag, and do the same for any `@better-auth/*` packages you use:
|
||||
|
||||
```package-install
|
||||
better-auth@rc
|
||||
```
|
||||
|
||||
Then run `npx auth generate` for any schema changes. The table below shows whether these changes affect you. For the detailed migration guide, see the [1.7 upgrade guide](/docs/guides/1-7-upgrade-guide).
|
||||
|
||||
| If you use | What to expect |
|
||||
| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
||||
| Basic Better Auth setup | Usually just the `@rc` install and any generated schema changes |
|
||||
| Social login, generic OAuth, One Tap, or SSO | New login-client behavior, especially generic OAuth, identity-token verification, and Google One Tap |
|
||||
| `@better-auth/oauth-provider` | The largest set of changes: resources, DPoP, back-channel logout, `max_age`, and safer OAuth checks |
|
||||
| MCP | A package move to `@better-auth/mcp`, new imports, new endpoint paths, and a schema migration |
|
||||
| SAML or SCIM | Safer SAML and SSO checks, and a manual SCIM ownership cleanup |
|
||||
| Magic links or email OTP | Safer handling for accounts whose email was never confirmed |
|
||||
| Custom adapters, storage, or rate-limit stores | New atomic methods are required |
|
||||
| Custom proxy or TLS termination | Check how your app computes its public origin before relying on DPoP or identity-provider redirects |
|
||||
|
||||
***
|
||||
|
||||
## Contributors
|
||||
|
||||
Thanks to all the contributors for making this release possible!
|
||||
|
||||
export const releaseContributorUsernames = [
|
||||
// cspell:disable
|
||||
"0-Sandy",
|
||||
"aarmful",
|
||||
"adityachaudhary99",
|
||||
"adrianmxb",
|
||||
"ahmedivy",
|
||||
"allandelmare",
|
||||
"Andrew1326",
|
||||
"arnnvv",
|
||||
"baptisteArno",
|
||||
"Bekacru",
|
||||
"Bekione",
|
||||
"bennettdams",
|
||||
"benpsnyder",
|
||||
"brentmitchell25",
|
||||
"brone1323",
|
||||
"bytaesu",
|
||||
"Byte-Biscuit",
|
||||
"cb-alish",
|
||||
"chdanielmueller",
|
||||
"ChrisMGeo",
|
||||
"Craga89",
|
||||
"cyphercodes",
|
||||
"demhadais",
|
||||
"dipan-ck",
|
||||
"DougInAMug",
|
||||
"dvanmali",
|
||||
"ejirocodes",
|
||||
"ElGauchooooo",
|
||||
"elliotBraem",
|
||||
"eluce2",
|
||||
"erquhart",
|
||||
"FaryalRizwaan",
|
||||
"florianamette",
|
||||
"formatlos",
|
||||
"frankeld",
|
||||
"GautamBytes",
|
||||
"GoPro16",
|
||||
"gustavovalverde",
|
||||
"himself65",
|
||||
"IcanDivideBy0",
|
||||
"IdrisGit",
|
||||
"ItalyPaleAle",
|
||||
"Jadenstanton",
|
||||
"jaydeep-pipaliya",
|
||||
"jjluzgin",
|
||||
"jonathansamines",
|
||||
"jsj",
|
||||
"kgarg2468",
|
||||
"Kinfe123",
|
||||
"Kvizas",
|
||||
"lubiah",
|
||||
"mausic",
|
||||
"moonevm",
|
||||
"MuzzaiyyanHussain",
|
||||
"nphlp",
|
||||
"Oluwatobi-Mustapha",
|
||||
"onmax",
|
||||
"OscarCornish",
|
||||
"ouwargui",
|
||||
"Paola3stefania",
|
||||
"pbacza",
|
||||
"peyremorgan",
|
||||
"pi0",
|
||||
"ping-maxwell",
|
||||
"programming-with-ia",
|
||||
"rachit367",
|
||||
"ramonclaudio",
|
||||
"reslear",
|
||||
"ruban-s",
|
||||
"Saiyaswanthpasupuleti",
|
||||
"seanfilimon",
|
||||
"seebykilian",
|
||||
"SferaDev",
|
||||
"skalkii",
|
||||
"sleepe229",
|
||||
"sovetski",
|
||||
"stewartjarod",
|
||||
"TanishValesha",
|
||||
"terijaki",
|
||||
"tonytkachenko",
|
||||
"tsushanth",
|
||||
"Tushar-Khandelwal-2004",
|
||||
"Vishesh-Verma-07",
|
||||
"WilsonnnTan",
|
||||
"wobedi",
|
||||
"yordis",
|
||||
"zeroknowledge0x",
|
||||
"zllovesuki",
|
||||
// cspell:enable
|
||||
];
|
||||
|
||||
<Contributors usernames={releaseContributorUsernames} />
|
||||
65
docs/content/blogs/road-to-v2.mdx
Normal file
65
docs/content/blogs/road-to-v2.mdx
Normal file
@@ -0,0 +1,65 @@
|
||||
---
|
||||
title: "Road to Better Auth v2"
|
||||
description: "Better Auth 1.7 is the last feature release on v1, which moves into maintenance as we turn to v2."
|
||||
date: 2026-06-29
|
||||
draft: false
|
||||
author:
|
||||
name: "Taesu Kim"
|
||||
avatar: ""
|
||||
tags: ["1.7", "v1", "maintenance", "v2"]
|
||||
---
|
||||
|
||||
# Road to Better Auth v2
|
||||
|
||||
Better Auth 1.7 is available as a release candidate, the final feature release on v1 before it moves into maintenance and we focus on v2.
|
||||
|
||||
## v1 enters maintenance mode
|
||||
|
||||
After 1.7 becomes stable, Better Auth v1 will enter maintenance mode.
|
||||
|
||||
In maintenance, v1 keeps receiving security fixes, bug fixes, and compatibility fixes. We will avoid adding new features to v1 unless a change is part of maintaining the current major version, such as an integration fix, a missing compatibility case that 1.7 should have covered, or a small API addition needed to unblock a correctness or security fix.
|
||||
|
||||
We do not expect another feature-focused v1 minor release after 1.7. If a maintenance fix needs a breaking change, or a v1 compatibility problem cannot be handled safely in a patch, we may still publish a v1 minor for that purpose. That release would be scoped to maintaining v1, not to opening a new feature track.
|
||||
|
||||
| After the stable 1.7 release | What to expect |
|
||||
| -------------------------------------------- | -------------------------------------------------------- |
|
||||
| Security fixes | Yes |
|
||||
| Bug fixes | Yes |
|
||||
| Compatibility fixes for existing v1 surfaces | Yes |
|
||||
| New v1 features | Avoided by default |
|
||||
| More v1 minor releases | Not expected, except for maintenance cases that need one |
|
||||
| v2 work | Main product focus |
|
||||
|
||||
This does not mean v1 is being abandoned or that it is flawed. It is still a solid choice to build on.
|
||||
|
||||
Better Auth has grown, together with its community, into a very broad framework. Continuing to add features and patches on top of that would make it much harder to design a more solid next architecture, and much harder to explain how it is structured and used.
|
||||
|
||||
## Why we're preparing for v2
|
||||
|
||||
Better Auth now covers many different use cases. Some teams only need sign-in and session handling. Some need a full user-management surface. Some need enterprise SSO. Some need Better Auth to act as an OAuth or OpenID Connect provider for other apps. Some need MCP and machine-client flows.
|
||||
|
||||
These use cases should not all have to carry the same shape. As v1 grew to cover them, every project ended up taking on the whole framework, whether it needed it or not. v2 is our chance to improve the foundation.
|
||||
|
||||
## What v2 will look like
|
||||
|
||||
In v2, we want Better Auth to stay familiar while becoming more modular. A small app should be able to install only the parts it needs for sign-in and sessions, without taking on the full user-management or provider-side model. A product that needs user management should be able to add that surface explicitly. A project that needs Better Auth as an identity provider should be able to install the provider-side pieces, storage, and adapters for that job.
|
||||
|
||||
That direction needs deeper changes than a normal minor release can carry. The package boundaries, schema, and adapter model need to change, and some names and contracts need to become more honest about what they represent.
|
||||
|
||||
We are not ready to publish the full v2 design yet, and we do not want this post to read as a promise about final APIs. What matters for now is the direction. v2 will be more modular, more explicit about storage and identity boundaries, and better suited to the different and evolving ways people use auth.
|
||||
|
||||
<Callout type="info">
|
||||
Share your ideas and feedback in the [Better Auth v2 discussion](https://github.com/better-auth/better-auth/discussions/10271).
|
||||
</Callout>
|
||||
|
||||
## What this means if you're on v1
|
||||
|
||||
If you are building on v1 today, you can keep going. v1 will stay maintained, and after the stable 1.7 release it should become even more stable and predictable. That stability is part of the plan, not a sign that v1 is going away.
|
||||
|
||||
When you are ready, the [1.7 RC](/blog/1-7-rc) is the best way to get the latest v1 behavior ahead of the stable release. The [1.7 upgrade guide](/docs/guides/1-7-upgrade-guide) covers the changes for the parts of Better Auth you use. We will actively monitor feedback during the release-candidate period and resolve any blocking issues before the stable release.
|
||||
|
||||
## Feedback is welcome
|
||||
|
||||
If something in the 1.7 RC does not work the way you expect, or an upgrade step is unclear, let us know in our [community](/community). You are also welcome to share your ideas on v2 in the [Better Auth v2 discussion](https://github.com/better-auth/better-auth/discussions/10271).
|
||||
|
||||
Thank you for being part of Better Auth as it grows together with its community.
|
||||
507
docs/content/docs/guides/1-7-upgrade-guide.mdx
Normal file
507
docs/content/docs/guides/1-7-upgrade-guide.mdx
Normal file
@@ -0,0 +1,507 @@
|
||||
---
|
||||
title: Upgrading to Better Auth 1.7
|
||||
description: Upgrade Better Auth from 1.6 to 1.7, including OAuth, OpenID Connect, MCP, SAML, SCIM, proxy, and custom adapter changes.
|
||||
---
|
||||
|
||||
Most Better Auth 1.7 changes are additive. Most projects start with one command:
|
||||
|
||||
```package-install
|
||||
npx auth upgrade
|
||||
```
|
||||
|
||||
Some areas need more care: OAuth, OpenID Connect, SAML, SCIM, two-factor authentication, MCP, custom storage, and proxy setups. Use this table to choose the sections that apply to your project.
|
||||
|
||||
| If your project | Read |
|
||||
| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
|
||||
| Uses Better Auth at all | [Before you upgrade](#before-you-upgrade) and [Behind a proxy](#behind-a-proxy) |
|
||||
| Uses social login, generic OAuth, One Tap, or SSO | [As a login client](#as-a-login-client) |
|
||||
| Runs its own OAuth or OpenID provider | [As an identity provider](#as-an-identity-provider) |
|
||||
| Runs MCP | [MCP](#mcp) |
|
||||
| Uses SAML, SSO domain verification, or SCIM | [Enterprise SSO](#enterprise-sso) |
|
||||
| Uses Stripe billing | [Stripe](#stripe) |
|
||||
| Uses two-factor auth, magic links, or email OTP | [Two-factor and passwordless security](#two-factor-and-passwordless-security) |
|
||||
| Uses a custom database adapter, secondary storage, or rate-limit store | [Custom adapters and storage](#custom-adapters-and-storage) |
|
||||
|
||||
## Before you upgrade
|
||||
|
||||
The upgrade command handles package updates. The database needs more care. A few 1.7 features add or change tables, and some also need a manual data step.
|
||||
|
||||
<Callout type="warn">
|
||||
Run the schema migration before deploying 1.7 if any changed feature applies to your project.
|
||||
</Callout>
|
||||
|
||||
Use the CLI migration commands:
|
||||
|
||||
```bash title="Terminal"
|
||||
npx auth@latest generate
|
||||
npx auth@latest migrate
|
||||
```
|
||||
|
||||
If you manage your own schema with Drizzle or Prisma, run `generate` and apply the result through your own migration tooling.
|
||||
|
||||
The `auth` CLI now requires Node.js 22.12 or newer to run.
|
||||
|
||||
These features change the schema:
|
||||
|
||||
| Feature | What it adds | Converts data for you? |
|
||||
| -------------------------- | ------------------------------------------------------------------------------------ | ------------------------ |
|
||||
| Protected resources | New resource tables and key columns | Not needed (config move) |
|
||||
| Resource-bound tokens | Resource columns on the token tables | Yes |
|
||||
| DPoP | A token-binding column | Not needed |
|
||||
| Refresh-token reuse window | A cached replay-response column on refresh tokens | Not needed |
|
||||
| Authorization-code replay | An indexed `authorizationCodeId` column on both token tables | Not needed |
|
||||
| Back-channel logout | Logout-URL and revoked columns | Not needed |
|
||||
| Requested user-info claims | A requested-claims column on the token and consent tables | Not needed |
|
||||
| SCIM org scoping | `organizationId` required, `userId` removed, `providerKey` added | **No, reclaim old rows** |
|
||||
| Organization team counters | `team.memberCount` and `teamMember.membershipKey` columns | Not needed |
|
||||
| SCIM groups | New `scimGroup`, `scimGroupMember`, `scimGroupRole`, and `scimGroupRoleGrant` tables | Not needed |
|
||||
|
||||
The SCIM reclaim is the one manual data step. Plan for it before you cut over.
|
||||
|
||||
<Callout type="warn">
|
||||
The SCIM reclaim is not automatic. Connections created before 1.7 may have no organization, so you must delete or assign an `organizationId` to rows without one before regenerating their tokens.
|
||||
</Callout>
|
||||
|
||||
***
|
||||
|
||||
## As a login client
|
||||
|
||||
This covers social login, the generic OAuth plugin, One Tap, and consuming SSO.
|
||||
|
||||
### Generic OAuth is rebuilt on the social-provider path
|
||||
|
||||
The generic OAuth plugin now works like the built-in social providers.
|
||||
|
||||
**What to do:**
|
||||
|
||||
* Replace `signIn.oauth2({ providerId })` with `signIn.social({ provider })`.
|
||||
* Replace `oauth2.link()` with `linkSocial()`.
|
||||
* Update your provider callback URL from `/api/auth/oauth2/callback/:id` to `/api/auth/callback/:id`.
|
||||
* Remove `genericOAuthClient()` from your client plugins and use the standard social client APIs.
|
||||
* PKCE now defaults to on. Set `pkce: false` only for a provider that rejects it.
|
||||
* Remove `issuer` and `requireIssuerValidation`; issuer validation is now automatic.
|
||||
* `authorizationUrlParams` and `tokenUrlParams` now accept only plain string maps.
|
||||
|
||||
### Identity tokens go through one verifier
|
||||
|
||||
Each provider used to verify its own identity tokens. Now there is one verifier, and each provider declares an `idToken` config: keys, issuer, and audience.
|
||||
|
||||
**What to do:** a custom provider should replace its `verifyIdToken` method with an `idToken` config. PayPal no longer accepts identity-token login; it uses its access token instead. Switch PayPal identity-token login to the redirect flow. The built-in provider options are unchanged.
|
||||
|
||||
### Electron requires modern PKCE
|
||||
|
||||
The Electron login flow now requires S256 PKCE, no longer trusts a custom origin header, and matches custom URL schemes more safely.
|
||||
|
||||
**What to do:** upgrade the `@better-auth/electron` client and server together. Make sure your app URL scheme is in `trustedOrigins`. Remove the old `disableOriginOverride` option. Review trusted entries with a host, such as `myapp://callback`, because they no longer match lookalike hosts.
|
||||
|
||||
### `generateState()` signature changed
|
||||
|
||||
The public `generateState()` helper now takes an options object instead of positional arguments.
|
||||
|
||||
**What to do:** if you call `generateState()` directly, replace `generateState(c, link, additionalData)` with the options form `generateState(c, options)`.
|
||||
|
||||
### OAuth callback error code renamed
|
||||
|
||||
The OAuth callback redirect error value `email_doesn't_match` is renamed `email_does_not_match`.
|
||||
|
||||
**What to do:** if you read this error code from the callback redirect, update the string to `email_does_not_match`.
|
||||
|
||||
### Google One Tap signs in the right person
|
||||
|
||||
One Tap now signs in the user who actually owns the Google account by validating the identity token's subject, rather than matching on email.
|
||||
|
||||
**What to do:** nothing to configure. If you relied on email-based matching, expect One Tap to bind to the Google account subject instead.
|
||||
|
||||
### Scopes are kept across logins
|
||||
|
||||
Granted scopes used to overwrite each other. A permission granted earlier could disappear after a later login that asked for less access.
|
||||
|
||||
Better Auth now keeps the scopes already on the account across re-login and token refresh, using the existing `account.scope` field. This is a behavior fix: no schema change, no backfill, and no action for most projects.
|
||||
|
||||
If you tracked the 1.7 prereleases, note that an early beta moved scopes into a `grantedScopes` array and a later beta reverted it. The shipped 1.7 keeps the original `account.scope` string. If you adopted the `grantedScopes` column from an early beta, drop it and restore `scope` on every schema you changed. See [Tracking prereleases](#tracking-prereleases).
|
||||
|
||||
### Discovery providers verify their identity tokens
|
||||
|
||||
A generic OAuth provider configured with a discovery URL now verifies the provider identity token. A token that fails verification is rejected.
|
||||
|
||||
**What to do:** if a discovery provider returned a token that could not be verified and your app trusted it anyway, the login is now rejected. Confirm the provider's published keys, issuer, and audience.
|
||||
|
||||
### Signed-assertion login validates at startup
|
||||
|
||||
A signed-assertion login setup, also called private-key JWT, is now checked when it is created. An unsupported algorithm, a key with no material, or a mismatch between the declared algorithm and the key now fails immediately instead of silently doing the wrong thing.
|
||||
|
||||
**What to do:** fix any signing setup whose declared algorithm disagrees with the key. Replace `createAuthorizationCodeRequest`, `createRefreshAccessTokenRequest`, and `createClientCredentialsTokenRequest` with the async `authorizationCodeRequest`, `refreshAccessTokenRequest`, and `clientCredentialsTokenRequest`.
|
||||
|
||||
### Anonymous account linking works in mobile and in-app browsers
|
||||
|
||||
Linking an anonymous account after a social login now works in Expo and other in-app browsers, where the callback returns without the usual cookie. A new `addOAuthServerContext` API carries trusted data across the login that a client cannot forge.
|
||||
|
||||
**What to do:** nothing for most apps. If you carried anonymous-link state across the OAuth redirect yourself, move it onto `addOAuthServerContext`.
|
||||
|
||||
***
|
||||
|
||||
## As an identity provider
|
||||
|
||||
This covers `@better-auth/oauth-provider`. Several changes need a schema migration; run `generate` and `migrate` once after applying them.
|
||||
|
||||
### Protected resources replace the audience list
|
||||
|
||||
Audiences are now resources. Each resource can have its own token lifetime, scopes, claims, and signing keys. The old `validAudiences` list is removed.
|
||||
|
||||
**What to do:**
|
||||
|
||||
* Move each entry from `validAudiences` into `resources`.
|
||||
* Link clients to specific resources through `oauthClientResource` or registration.
|
||||
* Run `generate` and `migrate` to add the new tables and columns.
|
||||
* Check your refresh-token lifetimes: the shortest applicable lifetime now wins, so a per-resource value longer than the provider default is capped at the default.
|
||||
|
||||
**If you accept dynamically registered clients,** the resource model enforces per-client resource access by default. A dynamic client's token request can be rejected with `invalid_target`. Set `enforcePerClientResources: false` for that case, and register each client's `grant_types` explicitly, or the token endpoint rejects them with `unauthorized_client`.
|
||||
|
||||
### Token target is locked to the login
|
||||
|
||||
The API a token is for is now captured at login and locked to that grant. A later request can narrow the target API but cannot widen it. Asking for an API the login did not cover is rejected. A custom-claims callback now receives a list of resources instead of one value.
|
||||
|
||||
**What to do:** run the migration to add the resource columns. Update custom-claims callbacks to read the resource list. Make sure clients ask only for resources their login covered.
|
||||
|
||||
### DPoP renames the token verifier
|
||||
|
||||
The plain token-checking helper `verifyAccessToken` is renamed `verifyBearerToken` and now rejects DPoP tokens. Use the new `verifyAccessTokenRequest` on endpoints that may receive DPoP requests.
|
||||
|
||||
**What to do:** rename `verifyAccessToken` to `verifyBearerToken`, and switch DPoP-capable endpoints to `verifyAccessTokenRequest`. Run `generate` and `migrate` to add the token-binding column. To support DPoP, configure database-backed verification storage.
|
||||
|
||||
**Watch the proxy case.** Native DPoP checks the proof's `htu` claim against the URL the token endpoint computes for itself. Behind a TLS-terminating proxy or a custom server, that computed URL can be the internal bind address (`http://0.0.0.0:3000`) or the proxy's internal scheme and port. The client signed `htu` from your public discovery URL, so a valid proof can be rejected. Canonicalize the incoming request's scheme and host to your configured `baseURL` at the route boundary before the provider reads it.
|
||||
|
||||
### Sign-out revokes session tokens
|
||||
|
||||
When a session ends, the access tokens tied to it are now revoked. They read as inactive at introspection and userinfo. Before, they lived until they expired. Your server also sends a logout message to each app that registered a logout URL.
|
||||
|
||||
**What to do:** run `generate` and `migrate` to add the new columns. Expect session-bound tokens to stop working at sign-out. On serverless platforms, set `advanced.backgroundTasks.handler` so sending logout messages does not slow down sign-out.
|
||||
|
||||
### Custom ID-token claims cannot override protocol claims
|
||||
|
||||
Your custom ID-token claims can no longer set protocol claims that the standard reserves for the server: issuer, subject, audience, expiry, nonce, session binding, `auth_time`, `acr`, `amr`, and `azp`. Your own namespaced claims still appear. ID tokens also report `acr: "0"` rather than a vendor-specific value.
|
||||
|
||||
**What to do:** if a `customIdTokenClaims` callback, an extension claim contributor, or a per-issuance `idTokenClaims` set one of those reserved claims, that value is now ignored. Move the data into a namespaced claim of your own, or rely on the server's value.
|
||||
|
||||
### ID tokens drop profile and email scope claims
|
||||
|
||||
ID tokens issued through the authorization-code flow no longer carry the profile and email scope claims. Those claims are available from the UserInfo endpoint.
|
||||
|
||||
**What to do:** read profile and email claims from UserInfo instead of the ID token.
|
||||
|
||||
### `jwt.sign` callbacks must match the configured alg
|
||||
|
||||
A custom `jwt.sign` callback is rejected when its algorithm differs from `keyPairConfig.alg` during ID-token issuance.
|
||||
|
||||
**What to do:** align your custom signing algorithm with `keyPairConfig.alg`.
|
||||
|
||||
### `/oauth2/revoke` rejects valid JWT access tokens
|
||||
|
||||
Revoking a still-valid JWT access token now returns `400 unsupported_token_type`.
|
||||
|
||||
**What to do:** revoke refresh tokens or opaque access tokens; do not call revoke on JWT access tokens.
|
||||
|
||||
### `max_age` is enforced
|
||||
|
||||
When a client asks for `max_age` and the user's login is older than that, the provider sends them back to log in. Before, the request was ignored.
|
||||
|
||||
**What to do:** nothing to configure. If a client sent `max_age` expecting it to be ignored, expect a re-login prompt now.
|
||||
|
||||
**Watch your date columns.** The `max_age` check reads a session's creation time back as a date. If your custom schema stores session timestamps as text instead of a real date or integer-timestamp type, the check can misread the value and send users into a login loop. Store `user`, `session`, `account`, and `verification` timestamps with a date or timestamp type. The Better Auth CLI generates the correct type.
|
||||
|
||||
### Client creation returns 201
|
||||
|
||||
Creating a client now returns `201 Created` instead of `200 OK`, and the registration endpoint enforces the same permission checks as the manual create endpoints.
|
||||
|
||||
**What to do:** update any client that expects a `200` from client creation to accept `201`. To allow machine clients to register, configure `validateInitialAccessToken`.
|
||||
|
||||
### Unauthenticated registration keeps the client's auth method
|
||||
|
||||
Dynamic Client Registration without a logged-in user no longer forces the client to be public. A client that omits `token_endpoint_auth_method` is now confidential with the RFC 7591 default `client_secret_basic` and a generated secret; it becomes public only when it registers `token_endpoint_auth_method: "none"`.
|
||||
|
||||
**What to do:** if you relied on unauthenticated registrations being downgraded to public, register `token_endpoint_auth_method: "none"` explicitly for clients that must stay public.
|
||||
|
||||
### Registration requires reciprocal response and grant types
|
||||
|
||||
A registered client's `response_types` and `grant_types` must now be reciprocal: a `code` response type requires the `authorization_code` grant, and a token grant requires its matching response type. Mismatched registrations are rejected.
|
||||
|
||||
**What to do:** register matching `response_types` and `grant_types` for each client.
|
||||
|
||||
### OAuth endpoints return standard error envelopes
|
||||
|
||||
Validation and malformed-request failures on the OAuth endpoints (token, authorize, revoke, introspect, register, end-session) now return RFC 6749 `{ error, error_description }` envelopes instead of the previous generic validation-error shape.
|
||||
|
||||
**What to do:** if a client or tool parsed the old error shape, update it to read `error` and `error_description`.
|
||||
|
||||
### Introspection returns consistent claims
|
||||
|
||||
`/oauth2/introspect` now returns the same claims for an opaque token as it does for a JWT, and a resource server can introspect a token issued to a different client.
|
||||
|
||||
**What to do:** nothing. Expect richer, consistent introspection responses for opaque tokens.
|
||||
|
||||
### UserInfo accepts a bearer token in the form body
|
||||
|
||||
The userinfo endpoint now accepts the access token in a form-encoded body and rejects a request that sends the token in both the header and the body.
|
||||
|
||||
**What to do:** send the access token in one place, the `Authorization` header or the form body, not both.
|
||||
|
||||
### Client authentication is tied to the grant
|
||||
|
||||
A custom client-authentication method registered through the extension surface can now only prove which client is calling. The server decides what that client is allowed to do.
|
||||
|
||||
**What to do:** companion plugins that added a client-authentication method should rely on the server-resolved client rather than returning their own client decision.
|
||||
|
||||
### Server-side OAuth requests refuse redirects
|
||||
|
||||
Better Auth now refuses HTTP redirects on the server-side OAuth requests it makes: token exchange, token refresh, client credentials, token introspection, and JWKS requests. Conformant OAuth providers answer these endpoints directly and do not redirect.
|
||||
|
||||
**What to do:** nothing for standard providers. If a custom provider endpoint redirects, make it return the final response directly.
|
||||
|
||||
### PKCE requirements for confidential and OIDC clients
|
||||
|
||||
PKCE is always required for public clients. A confidential client registered through Dynamic Client Registration can opt out with `clientRegistrationRequirePKCE: false`. A request that carries the `offline_access` scope still requires PKCE unless it is an OIDC request with a `nonce`, which a confidential client can use instead.
|
||||
|
||||
**What to do:** set `clientRegistrationRequirePKCE: false` only for confidential clients that cannot use PKCE. Send a `nonce` if a confidential OIDC client needs `offline_access` without PKCE.
|
||||
|
||||
### Authorize accepts form-encoded requests and rejects request objects
|
||||
|
||||
The authorization and userinfo endpoints now accept form-encoded (POST) requests, and the authorization endpoint explicitly rejects the OIDC `request` and `request_uri` parameters it does not support.
|
||||
|
||||
**What to do:** nothing for standard clients.
|
||||
|
||||
### Refresh-token retries can be tolerated
|
||||
|
||||
The OAuth provider can replay the same refresh response for duplicate refresh requests during `refreshTokenReuseInterval`. Strict refresh-token replay handling remains the default.
|
||||
|
||||
**What to do:** set `refreshTokenReuseInterval` only if a public or native client can retry a refresh request with an old token after another local session already rotated it. MCP defaults this window to 30 seconds for native and public clients. Set `refreshTokenReuseInterval: 0` to keep strict replay handling.
|
||||
|
||||
### If you extended the OAuth provider by hand
|
||||
|
||||
If you added custom grants, claims, or client-authentication methods by patching or forking the OAuth provider, use the supported extension surface instead of re-applying a patch. Register your contributions with `extendOAuthProvider(ctx, ...)` from your plugin's `init(ctx)` hook. Mint tokens with `provider.issueTokens(...)`, authenticate a client with `provider.authenticateClient(...)`, and hash a token with `provider.hashToken(...)`. Bind a token's audience by passing `resources` to `issueTokens`; the server owns the audience.
|
||||
|
||||
A contribution written in an older or hand-rolled shape can fail silently here. It may type-check and run while the grant or claim never reaches a token. After moving each one into `init()`, confirm the grant or claim reaches a real token.
|
||||
|
||||
### The old `oidcProvider` plugin is removed
|
||||
|
||||
**What to do:** replace `oidcProvider` from `better-auth/plugins` with `oauthProvider` from `@better-auth/oauth-provider`, move your config across, and run the schema migration.
|
||||
|
||||
***
|
||||
|
||||
## MCP
|
||||
|
||||
### MCP moves to its own package
|
||||
|
||||
The MCP plugin moves from `better-auth` into `@better-auth/mcp`, built on the OAuth provider. This is the largest single change for existing MCP users.
|
||||
|
||||
**What to do:**
|
||||
|
||||
1. Install `@better-auth/mcp` and update imports: the server plugin and helpers from `@better-auth/mcp`, the client and adapters from `@better-auth/mcp/client` and `@better-auth/mcp/client/adapters`.
|
||||
2. Add the `jwt()` plugin, which is now required.
|
||||
3. Move options that were nested under `oidcConfig` up to the top level of `mcp({ ... })`, and add a resource identifier, for example `resource: "https://api.example.com/mcp"`.
|
||||
4. Rename `withMcpAuth` to `requireMcpAuth`, and `createMcpAuthClient` to `createMcpResourceClient`.
|
||||
5. Regenerate or migrate your schema.
|
||||
|
||||
The OAuth endpoints move from `/mcp/*` to `/oauth2/*`. Discovery-based MCP clients find the new locations on their own. MCP also defaults `refreshTokenReuseInterval` to 30 seconds for native and public clients; set it to `0` if you want strict refresh-token replay handling.
|
||||
|
||||
***
|
||||
|
||||
## Enterprise SSO
|
||||
|
||||
### IdP-initiated SAML is off 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, and Single Logout requests are matched to their session by `SessionIndex`.
|
||||
|
||||
**What to do:** set `saml.allowIdpInitiated: true` to restore the old behavior if you depend on it.
|
||||
|
||||
### SAML certificates can be a list
|
||||
|
||||
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.
|
||||
|
||||
**What to do:** update any code that reads 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 configuration is simplified
|
||||
|
||||
The callback URL is derived automatically, service-provider metadata is generated for you, and several fields are removed. One endpoint path changes, and error codes in the redirect change from short aliases to the full lowercased internal code (for example `saml_multiple_assertions`).
|
||||
|
||||
**What to do:** remove `callbackUrl`, the empty `spMetadata`, and the removed fields from your config. Update your provider callback URL to `/sso/saml2/sp/acs/:providerId`. Set the post-login redirect with `callbackURL` in `signIn.sso()`. If you read SAML error codes from the redirect URL, switch to the lowercased codes.
|
||||
|
||||
### SCIM writes are safer
|
||||
|
||||
SCIM user writes now honor the `active` attribute, and non-organization SCIM deletes only remove the global user when the SCIM account is their only identity. `PUT` and `PATCH` reject changing a user's email to one already used by another user.
|
||||
|
||||
**What to do:** if your SCIM client sends `active: false`, Better Auth now deactivates the user through the admin plugin and revokes their sessions. If your SCIM client changes emails, handle `409` conflicts. Honoring `active` requires the admin plugin.
|
||||
|
||||
### SCIM connections are scoped to an organization
|
||||
|
||||
SCIM connections now require `organizationId`, add a `providerKey` column, and drop the old `userId` column. The `defaultSCIM` option becomes `staticProviders`, `trustedDomains` is removed, and provider IDs are namespaced per organization. The old per-user ownership option (`providerOwnership`) is removed, so every connection is bound to an organization.
|
||||
|
||||
**What to do:** run the migration, set `organizationId` on connections, rename the `defaultSCIM` config to `staticProviders`, and remove `trustedDomains`. Reclaim pre-1.7 connections that have no organization by assigning an `organizationId` or deleting them, then regenerate their tokens.
|
||||
|
||||
### `/sso/update-provider` rejects partial mappings
|
||||
|
||||
Updating an SSO provider now rejects a partial OIDC or SAML mapping object.
|
||||
|
||||
**What to do:** send a complete mapping object when you call `/sso/update-provider`.
|
||||
|
||||
### SCIM gains durable group resources
|
||||
|
||||
SCIM now manages groups with membership, roles, and lifecycle endpoints, backed by new `scimGroup`, `scimGroupMember`, `scimGroupRole`, and `scimGroupRoleGrant` tables.
|
||||
|
||||
**What to do:** run `generate` and `migrate` to add the SCIM group tables before using group provisioning.
|
||||
|
||||
### 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.
|
||||
|
||||
**What to do:** nothing. If a provider endpoint redirects, point your config at the final URL.
|
||||
|
||||
***
|
||||
|
||||
## Stripe
|
||||
|
||||
### Organization subscriptions require `organization.enabled`
|
||||
|
||||
`referenceMiddleware` now rejects organization-scoped subscriptions unless `organization: { enabled: true }` is set in the Stripe plugin config.
|
||||
|
||||
**What to do:** set `organization: { enabled: true }` in your `stripe()` plugin options for organization-scoped subscriptions. The organization plugin is still needed separately to resolve the active organization.
|
||||
|
||||
### `onSubscriptionCancel` event is required
|
||||
|
||||
The `event` parameter on the `onSubscriptionCancel` callback is now required.
|
||||
|
||||
**What to do:** update your `onSubscriptionCancel` callback to expect a non-optional `event`.
|
||||
|
||||
***
|
||||
|
||||
## Behind a proxy
|
||||
|
||||
### Forwarded headers are not trusted by default
|
||||
|
||||
When your app reads its own address, it now uses the `Host` header and ignores forwarded headers unless you opt in.
|
||||
|
||||
**What to do:** if your proxy exposes the public hostname only through `x-forwarded-host`, opt in:
|
||||
|
||||
```ts
|
||||
betterAuth({
|
||||
baseURL: { allowedHosts: [...] },
|
||||
advanced: {
|
||||
trustedProxyHeaders: true, // [!code highlight]
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Setups where the proxy rewrites the host for you, such as nginx, Vercel, Cloudflare, and Netlify, need no change.
|
||||
|
||||
### IdP redirects and DPoP need your canonical origin
|
||||
|
||||
Two OAuth-provider behaviors read the incoming request origin. Behind a custom server or TLS-terminating proxy, that origin can be the internal bind address rather than your public origin. The provider returns your `consentPage` and `loginPage` as relative paths, so a server-side redirect, such as `NextResponse.redirect`, needs them resolved against an absolute origin. Native DPoP also compares the proof's `htu` against the URL the token endpoint computes for itself.
|
||||
|
||||
**What to do:** treat `baseURL` as the server identity, and canonicalize the incoming request scheme and host to it at the route boundary before the provider reads the request. One helper, applied wherever a route forwards to the provider, fixes consent redirects, the DPoP `htu` check, and origin checks together.
|
||||
|
||||
***
|
||||
|
||||
## Custom adapters and storage
|
||||
|
||||
The atomic-state work introduces required methods. If you use only the built-in adapters and storage, you can skip this.
|
||||
|
||||
### Database adapters must implement `incrementOne` and `consumeOne`
|
||||
|
||||
`incrementOne` updates one row's counter atomically and returns the row, or null when the guard did not match. `consumeOne` reads and deletes a row in one step for single-use credentials. Both are now required, and the old fallback is gone.
|
||||
|
||||
**What to do:** implement both `incrementOne` and `consumeOne` in any custom adapter. A missing `consumeOne` throws at runtime. All built-in adapters already do.
|
||||
|
||||
### Secondary storage must implement `increment` and `getAndDelete`
|
||||
|
||||
`increment(key, ttl)` bumps a counter by one and sets the expiry only when the key is first created. `getAndDelete(key)` reads and removes a key in one step. Both were optional before and are now required.
|
||||
|
||||
**What to do:** implement both `increment` and `getAndDelete` in custom secondary storage. Redis storage already does.
|
||||
|
||||
### Rate-limit storage uses `consume`
|
||||
|
||||
Rate-limit storage now needs a single `consume(key, rule)` method that checks and increments in one step. Separate `get` and `set` are no longer accepted.
|
||||
|
||||
**What to do:** replace `get` and `set` in custom rate-limit storage with `consume`.
|
||||
|
||||
### Default Drizzle schema uses singular relation keys
|
||||
|
||||
The default Drizzle schema generator now emits singular relation keys.
|
||||
|
||||
**What to do:** regenerate your Drizzle schema and review the relation keys.
|
||||
|
||||
### `getIp` is renamed `getIP`
|
||||
|
||||
The public `getIp` export is renamed `getIP`.
|
||||
|
||||
**What to do:** update imports of the IP helper to `getIP`.
|
||||
|
||||
***
|
||||
|
||||
## Captcha
|
||||
|
||||
### Captcha matches full paths
|
||||
|
||||
Captcha rules now match full request paths or explicit wildcards, which closes a way to skip a captcha rule through partial path matching.
|
||||
|
||||
**What to do:** replace a partial path like `/sign-in` with `/sign-in/*` or `/sign-in/**`.
|
||||
|
||||
***
|
||||
|
||||
## Two-factor and passwordless security
|
||||
|
||||
### Two-factor `enableTwoFactor` returns a discriminated response
|
||||
|
||||
`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.
|
||||
|
||||
**What to do:** update callers that read `totpURI` or backup codes to branch on the returned `method`.
|
||||
|
||||
### Magic-link and email-OTP sign-in can 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.
|
||||
|
||||
**What to do:** if a user signed up with email and password but first signs in through a magic link or email OTP instead of confirming the verification email, ask them to set a new password through password reset.
|
||||
|
||||
***
|
||||
|
||||
## Behavior changes worth noticing
|
||||
|
||||
These do not need action for most setups, but they change what you see.
|
||||
|
||||
* **Safer OAuth token handling:** a refresh token used by a different client, a replayed authorization code, and a `redirect_uri` that does not match the one used at login are now rejected with the correct standard error. A replayed code also revokes the tokens it already issued. Well-behaved clients are unaffected.
|
||||
* **No caching of credentials:** token, introspection, userinfo, registration, and device-authorization responses now send `Cache-Control: no-store` so proxies and browsers do not cache them.
|
||||
* **userinfo rejects bad tokens:** an invalid access token at the userinfo endpoint returns `401 invalid_token` with a `WWW-Authenticate` header.
|
||||
* **OAuth authorize error redirect:** a missing `response_type` now redirects the error to the verified client `redirect_uri` instead of a generic error.
|
||||
* **Drizzle affected-row validation:** the Drizzle adapter throws on an invalid affected-row count instead of returning `0`.
|
||||
* **`organization.updateTeam` immutable fields:** `id`, `createdAt`, and `updatedAt` are no longer accepted in the request body.
|
||||
* **`updateMemberRole` ordering:** role-existence validation now runs after authorization checks.
|
||||
* **CLI `generate --output` to a directory:** picks an adapter-specific default filename.
|
||||
* **Generated schema and disabled migrations:** references to migration-disabled models are omitted.
|
||||
* **Cookie-cache session binding:** the cached session is now tied to the `session_token` cookie.
|
||||
* **SSRF host checks:** outbound-host classification now blocks additional reserved ranges (6to4 relay anycast, site-local IPv6, and IPv4-compatible IPv6).
|
||||
* **Standard token-redemption errors:** authorization-code redemption failures return `400 invalid_grant` instead of `401 invalid_client` or `invalid_request`.
|
||||
* **Sign-out hooks with external session stores:** `session.delete` hooks now run on sign-out even with `secondaryStorage` and `preserveSessionInDatabase`.
|
||||
* **Two-factor invalidation error code:** a failed two-factor challenge cleanup now returns `FAILED_TO_INVALIDATE_TWO_FACTOR_CHALLENGE`.
|
||||
|
||||
***
|
||||
|
||||
## Upgrade checklist
|
||||
|
||||
Run once, after applying the changes that affect you:
|
||||
|
||||
```bash title="Terminal"
|
||||
npx auth@latest generate
|
||||
npx auth@latest migrate
|
||||
```
|
||||
|
||||
Then complete the manual data step if it applies:
|
||||
|
||||
* **Reclaim SCIM connections:** delete or assign an `organizationId` to connection rows that have no organization.
|
||||
|
||||
***
|
||||
|
||||
## Tracking prereleases
|
||||
|
||||
If you upgraded straight from 1.6 to the final 1.7, skip this. It applies only if you adopted a 1.7 beta and followed its changes.
|
||||
|
||||
A breaking change in one beta can be reverted in a later beta of the same line, and the reversal is a migration in the opposite direction. The clearest case is OAuth scopes: an early beta moved `account.scope` into a `grantedScopes` array, and a later beta reverted it, so the final 1.7 keeps the original `account.scope` string. If you adopted the `grantedScopes` column, drop it and restore the `scope` column on every schema you changed. When your migration tool cannot tell a rename from a drop, reconcile the column with direct SQL so you keep existing rows.
|
||||
|
||||
Two habits make this cheaper. Read `revert` entries in the changelog, not just `feat` and `fix`; a revert is a migration for you even though it does not announce itself as breaking. Bump only the packages that carry the change, and confirm your own plugins do not import the reverted surface before you rebuild them.
|
||||
Reference in New Issue
Block a user