- Add OIDC back-channel logout to notify Relying Parties when a user's session ends at the OP
- Move joins configuration from experimental.joins to advanced.database.joins
- Scope accounts by issuer and require Account.issuer field, with account-specific APIs now selecting through accountId request property
- OAuth provider identity now sourced from raw verified profiles instead of switching between sub and id at runtime
- SSO account subjects now use protocol-defined values (OIDC sub claim, SAML NameID) with mapping.id removed from both configurations
- Support wildcard endpoint matching for captcha
- Ship MCP as its own package built on the OAuth provider with renamed route helper from withMcpAuth to requireMcpAuth
- Rename standalone protected-resource factory from mcpHandler to createMcpProtectedRequestHandler
- Migrate MCP to version 2 with stateless request and response transport
- Change MCP database models with oauthApplication becoming oauthClient and new oauthRefreshToken and oauthClientAssertion tables
- Introspection of access token whose bound session has ended now returns active false instead of staying active until token TTL
- Revoke refresh tokens without offline_access on session end while preserving offline_access refresh tokens for long-lived API access
- Validate custom description in createInsufficientScopeError against RFC 6750 error_description character set
- Remove MCP-route GET and DELETE exports and session-store options such as redisUrl
From Better Auth
Blog post: Better Auth 1.7
better-auth
❗ Breaking Changes
-
chore!: move joins to advanced.database.joins (#10359)
If you previously set
experimental: { joins: true }, update your config to:advanced: { database: { joins: true, }, }Adapters that support native joins use them when enabled. If an adapter cannot return joined data for a query, Better Auth falls back to additional queries and combines the results. Drizzle and Prisma users should ensure their schema includes the required relations (
npx auth@latest generate). -
feat(auth)!: scope accounts by issuer (#10403)
This release requires
Account.issuerbut preservesAccount.accountIdas the provider-assigned account identifier. Account-specific APIs select the localAccount.idthrough theaccountIdrequest property; token and provider-profile APIs can instead select the signed account cookie withuseAccountCookie: true. Credential accounts uselocal:credentialand the linked user's stableidas their provider identity.OAuth provider identity now comes from raw verified profiles. OpenID Connect discovery uses
sub, plain OAuth usesid, and providers can declareaccountSubjectfor another immutable field; Better Auth no longer switches betweensubandidat runtime.getUserInfo().userno longer carries provider identity, andmapProfileToUsercannot returnid. Read the selected identity fromaccountInfo.account.accountIdinstead ofaccountInfo.user.id. The genericmicrosoftEntraIdhelper now requires a concrete tenant GUID; use the built-in Microsoft provider for multi-tenant authorities.SSO account subjects are now protocol-defined. OIDC uses the verified
subclaim, and SAML uses the signedNameID;mapping.idis removed from both configurations. A manual SAML configuration without metadata XML must setidpMetadata.entityID, becausesamlConfig.issueridentifies the service provider and no longer acts as the IdP identity.Apply the reviewed account-identity backfill in the Better Auth 1.7 upgrade guide before deploying. The generated schema migration cannot assign trusted issuers or resolve existing identity collisions automatically.
-
feat(captcha)!: support wildcard endpoint matching (#10004)
-
feat(mcp)!: ship MCP as its own package built on the OAuth provider (#9992)
The shared-auth route helper is renamed from
withMcpAuthtorequireMcpAuth. The standalone protected-resource factory is renamed frommcpHandlertocreateMcpProtectedRequestHandler; pass one flatMcpProtectedRequestHandlerOptionsobject withissuer, a singleaudience, optionaljwtVerifyOptions, token-verification fields, and challenge fields. Its callback receivesaccessTokenClaims.requireMcpAuthverifies the access token against the published JWKS, validates DPoP proofs for DPoP-bound tokens, and passes the verified access-token claims to your handler.createInsufficientScopeErrornow validates a custom description against the RFC 6750error_descriptioncharacter set when the error is constructed. Invalid descriptions throwTypeError("invalid error_description")before an error can reach resource-challenge serialization.MCP 2026-07-28 uses a stateless request and response transport. Serve MCP routes with version 2 of
@modelcontextprotocol/server, configurecreateMcpHandlerwithlegacy: "reject", wrap it withrequireMcpAuth, and export onlyPOST. Remove MCP-routeGETandDELETEexports and session-store options such asredisUrl. OAuth clients, consent, authorization codes, refresh tokens, and security records remain durable authorization state.To migrate, install
@better-auth/mcp,@better-auth/cimd, and the official version 2 MCP client or server package needed by your application; add thejwt()plugin, which is now required for token signing; and move options that were nested underoidcConfigto flat options onmcp({ ... }). The database models change:oauthApplicationbecomesoauthClient, with newoauthRefreshTokenandoauthClientAssertiontables. Regenerate or migrate your schema withnpx auth migrateornpx auth generate. -
feat(oauth-provider)!: add OIDC back-channel logout (#9304)
When a user's session ends at the OP (sign-out,
/oauth2/end-session, admin revoke, ban),@better-auth/oauth-providernow notifies every Relying Party that holds tokens for that session. The user's API access is cut off right away, instead of access tokens staying usable until their own TTL. Each client opts in by registering abackchannel_logout_uri(and optionallybackchannel_logout_session_required) via DCR or the admin client-create endpoint. The provider signs alogout+jwtLogout Token per client and POSTs it to that client in parallel, with a short per-RP timeout.Breaking change. Introspection of an opaque or JWT access token whose bound session has ended now returns
{ active: false }, and/oauth2/userinforejects it withinvalid_token. Previously the token stayed active until its own TTL. If you relied on access tokens outliving the user's session, that no longer holds.Refresh tokens without
offline_accessare revoked on session end;offline_accessrefresh tokens are preserved so long-lived API access can survive the browser session (OIDC Back-Channel Logout 1.0 §2.7). Access-token invalidation on session end is an additional OP hardening choice beyond §2.7, enforced by session liveness, so it holds even when the JWT plugin is disabled.Delivery runs through the host's background task handler when one is configured (Vercel
waitUntil, Cloudflarectx.waitUntil); without a handler it completes inline so notifications are not lost on request teardown. Configureadvanced.backgroundTasks.handleron serverless runtimes to keep sign-out fast.Discovery at
/.well-known/openid-configurationand/.well-known/oauth-authorization-serveradvertisesbackchannel_logout_supported: trueandbackchannel_logout_session_supported: truewhen the JWT plugin is enabled. Every registeredbackchannel_logout_urimust be a credential-free public HTTPS URL without a fragment; loopback HTTP is rejected for both public and confidential clients. CIMD documents cannot register back-channel logout metadata. The SSRF host guard, which blocks private, reserved, tunneled, and cloud-metadata hosts, also covers aprivate_key_jwtclient'sjwks_uri.Schema changes on
@better-auth/oauth-provider:oauthClient.backchannelLogoutUri: string | nulloauthClient.backchannelLogoutSessionRequired: booleanoauthAccessToken.revoked: Date | null
better-auth'ssignJWTgains an optionalheaderargument, forwarded to custom remote signers. JWT profiles that need an explicit media type, such astyp: "logout+jwt", can now set it without reaching for the low-level signing primitives. -
feat(oauth-provider)!: model OAuth protected resources explicitly (#9648)
validAudiencesis removed. Move each existing resource identifier intoresources; link clients that should be limited to specific resources throughoauthClientResourceor Dynamic Client Registrationresources.Access-token issuance now applies resource policy to the requested RFC 8707
resourcevalues. The OAuth provider narrows scopes to resource allowlists, uses the shortest configured TTL, strips reserved RFC 9068 claim names from custom claims, emitsjti, and keeps repeatedresourceform parameters.Refresh-token TTLs now use the shortest applicable lifetime. Deployments with a per-resource
refreshTokenTtllonger thanrefreshTokenExpiresInwill see refresh tokens expire at the provider default instead of the longer resource value.JWT signing can now honor per-resource pins.
signJWT()acceptssigningKeyIdandsigningAlgorithm; JWKS adapters exposegetKeyById()andgetLatestKeyByAlg(). Thejwkstable adds nullablealgandcrvcolumns, andkeyPairConfigscan provision multiple algorithms in one keyring.After upgrading, run
npx @better-auth/cli generateand apply the migration before deploying. The migration addsoauthResource,oauthClientResource, and the newjwkscolumns. Without it, resources usingsigningAlgorithmcannot find matching keys.Resource servers should publish RFC 9728 protected-resource metadata at their own origin. The OAuth provider exposes challenge helpers that point clients at that metadata.
@better-auth/mcpnow requires an explicitresourceoption. The plugin stores that identifier as an OAuth resource, publishes RFC 9728 protected-resource metadata for it, and binds issued access tokens to that resource. Existingmcp({ loginPage, consentPage })setups should add a protected MCP resource identifier, for exampleresource: "https://api.example.com/mcp". -
feat(scim)!: decouple provisioning from the organization plugin (#10390)
This replaces the previous SCIM configuration, client APIs, database schema, and organization-backed Group model. Existing SCIM installations cannot migrate provisioning state in place. Follow the SCIM cutover in the 1.7 upgrade guide, including full directory reprovisioning, before resuming traffic.
Deferred database side effects now run only after a successful transaction. A rolled-back User update no longer refreshes its cached profile, and a rolled-back bulk session revocation no longer invalidates sessions.
-
feat(two-factor)!: add OTP enablement and discriminated response (#9057)
enableTwoFactornow accepts amethodparameter ("otp" | "totp", default"totp") and returns a discriminated response with amethodfield.method: "otp"- Sets
twoFactorEnabled: trueimmediately. - Returns
{ method: "otp" }. - Requires
otpOptions.sendOTPto be configured on the server; rejects withOTP_NOT_CONFIGUREDotherwise.
method: "totp"(default)- Returns
{ method: "totp", totpURI, backupCodes }. - Rejects with
TOTP_NOT_CONFIGUREDiftotpOptions.disableis set.
The existing
skipVerificationOnEnableoption remains supported for TOTP enrollment.Breaking changes
- Response shape changed:
enableTwoFactorincludes amethodfield in the response ("otp"or"totp").
- Sets
-
fix(auth)!: ignore x-forwarded headers by default on dynamic baseURL (#9134)
Requests using
baseURL: { allowedHosts }now resolve the auth origin fromHostby default, so forwarded headers cannot select another allowed host unless trusted proxy headers are enabled.Breaking change: if your proxy exposes the public hostname only through
x-forwarded-host, setadvanced.trustedProxyHeaders: true. Deployments where the proxy rewritesHostto the public hostname (nginx default, Vercel, Cloudflare, and Netlify) are unaffected.Migration:
betterAuth({ baseURL: { allowedHosts: [...] }, advanced: { trustedProxyHeaders: true, }, }); -
fix(device-authorization)!: add lookup indexes (#10059)
Generated codes are limited to 191 characters. Issuance makes up to 3 attempts to overcome unique-key collisions, then returns
server_errorif it cannot create a uniquedeviceCodeanduserCode. Default-generated user codes accept case changes and readability separators during verification, approval, and denial; custom codes outside the default alphabet are matched exactly. The/devicelimiter allows 5 requests over a window equal to the configured code lifetime, while/device/tokenpolling keeps its separate interval behavior. -
fix(electron)!: enforce S256 PKCE and harden origin checks (#9645)
The Electron sign-in flow now mandates PKCE S256. Plain PKCE is rejected: the
code_challenge_methodparameter is gone and every authorization code is verified by hashing the verifier with SHA-256. The server no longer trusts anelectron-originheader to set the request Origin. The Electron client now sends a realOrigin(for examplemyapp:/), so upgrade the@better-auth/electronclient and server together and make sure your app's scheme is intrustedOrigins. The unuseddisableOriginOverrideoption is removed.Custom-scheme entries in
trustedOriginsnow match by scheme and authority instead of string prefix. A host-less entry such asmyapp://orexp://still trusts every host of that scheme, but a host-bearing entry such asmyapp://callbackmatches that host exactly, so it is no longer satisfied bymyapp://callback.attacker.tld. -
fix(microsoft)!: use oid as account id (#10204)
-
fix(one-tap)!: require client id for audience validation (#10036)
-
refactor!: remove deprecated oidc-provider plugin (#10031)
-
refactor(generic-oauth)!: rewrite as first-class social provider with RFC compliance (#9069)
Breaking changes:
signIn.oauth2({ providerId })replaced bysignIn.social({ provider })oauth2.link()replaced bylinkSocial()- Callback URL changed from
/api/auth/oauth2/callback/:idto/api/auth/callback/:id genericOAuthClient()removed; generic OAuth providers now use the standard social client APIspkcedefaults totrue(wasfalse); setpkce: falsefor providers that reject PKCEauthorizationUrlParamsandtokenUrlParamsonly acceptRecord<string, string>issuerandrequireIssuerValidationconfig fields removed; issuer validation is automatic via OIDC discoverymapProfileToUserprofile typed asOAuth2UserInfo & Record<string, unknown>
-
refactor(oauth-provider)!: separate device grant ownership (#10746)
The OAuth integration replaces the optional
resourcecolumn withoauthClientIdandresources. Regenerate and apply the schema when using it. Before upgrading from an earlier 1.7 prerelease, let pending OAuth device codes expire or delete them because they cannot be exchanged through the new integration. -
refactor(oauth)!: verify provider
id_tokenswith a single shared verifier (#9828)Client-submitted id_token sign-in (
signIn.social({ idToken })and account linking) is verified by one function instead of a per-providerverifyIdTokenmethod. Each provider declares anidTokenconfig with a JWKS source, issuer, and audience, and the core verifier runs the signature, issuer, audience, and nonce checks. A provider that declares no config rejects the client id_token path.PayPal previously accepted any decodable id_token without verifying its signature. PayPal derives identity from the access token, so it now declares no
idTokenconfig, and the client id_token path returnsID_TOKEN_NOT_SUPPORTED. PayPal sign-in through the redirect flow is unchanged.Custom providers that implement
UpstreamProviderdirectly replace the removedverifyIdTokenmethod with anidTokenconfig:idToken: { jwks: createRemoteJWKSet(new URL("https://issuer.example/.well-known/jwks.json")), issuer: "https://issuer.example", audience: clientId, },For verification that cannot use a local JWKS, pass
idToken: { verify: async (token, nonce) => boolean }. TheverifyIdTokenanddisableIdTokenSignInprovider options are unchanged.
Features
- feat: add
clientAssertionsupport to the Microsoft Entra ID social provider (#9898) - feat: make
Authinstance fetchable (#9431) - feat(auth): add per-provider
requireEmailVerificationfor social sign-in (#9929) - feat(auth): add user.validateUserInfo provisioning gate (#9864)
- feat(client): add
hydrateSessionfor SSR session hydration (#8733) - feat(db): add compound table indexes (#10402)
- feat(generic-oauth,sso): support IDP-initiated flows via secure bounce (#9301)
- feat(generic-oauth): add RP-initiated logout support (#9368)
- feat(generic-oauth): forward
refreshTokenParamsto token endpoint (#9948) - feat(generic-oauth): verify discovery
id_tokensand enableid_tokensign-in (#9966) - feat(oauth-provider): add device authorization grant (RFC 8628) (#10135)
- feat(oauth-provider): add DPoP support (#10039)
- feat(oauth-provider): compute
at_hashin id tokens per OIDC Core §3.1.3.6 (#9079) - feat(oauth): add
private_key_jwtclient authentication (RFC 7523) (#8836) - feat(oauth): enforce no-store on credential responses via a declarative flag (#10065)
- feat(oauth): per-request
additionalParamsandloginHint(#9305) - feat(oauth): server-trusted state channel; fix anonymous cookieless linking (#9930)
- feat(org): allow passing userId and organizationId to listUserTeams API (#8977)
- feat(organization): add getOrganization for metadata-only fetches (#10397)
- feat(phone-number): add server-side OTP consumption API (#9766)
- feat(session): support JWKS-backed JWT session cookie cache (#8931)
- feat(sso): add transactional OIDC user resolution (#10473)
- feat(username): add immutable username option (#9240)
- feat(username): disable display-name (#10330)
Bug Fixes
- Allow test instances to enable native database transactions for postgres and mysql.
- Bundled dependencies were refreshed to their latest compatible releases, including jose, nanostores, the noble crypto packages, and SimpleWebAuthn. These updates are backward compatible and require no changes to existing projects.
- chore: widen drizzle-kit peer dependency range (#10299)
- fix(cookies): decouple cookie cache from JWT plugin internals (#10666)
- fix(db): don't abort auth migrate when adding required or unique columns (#10293)
- fix(generic-oauth): bind id token nonce in redirect flow (#10095)
- fix(kysely-adapter): report native transaction support for auto-detected dialects (#10622)
- fix(oauth): create new oauth account in transaction (#10125)
- fix(oauth): derive redirect URI from per-request baseURL (#10127)
- fix(oauth): preserve account.scope across re-auth and refresh (#10128)
- fix(oauth): preserve user on null profile override (#10124)
- fix(session): fire session-delete hooks for preserved sessions on secondaryStorage (#9969)
- fix(siwe): issue addressless nonces (#10234)
- fix(types): expand workspace and consumer type checking (#10505)
- refactor(oauth): single-source Basic credentials + getHttpTestInstance (#9657)
For detailed changes, see CHANGELOG
@better-auth/oauth-provider
❗ Breaking Changes
-
feat(mcp)!: ship MCP as its own package built on the OAuth provider (#9992)
The shared-auth route helper is renamed from
withMcpAuthtorequireMcpAuth. The standalone protected-resource factory is renamed frommcpHandlertocreateMcpProtectedRequestHandler; pass one flatMcpProtectedRequestHandlerOptionsobject withissuer, a singleaudience, optionaljwtVerifyOptions, token-verification fields, and challenge fields. Its callback receivesaccessTokenClaims.requireMcpAuthverifies the access token against the published JWKS, validates DPoP proofs for DPoP-bound tokens, and passes the verified access-token claims to your handler.createInsufficientScopeErrornow validates a custom description against the RFC 6750error_descriptioncharacter set when the error is constructed. Invalid descriptions throwTypeError("invalid error_description")before an error can reach resource-challenge serialization.MCP 2026-07-28 uses a stateless request and response transport. Serve MCP routes with version 2 of
@modelcontextprotocol/server, configurecreateMcpHandlerwithlegacy: "reject", wrap it withrequireMcpAuth, and export onlyPOST. Remove MCP-routeGETandDELETEexports and session-store options such asredisUrl. OAuth clients, consent, authorization codes, refresh tokens, and security records remain durable authorization state.To migrate, install
@better-auth/mcp,@better-auth/cimd, and the official version 2 MCP client or server package needed by your application; add thejwt()plugin, which is now required for token signing; and move options that were nested underoidcConfigto flat options onmcp({ ... }). The database models change:oauthApplicationbecomesoauthClient, with newoauthRefreshTokenandoauthClientAssertiontables. Regenerate or migrate your schema withnpx auth migrateornpx auth generate. -
feat(oauth-provider)!: add OIDC back-channel logout (#9304)
When a user's session ends at the OP (sign-out,
/oauth2/end-session, admin revoke, ban),@better-auth/oauth-providernow notifies every Relying Party that holds tokens for that session. The user's API access is cut off right away, instead of access tokens staying usable until their own TTL. Each client opts in by registering abackchannel_logout_uri(and optionallybackchannel_logout_session_required) via DCR or the admin client-create endpoint. The provider signs alogout+jwtLogout Token per client and POSTs it to that client in parallel, with a short per-RP timeout.Breaking change. Introspection of an opaque or JWT access token whose bound session has ended now returns
{ active: false }, and/oauth2/userinforejects it withinvalid_token. Previously the token stayed active until its own TTL. If you relied on access tokens outliving the user's session, that no longer holds.Refresh tokens without
offline_accessare revoked on session end;offline_accessrefresh tokens are preserved so long-lived API access can survive the browser session (OIDC Back-Channel Logout 1.0 §2.7). Access-token invalidation on session end is an additional OP hardening choice beyond §2.7, enforced by session liveness, so it holds even when the JWT plugin is disabled.Delivery runs through the host's background task handler when one is configured (Vercel
waitUntil, Cloudflarectx.waitUntil); without a handler it completes inline so notifications are not lost on request teardown. Configureadvanced.backgroundTasks.handleron serverless runtimes to keep sign-out fast.Discovery at
/.well-known/openid-configurationand/.well-known/oauth-authorization-serveradvertisesbackchannel_logout_supported: trueandbackchannel_logout_session_supported: truewhen the JWT plugin is enabled. Every registeredbackchannel_logout_urimust be a credential-free public HTTPS URL without a fragment; loopback HTTP is rejected for both public and confidential clients. CIMD documents cannot register back-channel logout metadata. The SSRF host guard, which blocks private, reserved, tunneled, and cloud-metadata hosts, also covers aprivate_key_jwtclient'sjwks_uri.Schema changes on
@better-auth/oauth-provider:oauthClient.backchannelLogoutUri: string | nulloauthClient.backchannelLogoutSessionRequired: booleanoauthAccessToken.revoked: Date | null
better-auth'ssignJWTgains an optionalheaderargument, forwarded to custom remote signers. JWT profiles that need an explicit media type, such astyp: "logout+jwt", can now set it without reaching for the low-level signing primitives. -
feat(oauth-provider)!: align MCP authorization with 2026-07-28 (#10577)
OAuthClientno longer has a catch-all string index. Model custom wire extensions explicitly with a named intersection such asOAuthClient & YourExtensionMetadata; legacytypeandpublicfields no longer type-check as unknown baggage.- Dynamic, administrative, and user-managed registrations default an omitted
application_typetoweb. Client ID Metadata Documents preserve an omitted value asnull. - Web redirects require HTTPS on a non-loopback host. Native redirects accept claimed HTTPS URLs, exact HTTP loopback hosts, or reverse-domain private-use schemes.
- Registration resource options control resource links.
mcp()contributes its protected resource by default, so standards-based clients no longer need aresourcesextension. mcp()no longer enables unauthenticated Dynamic Client Registration. Composemcp()withcimd()for Client ID Metadata Documents, or enable both DCR flags explicitly.
This release requires a database migration. Add
applicationTypeand nullableclientDiscoveryId; map oldwebandnativevalues directly, mapuser-agent-basedtoNULLfor manual reclassification, and never derive it frompublic. SetclientDiscoveryIdonly from known discovery provenance, never by inspecting an HTTPS client ID. Deduplicate existing(clientId, resourceId)links before adding the new compound unique index, then drop the legacy columns. Deployments with custom schema mappings must apply this backfill manually.Machine-to-machine scope authority is now stored separately in nullable
oauthClient.clientCredentialsScopes. Missing,NULL, and empty values denyclient_credentialstoken issuance. Only the administrative create and update endpoints exposeclient_credentials_scopes, and assigning a non-empty value requiresclientPrivilegesto approve the newconfigure-client-credentials-scopesaction. DCR, CIMD, and user-managed registration cannot assign this field; CIMD refresh preserves an existing administrator-owned value. RemoveclientCredentialGrantDefaultScopes, backfill every existing client to[], configure[]as the default for new rows, then explicitly assign every approved machine scope after auditing the client. - Dynamic, administrative, and user-managed registrations default an omitted
-
feat(oauth-provider)!: enforce max_age (#9936)
-
feat(oauth-provider)!: make id-token claim authority explicit (#10140) ISO/IEC 29115 level 1, and OpenID discovery advertises only
"0". Becauseacr_valuesis voluntary, requests for other classes continue instead of failing. Essentialclaims.id_token.acrrequests in OpenID Connect flows still fail when their requiredvalueorvaluescannot be met.customIdTokenClaims, extension ID-token claims, and per-issuanceidTokenClaimscan no longer set OIDC/JWT protocol claims such as issuer, subject, audience, token lifetime, nonce, session or hash binding,auth_time,acr,amr, orazp. Namespaced custom claims still appear in ID tokens. -
feat(oauth-provider)!: model OAuth protected resources explicitly (#9648)
validAudiencesis removed. Move each existing resource identifier intoresources; link clients that should be limited to specific resources throughoauthClientResourceor Dynamic Client Registrationresources.Access-token issuance now applies resource policy to the requested RFC 8707
resourcevalues. The OAuth provider narrows scopes to resource allowlists, uses the shortest configured TTL, strips reserved RFC 9068 claim names from custom claims, emitsjti, and keeps repeatedresourceform parameters.Refresh-token TTLs now use the shortest applicable lifetime. Deployments with a per-resource
refreshTokenTtllonger thanrefreshTokenExpiresInwill see refresh tokens expire at the provider default instead of the longer resource value.JWT signing can now honor per-resource pins.
signJWT()acceptssigningKeyIdandsigningAlgorithm; JWKS adapters exposegetKeyById()andgetLatestKeyByAlg(). Thejwkstable adds nullablealgandcrvcolumns, andkeyPairConfigscan provision multiple algorithms in one keyring.After upgrading, run
npx @better-auth/cli generateand apply the migration before deploying. The migration addsoauthResource,oauthClientResource, and the newjwkscolumns. Without it, resources usingsigningAlgorithmcannot find matching keys.Resource servers should publish RFC 9728 protected-resource metadata at their own origin. The OAuth provider exposes challenge helpers that point clients at that metadata.
@better-auth/mcpnow requires an explicitresourceoption. The plugin stores that identifier as an OAuth resource, publishes RFC 9728 protected-resource metadata for it, and binds issued access tokens to that resource. Existingmcp({ loginPage, consentPage })setups should add a protected MCP resource identifier, for exampleresource: "https://api.example.com/mcp". -
fix(oauth-provider)!: bind client authentication to the issuing grant (#10063)
-
fix(oauth-provider)!: bind RFC 8707 resource indicators to the authorization grant (#9836)
Breaking change: when the authorization includes a
resource, the token and refresh requests may only narrow it. A request for a resource the authorization did not cover returnsinvalid_target. ThecustomAccessTokenClaimscallback now receives aresourcesarray in place of theresourcestring.Migration: run the schema migration (
npx @better-auth/cli migrate, orgenerateif you manage the schema yourself) to add the new resource columns. -
fix(oauth-provider)!: return RFC-compliant error envelopes from validation failures (#9277)
Authorization errors redirect to a registered client's trusted redirect URI with
stateandiss. The response uses the URL fragment for implicittokenandid_tokenresponses unless the client explicitly requests query mode. Requests without a trusted redirect URI continue to use the server error page.Token, introspection, and revocation requests now treat empty credential values as omitted, reject repeated non-empty client credentials, and require confidential clients to use their registered
token_endpoint_auth_method. Introspection and revocation requests also ignore unrecognizedtoken_type_hintvalues instead of rejecting the request. -
refactor(generic-oauth)!: rewrite as first-class social provider with RFC compliance (#9069)
Breaking changes:
signIn.oauth2({ providerId })replaced bysignIn.social({ provider })oauth2.link()replaced bylinkSocial()- Callback URL changed from
/api/auth/oauth2/callback/:idto/api/auth/callback/:id genericOAuthClient()removed; generic OAuth providers now use the standard social client APIspkcedefaults totrue(wasfalse); setpkce: falsefor providers that reject PKCEauthorizationUrlParamsandtokenUrlParamsonly acceptRecord<string, string>issuerandrequireIssuerValidationconfig fields removed; issuer validation is automatic via OIDC discoverymapProfileToUserprofile typed asOAuth2UserInfo & Record<string, unknown>
-
refactor(oauth-provider)!: separate device grant ownership (#10746)
The OAuth integration replaces the optional
resourcecolumn withoauthClientIdandresources. Regenerate and apply the schema when using it. Before upgrading from an earlier 1.7 prerelease, let pending OAuth device codes expire or delete them because they cannot be exchanged through the new integration.
Features
- feat: add token endpoint client authentication (#9625)
- feat(cimd): add Client ID Metadata Document plugin (#9159)
- feat(oauth-provider): add device authorization grant (RFC 8628) (#10135)
- feat(oauth-provider): add DPoP support (#10039)
- feat(oauth-provider): add extension surface (#10030)
- feat(oauth-provider): add refresh token reuse interval (#10145)
- feat(oauth-provider): allow confidential DCR clients without PKCE (#10146)
- feat(oauth-provider): compute
at_hashin id tokens per OIDC Core §3.1.3.6 (#9079) - feat(oauth-provider): consistent and audience-scoped token introspection (#10045)
- feat(oauth-provider): expose sessionId to
id_tokenclaim contributors (#10113) - feat(oauth-provider): honor requested UserInfo claims via a claim registry (#10156)
- feat(oauth-provider): remove silenceWarnings config and well-known endpoint warnings (#10703)
- feat(oauth-provider): support protected dynamic client registration (#10037)
- feat(oauth): add
private_key_jwtclient authentication (RFC 7523) (#8836) - feat(oauth): enforce no-store on credential responses via a declarative flag (#10065)
- feat(oauth): server-trusted state channel; fix anonymous cookieless linking (#9930)
Bug Fixes
- fix(device-authorization): enforce RFC device flow requirements (#10752)
- fix(oauth-provider): accept issuer audience for client assertions (#10811)
- fix(oauth-provider): accept UserInfo form-body tokens (#10155)
- fix(oauth-provider): allow nonce-bound offline access without PKCE (#10153)
- fix(oauth-provider): challenge invalid userinfo tokens (#10068)
- fix(oauth-provider): complete RP-initiated logout flow (#10812)
- fix(oauth-provider): defer logout effects until commit (#10472)
- fix(oauth-provider): handle OIDC authorization request inputs (#10151)
- fix(oauth-provider): handle voluntary and essential ACR requests (#10790)
- fix(oauth-provider): keep OIDC scope claims on UserInfo (#10152)
- fix(oauth-provider): make
private_key_jwtjti single-use atomic across processes (#9964) - fix(oauth-provider): make
redirect_uriconditional at the token endpoint (#10159) - fix(oauth-provider): preserve dcr client key metadata (#10144)
- fix(oauth-provider): redirect missing
response_typeerrors (#10149) - fix(oauth-provider): reject authorization code replay correctly (#10150)
- fix(oauth-provider): report
unsupported_token_typefor JWT access-token revocation (#9970) - fix(oauth-provider): require openid for claims requests (#10791)
- fix(oauth-provider): return invalid_grant for cross-client refresh tokens (#10154)
- MCP clients that hit a scope wall now learn exactly which scopes to ask for. Missing protected scopes produce a
403with an RFC 6750insufficient_scopeWWW-Authenticatechallenge that names every missing scope. Clients can union those scopes into one authorization request instead of opening one browser redirect per scope. - refactor(oauth): single-source Basic credentials + getHttpTestInstance (#9657)
For detailed changes, see CHANGELOG
@better-auth/core
❗ Breaking Changes
-
chore!: move joins to advanced.database.joins (#10359)
If you previously set
experimental: { joins: true }, update your config to:advanced: { database: { joins: true, }, }Adapters that support native joins use them when enabled. If an adapter cannot return joined data for a query, Better Auth falls back to additional queries and combines the results. Drizzle and Prisma users should ensure their schema includes the required relations (
npx auth@latest generate). -
feat(auth)!: scope accounts by issuer (#10403)
This release requires
Account.issuerbut preservesAccount.accountIdas the provider-assigned account identifier. Account-specific APIs select the localAccount.idthrough theaccountIdrequest property; token and provider-profile APIs can instead select the signed account cookie withuseAccountCookie: true. Credential accounts uselocal:credentialand the linked user's stableidas their provider identity.OAuth provider identity now comes from raw verified profiles. OpenID Connect discovery uses
sub, plain OAuth usesid, and providers can declareaccountSubjectfor another immutable field; Better Auth no longer switches betweensubandidat runtime.getUserInfo().userno longer carries provider identity, andmapProfileToUsercannot returnid. Read the selected identity fromaccountInfo.account.accountIdinstead ofaccountInfo.user.id. The genericmicrosoftEntraIdhelper now requires a concrete tenant GUID; use the built-in Microsoft provider for multi-tenant authorities.SSO account subjects are now protocol-defined. OIDC uses the verified
subclaim, and SAML uses the signedNameID;mapping.idis removed from both configurations. A manual SAML configuration without metadata XML must setidpMetadata.entityID, becausesamlConfig.issueridentifies the service provider and no longer acts as the IdP identity.Apply the reviewed account-identity backfill in the Better Auth 1.7 upgrade guide before deploying. The generated schema migration cannot assign trusted issuers or resolve existing identity collisions automatically.
-
feat(scim)!: decouple provisioning from the organization plugin (#10390)
This replaces the previous SCIM configuration, client APIs, database schema, and organization-backed Group model. Existing SCIM installations cannot migrate provisioning state in place. Follow the SCIM cutover in the 1.7 upgrade guide, including full directory reprovisioning, before resuming traffic.
Deferred database side effects now run only after a successful transaction. A rolled-back User update no longer refreshes its cached profile, and a rolled-back bulk session revocation no longer invalidates sessions.
-
fix(microsoft)!: use oid as account id (#10204)
-
refactor(oauth)!: verify provider
id_tokenswith a single shared verifier (#9828)Client-submitted id_token sign-in (
signIn.social({ idToken })and account linking) is verified by one function instead of a per-providerverifyIdTokenmethod. Each provider declares anidTokenconfig with a JWKS source, issuer, and audience, and the core verifier runs the signature, issuer, audience, and nonce checks. A provider that declares no config rejects the client id_token path.PayPal previously accepted any decodable id_token without verifying its signature. PayPal derives identity from the access token, so it now declares no
idTokenconfig, and the client id_token path returnsID_TOKEN_NOT_SUPPORTED. PayPal sign-in through the redirect flow is unchanged.Custom providers that implement
UpstreamProviderdirectly replace the removedverifyIdTokenmethod with anidTokenconfig:idToken: { jwks: createRemoteJWKSet(new URL("https://issuer.example/.well-known/jwks.json")), issuer: "https://issuer.example", audience: clientId, },For verification that cannot use a local JWKS, pass
idToken: { verify: async (token, nonce) => boolean }. TheverifyIdTokenanddisableIdTokenSignInprovider options are unchanged.
Features
- feat: add
clientAssertionsupport to the Microsoft Entra ID social provider (#9898) - feat(auth): add per-provider
requireEmailVerificationfor social sign-in (#9929) - feat(auth): add user.validateUserInfo provisioning gate (#9864)
- feat(db): add compound table indexes (#10402)
- feat(generic-oauth,sso): support IDP-initiated flows via secure bounce (#9301)
- feat(generic-oauth): add RP-initiated logout support (#9368)
- feat(generic-oauth): forward
refreshTokenParamsto token endpoint (#9948) - feat(google): add
includeGrantedScopesoption (#10129) - feat(oauth-provider): add DPoP support (#10039)
- feat(oauth): add
private_key_jwtclient authentication (RFC 7523) (#8836) - feat(oauth): enforce no-store on credential responses via a declarative flag (#10065)
- feat(oauth): per-request
additionalParamsandloginHint(#9305) - feat(session): support JWKS-backed JWT session cookie cache (#8931)
- feat(sso): add transactional OIDC user resolution (#10473)
Bug Fixes
- fix(cimd): route
client_idSSRF checks through the shared host classifier (#10126) - fix(oauth): derive redirect URI from per-request baseURL (#10127)
- fix(oauth): preserve account.scope across re-auth and refresh (#10128)
- fix(types): expand workspace and consumer type checking (#10505)
- refactor(oauth): single-source Basic credentials + getHttpTestInstance (#9657)
For detailed changes, see CHANGELOG
@better-auth/sso
❗ Breaking Changes
-
feat(auth)!: scope accounts by issuer (#10403)
This release requires
Account.issuerbut preservesAccount.accountIdas the provider-assigned account identifier. Account-specific APIs select the localAccount.idthrough theaccountIdrequest property; token and provider-profile APIs can instead select the signed account cookie withuseAccountCookie: true. Credential accounts uselocal:credentialand the linked user's stableidas their provider identity.OAuth provider identity now comes from raw verified profiles. OpenID Connect discovery uses
sub, plain OAuth usesid, and providers can declareaccountSubjectfor another immutable field; Better Auth no longer switches betweensubandidat runtime.getUserInfo().userno longer carries provider identity, andmapProfileToUsercannot returnid. Read the selected identity fromaccountInfo.account.accountIdinstead ofaccountInfo.user.id. The genericmicrosoftEntraIdhelper now requires a concrete tenant GUID; use the built-in Microsoft provider for multi-tenant authorities.SSO account subjects are now protocol-defined. OIDC uses the verified
subclaim, and SAML uses the signedNameID;mapping.idis removed from both configurations. A manual SAML configuration without metadata XML must setidpMetadata.entityID, becausesamlConfig.issueridentifies the service provider and no longer acts as the IdP identity.Apply the reviewed account-identity backfill in the Better Auth 1.7 upgrade guide before deploying. The generated schema migration cannot assign trusted issuers or resolve existing identity collisions automatically.
-
feat(sso)!: support multiple IdP signing certificates (#8805)
SAML signing certificates now accept an array of PEM strings, so administrators can publish a new IdP cert alongside the old one and complete the rotation without forcing every active session to re-authenticate. Responses signed by any listed cert are accepted.
samlConfig: { idpMetadata: { cert: [currentPem, nextPem], }, }Both
samlConfig.certandsamlConfig.idpMetadata.certaccept either a single PEM string or an array. When both are set,idpMetadata.certwins.Breaking: response shape
The management endpoints (
getSSOProvider,listSSOProviders,updateSSOProvider) now returnsamlConfig.certificateas an array of parsed certificates in every case, even when a single cert is configured. The field is absent only when certs live insideidpMetadata.metadata. Update consumers to read an array; no moreArray.isArraybranching.Validation
Registration now rejects SAML configs that supply no signing-cert source. samlify needs either an
idpMetadata.metadataXML document (which embeds the certs) or an explicit PEM undercertoridpMetadata.cert. Configs missing both fail withCERT_SOURCE_MISSING.Fix
SAML Single Logout could fail to decrypt encrypted
LogoutResponsepayloads because the IdP entity was constructed withoutprivateKey,encPrivateKey, orencPrivateKeyPasson that code path. All three are now applied on every IdP construction. -
fix(auth)!: harden validateUserInfo source contract (#9940)
-
fix(sso)!: harden SAML response validation (InResponseTo, Audience, SessionIndex) (#9055)
Breaking Changes
allowIdpInitiatednow defaults tofalse— IdP-initiated SSO (unsolicited SAML responses) is disabled by default. Setsaml.allowIdpInitiated: trueto restore the previous behavior. This aligns with the SAML2Int interoperability profile which recommends against IdP-initiated SSO due to its susceptibility to injection attacks.
Bug Fixes
- InResponseTo validation was completely non-functional — The code read
extract.inResponseTo(alwaysundefined) instead of samlify's actual pathextract.response.inResponseTo. SP-initiated InResponseTo validation now works as intended in both ACS handlers. - Audience Restriction was never validated — SAML assertions issued for a different service provider were accepted without checking the
<AudienceRestriction>element. Audience is now validated against the configuredsamlConfig.audiencevalue per SAML 2.0 Core §2.5.1. - SessionIndex stored as object instead of string — samlify returns
sessionIndexfrom login responses as{ authnInstant, sessionNotOnOrAfter, sessionIndex }, but the code stored the whole object. SLO session-index comparisons always failed silently. The correct innersessionIndexstring is now extracted.
Improvements
- Extracted shared
validateInResponseTo()andvalidateAudience()intopackages/sso/src/saml/response-validation.ts, eliminating ~160 lines of duplicated validation logic between the two ACS handlers. - Fixed
SAMLAssertionExtracttype to match samlify's actual extractor output shape.
-
refactor(sso)!: remove callbackUrl, consolidate ACS endpoint, fix SLO (#9117)
callbackUrlno longer configures the ACS URL. The default ACS URL is derived frombaseURLandproviderId. UsecallbackUrlas the provider-level post-auth redirect, or passcallbackURLtosignIn.sso()for an SP-initiated request:await authClient.signIn.sso({ providerId: "my-provider", callbackURL: "/dashboard", });/sso/saml2/callback/:providerIdendpoint removed. Update your IdP's ACS URL to/sso/saml2/sp/acs/:providerId. This endpoint handles both GET and POST requests.spMetadatais now optional. You no longer need to passspMetadata: {}when registering a provider. SP metadata is auto-generated from your configuration.Removed unused fields from
SAMLConfig:decryptionPvk,additionalParams,idpMetadata.entityURL,idpMetadata.redirectURL. These were stored but never read. Remove them from your configuration if present.Bug fixes
- Fix SLO SessionIndex matching: LogoutRequests with a SessionIndex were silently failing to delete the correct session.
- Audience validation now defaults to the SP entity ID when
audienceis not configured, per SAML Core section 2.5.1. - Restore
AllowCreatein AuthnRequests, required by IdPs that use JIT provisioning. - SP metadata endpoint now reflects actual SP capabilities (encryption, signing, SLO).
Features
- feat(auth): add user.validateUserInfo provisioning gate (#9864)
- feat(generic-oauth,sso): support IDP-initiated flows via secure bounce (#9301)
- feat(oauth): add
private_key_jwtclient authentication (RFC 7523) (#8836) - feat(oauth): per-request
additionalParamsandloginHint(#9305) - feat(oauth): server-trusted state channel; fix anonymous cookieless linking (#9930)
- feat(sso): add transactional OIDC user resolution (#10473)
- feat(sso): extend resolveUser to SAML and harden provider lifecycle (#10621)
- feat(sso): support additionalFields on ssoProvider (#9445)
Bug Fixes
- Allow SSO provider registration to reuse a SCIM connection ID. SCIM connections no longer participate in the authentication provider namespace.
- fix(sso): reject OIDC endpoint redirects portably (#10072)
- fix(sso): update samlify to 2.13.1 for signed-assertion XML injection (#9821)
- fix(sso): upgrade samlify to 2.12.0 with XPath injection and XXE fixes (#9121)
- refactor(oauth): single-source Basic credentials + getHttpTestInstance (#9657)
- Verify SAML assertion signatures directly instead of trusting an already-parsed response, and enforce a signing policy and size limit on SP metadata the same way IdP metadata is already enforced.
wantAssertionsSignednow controls whether the SP requires signed assertions instead of signed response messages, matching how IdPs sign SAML responses in practice.
For detailed changes, see CHANGELOG
@better-auth/scim
❗ Breaking Changes
-
feat(scim)!: decouple provisioning from the organization plugin (#10390)
This replaces the previous SCIM configuration, client APIs, database schema, and organization-backed Group model. Existing SCIM installations cannot migrate provisioning state in place. Follow the SCIM cutover in the 1.7 upgrade guide, including full directory reprovisioning, before resuming traffic.
Deferred database side effects now run only after a successful transaction. A rolled-back User update no longer refreshes its cached profile, and a rolled-back bulk session revocation no longer invalidates sessions.
-
feat(scim)!: isolate provider connections by organization (#10249) them statically, resolve them with
authentication.verifyBearerToken, or use the optionalmanagedConnectionscatalog.Legacy connection management, organization-scoped configuration, and SCIM-created authentication accounts are removed. Use identity and projection callbacks to connect SCIM resources to application users and roles.
Legacy SCIM state is not migrated. Back it up, issue new credentials, and fully reprovision Users and Groups after upgrading.
-
fix(scim)!: always bind personal SCIM connections to their creator (#9840)
providerOwnership. Applications now authorize their own SCIM administration workflows instead of relying on Better Auth user ownership.Legacy
scimProviderrows and credentials are not migrated. Follow the 1.7 SCIM upgrade guide, issue new credentials, and fully reprovision Users and Groups.
Features
- feat(auth): add user.validateUserInfo provisioning gate (#9864)
- feat(scim): add durable group resources (#10018)
- feat(scim): add enterprise user attributes and interop conformance (#10620)
- feat(scim): add managed connection catalog and runtime connection resolution (#10592)
- feat(scim): expose active provisioned user links (#10474)
Bug Fixes
- Accept exact case-insensitive string Boolean values for SCIM User
activeand theprimarysub-attribute ofemails,phoneNumbers,addresses,roles, andentitlementsat the HTTP ingress for Microsoft Entra interoperability. - Add an optional SCIM-owned connection and credential catalog. Configure
managedConnectionsto let trusted server code create runtime tenant connections and issue, rotate, and revoke their bearer credentials through server-onlyauth.apimethods, without a code-defined connection or an application-owned verifier. - Allow trusted server code to retain a terminal connection binding before a dynamic SCIM connection's first authenticated request by supplying its provisioning domain during decommissioning.
- fix(scim): create filtered PATCH values when no target matches (#10682)
For detailed changes, see CHANGELOG
@better-auth/mcp ✨
❗ Breaking Changes
-
feat(mcp)!: ship MCP as its own package built on the OAuth provider (#9992)
The shared-auth route helper is renamed from
withMcpAuthtorequireMcpAuth. The standalone protected-resource factory is renamed frommcpHandlertocreateMcpProtectedRequestHandler; pass one flatMcpProtectedRequestHandlerOptionsobject withissuer, a singleaudience, optionaljwtVerifyOptions, token-verification fields, and challenge fields. Its callback receivesaccessTokenClaims.requireMcpAuthverifies the access token against the published JWKS, validates DPoP proofs for DPoP-bound tokens, and passes the verified access-token claims to your handler.createInsufficientScopeErrornow validates a custom description against the RFC 6750error_descriptioncharacter set when the error is constructed. Invalid descriptions throwTypeError("invalid error_description")before an error can reach resource-challenge serialization.MCP 2026-07-28 uses a stateless request and response transport. Serve MCP routes with version 2 of
@modelcontextprotocol/server, configurecreateMcpHandlerwithlegacy: "reject", wrap it withrequireMcpAuth, and export onlyPOST. Remove MCP-routeGETandDELETEexports and session-store options such asredisUrl. OAuth clients, consent, authorization codes, refresh tokens, and security records remain durable authorization state.To migrate, install
@better-auth/mcp,@better-auth/cimd, and the official version 2 MCP client or server package needed by your application; add thejwt()plugin, which is now required for token signing; and move options that were nested underoidcConfigto flat options onmcp({ ... }). The database models change:oauthApplicationbecomesoauthClient, with newoauthRefreshTokenandoauthClientAssertiontables. Regenerate or migrate your schema withnpx auth migrateornpx auth generate. -
feat(oauth-provider)!: align MCP authorization with 2026-07-28 (#10577)
OAuthClientno longer has a catch-all string index. Model custom wire extensions explicitly with a named intersection such asOAuthClient & YourExtensionMetadata; legacytypeandpublicfields no longer type-check as unknown baggage.- Dynamic, administrative, and user-managed registrations default an omitted
application_typetoweb. Client ID Metadata Documents preserve an omitted value asnull. - Web redirects require HTTPS on a non-loopback host. Native redirects accept claimed HTTPS URLs, exact HTTP loopback hosts, or reverse-domain private-use schemes.
- Registration resource options control resource links.
mcp()contributes its protected resource by default, so standards-based clients no longer need aresourcesextension. mcp()no longer enables unauthenticated Dynamic Client Registration. Composemcp()withcimd()for Client ID Metadata Documents, or enable both DCR flags explicitly.
This release requires a database migration. Add
applicationTypeand nullableclientDiscoveryId; map oldwebandnativevalues directly, mapuser-agent-basedtoNULLfor manual reclassification, and never derive it frompublic. SetclientDiscoveryIdonly from known discovery provenance, never by inspecting an HTTPS client ID. Deduplicate existing(clientId, resourceId)links before adding the new compound unique index, then drop the legacy columns. Deployments with custom schema mappings must apply this backfill manually.Machine-to-machine scope authority is now stored separately in nullable
oauthClient.clientCredentialsScopes. Missing,NULL, and empty values denyclient_credentialstoken issuance. Only the administrative create and update endpoints exposeclient_credentials_scopes, and assigning a non-empty value requiresclientPrivilegesto approve the newconfigure-client-credentials-scopesaction. DCR, CIMD, and user-managed registration cannot assign this field; CIMD refresh preserves an existing administrator-owned value. RemoveclientCredentialGrantDefaultScopes, backfill every existing client to[], configure[]as the default for new rows, then explicitly assign every approved machine scope after auditing the client. - Dynamic, administrative, and user-managed registrations default an omitted
-
feat(oauth-provider)!: model OAuth protected resources explicitly (#9648)
validAudiencesis removed. Move each existing resource identifier intoresources; link clients that should be limited to specific resources throughoauthClientResourceor Dynamic Client Registrationresources.Access-token issuance now applies resource policy to the requested RFC 8707
resourcevalues. The OAuth provider narrows scopes to resource allowlists, uses the shortest configured TTL, strips reserved RFC 9068 claim names from custom claims, emitsjti, and keeps repeatedresourceform parameters.Refresh-token TTLs now use the shortest applicable lifetime. Deployments with a per-resource
refreshTokenTtllonger thanrefreshTokenExpiresInwill see refresh tokens expire at the provider default instead of the longer resource value.JWT signing can now honor per-resource pins.
signJWT()acceptssigningKeyIdandsigningAlgorithm; JWKS adapters exposegetKeyById()andgetLatestKeyByAlg(). Thejwkstable adds nullablealgandcrvcolumns, andkeyPairConfigscan provision multiple algorithms in one keyring.After upgrading, run
npx @better-auth/cli generateand apply the migration before deploying. The migration addsoauthResource,oauthClientResource, and the newjwkscolumns. Without it, resources usingsigningAlgorithmcannot find matching keys.Resource servers should publish RFC 9728 protected-resource metadata at their own origin. The OAuth provider exposes challenge helpers that point clients at that metadata.
@better-auth/mcpnow requires an explicitresourceoption. The plugin stores that identifier as an OAuth resource, publishes RFC 9728 protected-resource metadata for it, and binds issued access tokens to that resource. Existingmcp({ loginPage, consentPage })setups should add a protected MCP resource identifier, for exampleresource: "https://api.example.com/mcp".
Features
- feat(oauth-provider): add DPoP support (#10039)
- feat(oauth-provider): add refresh token reuse interval (#10145)
For detailed changes, see CHANGELOG
@better-auth/electron
❗ Breaking Changes
-
fix(electron)!: enforce S256 PKCE and harden origin checks (#9645)
The Electron sign-in flow now mandates PKCE S256. Plain PKCE is rejected: the
code_challenge_methodparameter is gone and every authorization code is verified by hashing the verifier with SHA-256. The server no longer trusts anelectron-originheader to set the request Origin. The Electron client now sends a realOrigin(for examplemyapp:/), so upgrade the@better-auth/electronclient and server together and make sure your app's scheme is intrustedOrigins. The unuseddisableOriginOverrideoption is removed.Custom-scheme entries in
trustedOriginsnow match by scheme and authority instead of string prefix. A host-less entry such asmyapp://orexp://still trusts every host of that scheme, but a host-bearing entry such asmyapp://callbackmatches that host exactly, so it is no longer satisfied bymyapp://callback.attacker.tld. -
refactor(generic-oauth)!: rewrite as first-class social provider with RFC compliance (#9069)
Breaking changes:
signIn.oauth2({ providerId })replaced bysignIn.social({ provider })oauth2.link()replaced bylinkSocial()- Callback URL changed from
/api/auth/oauth2/callback/:idto/api/auth/callback/:id genericOAuthClient()removed; generic OAuth providers now use the standard social client APIspkcedefaults totrue(wasfalse); setpkce: falsefor providers that reject PKCEauthorizationUrlParamsandtokenUrlParamsonly acceptRecord<string, string>issuerandrequireIssuerValidationconfig fields removed; issuer validation is automatic via OIDC discoverymapProfileToUserprofile typed asOAuth2UserInfo & Record<string, unknown>
Bug Fixes
- fix(types): expand workspace and consumer type checking (#10505)
For detailed changes, see CHANGELOG
@better-auth/expo
❗ Breaking Changes
-
fix(expo)!: use async secure storage access (#10438)
-
refactor(generic-oauth)!: rewrite as first-class social provider with RFC compliance (#9069)
Breaking changes:
signIn.oauth2({ providerId })replaced bysignIn.social({ provider })oauth2.link()replaced bylinkSocial()- Callback URL changed from
/api/auth/oauth2/callback/:idto/api/auth/callback/:id genericOAuthClient()removed; generic OAuth providers now use the standard social client APIspkcedefaults totrue(wasfalse); setpkce: falsefor providers that reject PKCEauthorizationUrlParamsandtokenUrlParamsonly acceptRecord<string, string>issuerandrequireIssuerValidationconfig fields removed; issuer validation is automatic via OIDC discoverymapProfileToUserprofile typed asOAuth2UserInfo & Record<string, unknown>
Bug Fixes
- fix(types): expand workspace and consumer type checking (#10505)
For detailed changes, see CHANGELOG
@better-auth/stripe
❗ Breaking Changes
- fix(stripe)!: make
onSubscriptionCancel.eventrequired (#9531) - fix(stripe)!: remove optional marker from onSubscriptionCancel
event(#9359)
For detailed changes, see CHANGELOG
auth
❗ Breaking Changes
- feat(oauth)!: accumulate granted scopes as
grantedScopes string[](#9825)
Features
Bug Fixes
- fix(core): preserve issuer-scoped account identities (#10668)
- fix(drizzle): export generated pgSchema for drizzle-kit (#10770)
- refactor(cli): leverage c12 v4
resolveModulefor auth config loading (#9477) - revert(oauth): remove granted scopes architecture (#10123)
For detailed changes, see CHANGELOG
@better-auth/drizzle-adapter
❗ Breaking Changes
-
chore!: move joins to advanced.database.joins (#10359)
If you previously set
experimental: { joins: true }, update your config to:advanced: { database: { joins: true, }, }Adapters that support native joins use them when enabled. If an adapter cannot return joined data for a query, Better Auth falls back to additional queries and combines the results. Drizzle and Prisma users should ensure their schema includes the required relations (
npx auth@latest generate).
Features
- feat(db): add compound table indexes (#10402)
- feat(drizzle-adapter): support Drizzle Relations v2 (#9489)
- feat(drizzle): generate drizzle schema with schema namespace (#7169)
Bug Fixes
- fix(drizzle): export generated pgSchema for drizzle-kit (#10770)
For detailed changes, see CHANGELOG
@better-auth/cimd ✨
❗ Breaking Changes
-
feat(oauth-provider)!: align MCP authorization with 2026-07-28 (#10577)
OAuthClientno longer has a catch-all string index. Model custom wire extensions explicitly with a named intersection such asOAuthClient & YourExtensionMetadata; legacytypeandpublicfields no longer type-check as unknown baggage.- Dynamic, administrative, and user-managed registrations default an omitted
application_typetoweb. Client ID Metadata Documents preserve an omitted value asnull. - Web redirects require HTTPS on a non-loopback host. Native redirects accept claimed HTTPS URLs, exact HTTP loopback hosts, or reverse-domain private-use schemes.
- Registration resource options control resource links.
mcp()contributes its protected resource by default, so standards-based clients no longer need aresourcesextension. mcp()no longer enables unauthenticated Dynamic Client Registration. Composemcp()withcimd()for Client ID Metadata Documents, or enable both DCR flags explicitly.
This release requires a database migration. Add
applicationTypeand nullableclientDiscoveryId; map oldwebandnativevalues directly, mapuser-agent-basedtoNULLfor manual reclassification, and never derive it frompublic. SetclientDiscoveryIdonly from known discovery provenance, never by inspecting an HTTPS client ID. Deduplicate existing(clientId, resourceId)links before adding the new compound unique index, then drop the legacy columns. Deployments with custom schema mappings must apply this backfill manually.Machine-to-machine scope authority is now stored separately in nullable
oauthClient.clientCredentialsScopes. Missing,NULL, and empty values denyclient_credentialstoken issuance. Only the administrative create and update endpoints exposeclient_credentials_scopes, and assigning a non-empty value requiresclientPrivilegesto approve the newconfigure-client-credentials-scopesaction. DCR, CIMD, and user-managed registration cannot assign this field; CIMD refresh preserves an existing administrator-owned value. RemoveclientCredentialGrantDefaultScopes, backfill every existing client to[], configure[]as the default for new rows, then explicitly assign every approved machine scope after auditing the client. - Dynamic, administrative, and user-managed registrations default an omitted
Features
- feat(cimd): add Client ID Metadata Document plugin (#9159)
Bug Fixes
- Client ID Metadata Documents now follow shared-cache freshness rules and fail closed when freshness is ambiguous. The plugin prefers
s-maxageovermax-ageandExpires, honorss-maxage=0, conditionally revalidates with ETag or Last-Modified, and treats invalid or duplicate freshness directives as immediately stale. Concurrent refreshes converge on one client-resource link instead of failing on its unique constraint.
For detailed changes, see CHANGELOG
@better-auth/api-key
❗ Breaking Changes
- feat(auth)!: harden atomic state transitions (#10000)
Bug Fixes
- chore: sync main to next (#9533)
For detailed changes, see CHANGELOG
@better-auth/kysely-adapter
Bug Fixes
- fix(kysely-adapter): restore local migration constants (#10377)
- Raw database instances (better-sqlite3,
node:sqlite,bun:sqlite,mysql2,pg) passed directly asdatabasenow get native adapter transactions automatically, matching the behavior of the explicit{ db }/{ dialect }config shapes. This unblocks plugins that require native transactions (such as@better-auth/scim) when the database is provided in the quickstartdatabase: new Database(...)shape.
For detailed changes, see CHANGELOG
@better-auth/i18n
Features
- feat(i18n): add built-in translations for 22 languages (#9157)
For detailed changes, see CHANGELOG
@better-auth/mongo-adapter
Features
- feat(db): add compound table indexes (#10402)
For detailed changes, see CHANGELOG
@better-auth/passkey
Features
- feat(passkey): create session during passkey registration (#9873)
For detailed changes, see CHANGELOG
Contributors
Thanks to everyone who contributed to this release:
@adrianmxb, @app/better-release, @brentmitchell25, @bytaesu, @GautamBytes, @gustavovalverde, @ItalyPaleAle, @momomuchu, @OscarCornish, @pi0, @ping-maxwell, @ruban-s, @sovetski, @yordis
Full changelog: v1.6.30...v1.7.0