Every release shipped to Better Auth, straight from GitHub.
Every release shipped to Better Auth, straight from GitHub.
Blog post: Better Auth 1.7
better-authchore!: 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.issuer but preserves Account.accountId as the provider-assigned account identifier. Account-specific APIs select the local Account.id through the accountId request property; token and provider-profile APIs can instead select the signed account cookie with useAccountCookie: true. Credential accounts use local:credential and the linked user's stable id as their provider identity.
OAuth provider identity now comes from raw verified profiles. OpenID Connect discovery uses sub, plain OAuth uses id, and providers can declare accountSubject for another immutable field; Better Auth no longer switches between sub and id at runtime. getUserInfo().user no longer carries provider identity, and mapProfileToUser cannot return id. Read the selected identity from accountInfo.account.accountId instead of accountInfo.user.id. The generic microsoftEntraId helper 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 sub claim, and SAML uses the signed NameID; mapping.id is removed from both configurations. A manual SAML configuration without metadata XML must set idpMetadata.entityID, because samlConfig.issuer identifies 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 withMcpAuth to requireMcpAuth. The standalone protected-resource factory is renamed from mcpHandler to createMcpProtectedRequestHandler; pass one flat McpProtectedRequestHandlerOptions object with issuer, a single audience, optional jwtVerifyOptions, token-verification fields, and challenge fields. Its callback receives accessTokenClaims. requireMcpAuth verifies the access token against the published JWKS, validates DPoP proofs for DPoP-bound tokens, and passes the verified access-token claims to your handler.
createInsufficientScopeError now validates a custom description against the RFC 6750 error_description character set when the error is constructed. Invalid descriptions throw TypeError("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, configure createMcpHandler with legacy: "reject", wrap it with requireMcpAuth, and export only POST. Remove MCP-route GET and DELETE exports and session-store options such as redisUrl. 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 the jwt() plugin, which is now required for token signing; and move options that were nested under oidcConfig to flat options on mcp({ ... }). The database models change: oauthApplication becomes oauthClient, with new oauthRefreshToken and oauthClientAssertion tables. Regenerate or migrate your schema with npx auth migrate or npx 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-provider now 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 a backchannel_logout_uri (and optionally backchannel_logout_session_required) via DCR or the admin client-create endpoint. The provider signs a logout+jwt Logout 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/userinfo rejects it with invalid_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_access are revoked on session end; offline_access refresh 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, Cloudflare ctx.waitUntil); without a handler it completes inline so notifications are not lost on request teardown. Configure advanced.backgroundTasks.handler on serverless runtimes to keep sign-out fast.
Discovery at /.well-known/openid-configuration and /.well-known/oauth-authorization-server advertises backchannel_logout_supported: true and backchannel_logout_session_supported: true when the JWT plugin is enabled. Every registered backchannel_logout_uri must 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 a private_key_jwt client's jwks_uri.
Schema changes on @better-auth/oauth-provider:
oauthClient.backchannelLogoutUri: string | nulloauthClient.backchannelLogoutSessionRequired: booleanoauthAccessToken.revoked: Date | nullbetter-auth's signJWT gains an optional header argument, forwarded to custom remote signers. JWT profiles that need an explicit media type, such as typ: "logout+jwt", can now set it without reaching for the low-level signing primitives.
feat(oauth-provider)!: model OAuth protected resources explicitly (#9648)
validAudiences is removed. Move each existing resource identifier into resources; link clients that should be limited to specific resources through oauthClientResource or Dynamic Client Registration resources.
Access-token issuance now applies resource policy to the requested RFC 8707 resource values. The OAuth provider narrows scopes to resource allowlists, uses the shortest configured TTL, strips reserved RFC 9068 claim names from custom claims, emits jti, and keeps repeated resource form parameters.
Refresh-token TTLs now use the shortest applicable lifetime. Deployments with a per-resource refreshTokenTtl longer than refreshTokenExpiresIn will see refresh tokens expire at the provider default instead of the longer resource value.
JWT signing can now honor per-resource pins. signJWT() accepts signingKeyId and signingAlgorithm; JWKS adapters expose getKeyById() and getLatestKeyByAlg(). The jwks table adds nullable alg and crv columns, and keyPairConfigs can provision multiple algorithms in one keyring.
After upgrading, run npx @better-auth/cli generate and apply the migration before deploying. The migration adds oauthResource, oauthClientResource, and the new jwks columns. Without it, resources using signingAlgorithm cannot 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/mcp now requires an explicit resource option. 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. Existing mcp({ loginPage, consentPage }) setups should add a protected MCP resource identifier, for example resource: "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)
enableTwoFactor now accepts a method parameter ("otp" | "totp", default "totp") and returns a discriminated response with a method field.
method: "otp"twoFactorEnabled: true immediately.{ method: "otp" }.otpOptions.sendOTP to be configured on the servermethod: "totp" (default){ method: "totp", totpURI, backupCodes }.TOTP_NOT_CONFIGURED if totpOptions.disable is set.The existing skipVerificationOnEnable option remains supported for TOTP enrollment.
Response shape changed: enableTwoFactor includes a method field in the response ("otp" or "totp").
fix(auth)!: ignore x-forwarded headers by default on dynamic baseURL (#9134)
Requests using baseURL: { allowedHosts } now resolve the auth origin from Host by 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, set advanced.trustedProxyHeaders: true. Deployments where the proxy rewrites Host to 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_error if it cannot create a unique deviceCode and userCode. 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 /device limiter allows 5 requests over a window equal to the configured code lifetime, while /device/token polling 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_method parameter is gone and every authorization code is verified by hashing the verifier with SHA-256. The server no longer trusts an electron-origin header to set the request Origin. The Electron client now sends a real Origin (for example myapp:/), so upgrade the @better-auth/electron client and server together and make sure your app's scheme is in trustedOrigins. The unused disableOriginOverride option is removed.
Custom-scheme entries in trustedOrigins now match by scheme and authority instead of string prefix. A host-less entry such as myapp:// or exp:// still trusts every host of that scheme, but a host-bearing entry such as myapp://callback matches that host exactly, so it is no longer satisfied by myapp://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 by signIn.social({ provider })oauth2.link() replaced by linkSocial()/api/auth/oauth2/callback/:id to /api/auth/callback/:idgenericOAuthClient() removedpkce defaults to true (was false)authorizationUrlParams and tokenUrlParams only accept Record<string, string>issuer and requireIssuerValidation config fields removedmapProfileToUser profile typed as OAuth2UserInfo & Record<string, unknown>refactor(oauth-provider)!: separate device grant ownership (#10746)
The OAuth integration replaces the optional resource column with oauthClientId and resources. 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_tokens with 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-provider verifyIdToken method. Each provider declares an idToken config 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 idToken config, and the client id_token path returns ID_TOKEN_NOT_SUPPORTED. PayPal sign-in through the redirect flow is unchanged.
Custom providers that implement UpstreamProvider directly replace the removed verifyIdToken method with an idToken config:
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 }. The verifyIdToken and disableIdTokenSignIn provider options are unchanged.
clientAssertion support to the Microsoft Entra ID social provider (#9898)Auth instance fetchable (#9431)requireEmailVerification for social sign-in (#9929)hydrateSession for SSR session hydration (#8733)refreshTokenParams to token endpoint (#9948)id_tokens and enable id_token sign-in (#9966)at_hash in id tokens per OIDC Core §3.1.3.6 (#9079)private_key_jwt client authentication (RFC 7523) (#8836)additionalParams and loginHint (#9305)For detailed changes, see CHANGELOG
@better-auth/oauth-providerfeat(mcp)!: ship MCP as its own package built on the OAuth provider (#9992)
The shared-auth route helper is renamed from withMcpAuth to requireMcpAuth. The standalone protected-resource factory is renamed from mcpHandler to createMcpProtectedRequestHandler; pass one flat McpProtectedRequestHandlerOptions object with issuer, a single audience, optional jwtVerifyOptions, token-verification fields, and challenge fields. Its callback receives accessTokenClaims. requireMcpAuth verifies the access token against the published JWKS, validates DPoP proofs for DPoP-bound tokens, and passes the verified access-token claims to your handler.
createInsufficientScopeError now validates a custom description against the RFC 6750 error_description character set when the error is constructed. Invalid descriptions throw TypeError("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, configure createMcpHandler with legacy: "reject", wrap it with requireMcpAuth, and export only POST. Remove MCP-route GET and DELETE exports and session-store options such as redisUrl. 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 the jwt() plugin, which is now required for token signing; and move options that were nested under oidcConfig to flat options on mcp({ ... }). The database models change: oauthApplication becomes oauthClient, with new oauthRefreshToken and oauthClientAssertion tables. Regenerate or migrate your schema with npx auth migrate or npx 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-provider now 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 a backchannel_logout_uri (and optionally backchannel_logout_session_required) via DCR or the admin client-create endpoint. The provider signs a logout+jwt Logout 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/userinfo rejects it with invalid_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_access are revoked on session end; offline_access refresh 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, Cloudflare ctx.waitUntil); without a handler it completes inline so notifications are not lost on request teardown. Configure advanced.backgroundTasks.handler on serverless runtimes to keep sign-out fast.
Discovery at /.well-known/openid-configuration and /.well-known/oauth-authorization-server advertises backchannel_logout_supported: true and backchannel_logout_session_supported: true when the JWT plugin is enabled. Every registered backchannel_logout_uri must 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 a private_key_jwt client's jwks_uri.
Schema changes on @better-auth/oauth-provider:
oauthClient.backchannelLogoutUri: string | nulloauthClient.backchannelLogoutSessionRequired: booleanoauthAccessToken.revoked: Date | nullbetter-auth's signJWT gains an optional header argument, forwarded to custom remote signers. JWT profiles that need an explicit media type, such as typ: "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)
OAuthClient no longer has a catch-all string index. Model custom wire extensions explicitly with a named intersection such as OAuthClient & YourExtensionMetadata; legacy type and public fields no longer type-check as unknown baggage.
application_type to web. Client ID Metadata Documents preserve an omitted value as null.mcp() contributes its protected resource by default, so standards-based clients no longer need a resources extension.mcp() no longer enables unauthenticated Dynamic Client Registration. Compose mcp() with cimd() for Client ID Metadata Documents, or enable both DCR flags explicitly.This release requires a database migration. Add applicationType and nullable clientDiscoveryId; map old web and native values directly, map user-agent-based to NULL for manual reclassification, and never derive it from public. Set clientDiscoveryId only 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 deny client_credentials token issuance. Only the administrative create and update endpoints expose client_credentials_scopes, and assigning a non-empty value requires clientPrivileges to approve the new configure-client-credentials-scopes action. DCR, CIMD, and user-managed registration cannot assign this field; CIMD refresh preserves an existing administrator-owned value. Remove clientCredentialGrantDefaultScopes, backfill every existing client to [], configure [] as the default for new rows, then explicitly assign every approved machine scope after auditing the client.
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". Because
acr_values is voluntary, requests for other classes continue instead of
failing. Essential claims.id_token.acr requests in OpenID Connect flows still
fail when their required value or values cannot be met.
customIdTokenClaims, extension ID-token claims, and per-issuance idTokenClaims can no longer set OIDC/JWT protocol claims such as issuer, subject, audience, token lifetime, nonce, session or hash binding, auth_time, acr, amr, or azp. Namespaced custom claims still appear in ID tokens.
feat(oauth-provider)!: model OAuth protected resources explicitly (#9648)
validAudiences is removed. Move each existing resource identifier into resources; link clients that should be limited to specific resources through oauthClientResource or Dynamic Client Registration resources.
Access-token issuance now applies resource policy to the requested RFC 8707 resource values. The OAuth provider narrows scopes to resource allowlists, uses the shortest configured TTL, strips reserved RFC 9068 claim names from custom claims, emits jti, and keeps repeated resource form parameters.
Refresh-token TTLs now use the shortest applicable lifetime. Deployments with a per-resource refreshTokenTtl longer than refreshTokenExpiresIn will see refresh tokens expire at the provider default instead of the longer resource value.
JWT signing can now honor per-resource pins. signJWT() accepts signingKeyId and signingAlgorithm; JWKS adapters expose getKeyById() and getLatestKeyByAlg(). The jwks table adds nullable alg and crv columns, and keyPairConfigs can provision multiple algorithms in one keyring.
After upgrading, run npx @better-auth/cli generate and apply the migration before deploying. The migration adds oauthResource, oauthClientResource, and the new jwks columns. Without it, resources using signingAlgorithm cannot 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/mcp now requires an explicit resource option. 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. Existing mcp({ loginPage, consentPage }) setups should add a protected MCP resource identifier, for example resource: "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 returns invalid_target. The customAccessTokenClaims callback now receives a resources array in place of the resource string.
Migration: run the schema migration (npx @better-auth/cli migrate, or generate if 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 state and iss. The response uses the URL fragment for implicit token and id_token responses 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 unrecognized token_type_hint values instead of rejecting the request.
refactor(generic-oauth)!: rewrite as first-class social provider with RFC compliance (#9069)
Breaking changes:
signIn.oauth2({ providerId }) replaced by signIn.social({ provider })oauth2.link() replaced by linkSocial()/api/auth/oauth2/callback/:id to /api/auth/callback/:idgenericOAuthClient() removedpkce defaults to true (was false)authorizationUrlParams and tokenUrlParams only accept Record<string, string>issuer and requireIssuerValidation config fields removedmapProfileToUser profile typed as OAuth2UserInfo & Record<string, unknown>refactor(oauth-provider)!: separate device grant ownership (#10746)
The OAuth integration replaces the optional resource column with oauthClientId and resources. 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.
at_hash in id tokens per OIDC Core §3.1.3.6 (#9079)id_token claim contributors (#10113)private_key_jwt client authentication (RFC 7523) (#8836)private_key_jwt jti single-use atomic across processes (#9964)redirect_uri conditional at the token endpoint (#10159)response_type errors (#10149)unsupported_token_type for JWT access-token revocation (#9970)403 with an RFC 6750 insufficient_scope WWW-Authenticate challenge that names every missing scope. Clients can union those scopes into one authorization request instead of opening one browser redirect per scope.For detailed changes, see CHANGELOG
@better-auth/corechore!: 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.issuer but preserves Account.accountId as the provider-assigned account identifier. Account-specific APIs select the local Account.id through the accountId request property; token and provider-profile APIs can instead select the signed account cookie with useAccountCookie: true. Credential accounts use local:credential and the linked user's stable id as their provider identity.
OAuth provider identity now comes from raw verified profiles. OpenID Connect discovery uses sub, plain OAuth uses id, and providers can declare accountSubject for another immutable field; Better Auth no longer switches between sub and id at runtime. getUserInfo().user no longer carries provider identity, and mapProfileToUser cannot return id. Read the selected identity from accountInfo.account.accountId instead of accountInfo.user.id. The generic microsoftEntraId helper 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 sub claim, and SAML uses the signed NameID; mapping.id is removed from both configurations. A manual SAML configuration without metadata XML must set idpMetadata.entityID, because samlConfig.issuer identifies 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_tokens with 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-provider verifyIdToken method. Each provider declares an idToken config 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 idToken config, and the client id_token path returns ID_TOKEN_NOT_SUPPORTED. PayPal sign-in through the redirect flow is unchanged.
Custom providers that implement UpstreamProvider directly replace the removed verifyIdToken method with an idToken config:
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 }. The verifyIdToken and disableIdTokenSignIn provider options are unchanged.
clientAssertion support to the Microsoft Entra ID social provider (#9898)requireEmailVerification for social sign-in (#9929)refreshTokenParams to token endpoint (#9948)includeGrantedScopes option (#10129)private_key_jwt client authentication (RFC 7523) (#8836)additionalParams and loginHint (#9305)client_id SSRF checks through the shared host classifier (#10126)For detailed changes, see CHANGELOG
@better-auth/ssofeat(auth)!: scope accounts by issuer (#10403)
This release requires Account.issuer but preserves Account.accountId as the provider-assigned account identifier. Account-specific APIs select the local Account.id through the accountId request property; token and provider-profile APIs can instead select the signed account cookie with useAccountCookie: true. Credential accounts use local:credential and the linked user's stable id as their provider identity.
OAuth provider identity now comes from raw verified profiles. OpenID Connect discovery uses sub, plain OAuth uses id, and providers can declare accountSubject for another immutable field; Better Auth no longer switches between sub and id at runtime. getUserInfo().user no longer carries provider identity, and mapProfileToUser cannot return id. Read the selected identity from accountInfo.account.accountId instead of accountInfo.user.id. The generic microsoftEntraId helper 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 sub claim, and SAML uses the signed NameID; mapping.id is removed from both configurations. A manual SAML configuration without metadata XML must set idpMetadata.entityID, because samlConfig.issuer identifies 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.cert and samlConfig.idpMetadata.cert accept either a single PEM string or an array. When both are set, idpMetadata.cert wins.
Breaking: response shape
The management endpoints (getSSOProvider, listSSOProviders, updateSSOProvider) now return samlConfig.certificate as an array of parsed certificates in every case, even when a single cert is configured. The field is absent only when certs live inside idpMetadata.metadata. Update consumers to read an array; no more Array.isArray branching.
Validation
Registration now rejects SAML configs that supply no signing-cert source. samlify needs either an idpMetadata.metadata XML document (which embeds the certs) or an explicit PEM under cert or idpMetadata.cert. Configs missing both fail with CERT_SOURCE_MISSING.
Fix
SAML Single Logout could fail to decrypt encrypted LogoutResponse payloads because the IdP entity was constructed without privateKey, encPrivateKey, or encPrivateKeyPass on 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)
allowIdpInitiated now defaults to false — IdP-initiated SSO (unsolicited SAML responses) is disabled by default. Set saml.allowIdpInitiated: true to restore the previous behavior. This aligns with the SAML2Int interoperability profile which recommends against IdP-initiated SSO due to its susceptibility to injection attacks.extract.inResponseTo (always undefined) instead of samlify's actual path extract.response.inResponseTo. SP-initiated InResponseTo validation now works as intended in both ACS handlers.<AudienceRestriction> element. Audience is now validated against the configured samlConfig.audience value per SAML 2.0 Core §2.5.1.sessionIndex from login responses as { authnInstant, sessionNotOnOrAfter, sessionIndex }, but the code stored the whole object. SLO session-index comparisons always failed silently. The correct inner sessionIndex string is now extracted.Extracted shared validateInResponseTo() and validateAudience() into packages/sso/src/saml/response-validation.ts, eliminating ~160 lines of duplicated validation logic between the two ACS handlers.
Fixed SAMLAssertionExtract type to match samlify's actual extractor output shape.
refactor(sso)!: remove callbackUrl, consolidate ACS endpoint, fix SLO (#9117)
callbackUrl no longer configures the ACS URL.
The default ACS URL is derived from baseURL and providerId. Use callbackUrl
as the provider-level post-auth redirect, or pass callbackURL to signIn.sso()
for an SP-initiated request:
await authClient.signIn.sso({ providerId: "my-provider", callbackURL: "/dashboard",});/sso/saml2/callback/:providerId endpoint removed.
Update your IdP's ACS URL to /sso/saml2/sp/acs/:providerId. This endpoint handles both GET and POST requests.
spMetadata is now optional.
You no longer need to pass spMetadata: {} 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.
audience is not configured, per SAML Core section 2.5.1.AllowCreate in AuthnRequests, required by IdPs that use JIT provisioning.private_key_jwt client authentication (RFC 7523) (#8836)additionalParams and loginHint (#9305)wantAssertionsSigned now 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/scimfeat(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
optional managedConnections catalog.
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 scimProvider rows and credentials are not migrated. Follow the 1.7 SCIM
upgrade guide, issue new credentials, and fully reprovision Users and Groups.
active and the primary sub-attribute of emails, phoneNumbers, addresses, roles, and entitlements at the HTTP ingress for Microsoft Entra interoperability.managedConnections to let trusted server code create runtime tenant connections and issue, rotate, and revoke their bearer credentials through server-only auth.api methods, without a code-defined connection or an application-owned verifier.For detailed changes, see CHANGELOG
@better-auth/mcp ✨feat(mcp)!: ship MCP as its own package built on the OAuth provider (#9992)
The shared-auth route helper is renamed from withMcpAuth to requireMcpAuth. The standalone protected-resource factory is renamed from mcpHandler to createMcpProtectedRequestHandler; pass one flat McpProtectedRequestHandlerOptions object with issuer, a single audience, optional jwtVerifyOptions, token-verification fields, and challenge fields. Its callback receives accessTokenClaims. requireMcpAuth verifies the access token against the published JWKS, validates DPoP proofs for DPoP-bound tokens, and passes the verified access-token claims to your handler.
createInsufficientScopeError now validates a custom description against the RFC 6750 error_description character set when the error is constructed. Invalid descriptions throw TypeError("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, configure createMcpHandler with legacy: "reject", wrap it with requireMcpAuth, and export only POST. Remove MCP-route GET and DELETE exports and session-store options such as redisUrl. 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 the jwt() plugin, which is now required for token signing; and move options that were nested under oidcConfig to flat options on mcp({ ... }). The database models change: oauthApplication becomes oauthClient, with new oauthRefreshToken and oauthClientAssertion tables. Regenerate or migrate your schema with npx auth migrate or npx auth generate.
feat(oauth-provider)!: align MCP authorization with 2026-07-28 (#10577)
OAuthClient no longer has a catch-all string index. Model custom wire extensions explicitly with a named intersection such as OAuthClient & YourExtensionMetadata; legacy type and public fields no longer type-check as unknown baggage.
application_type to web. Client ID Metadata Documents preserve an omitted value as null.mcp() contributes its protected resource by default, so standards-based clients no longer need a resources extension.mcp() no longer enables unauthenticated Dynamic Client Registration. Compose mcp() with cimd() for Client ID Metadata Documents, or enable both DCR flags explicitly.This release requires a database migration. Add applicationType and nullable clientDiscoveryId; map old web and native values directly, map user-agent-based to NULL for manual reclassification, and never derive it from public. Set clientDiscoveryId only 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 deny client_credentials token issuance. Only the administrative create and update endpoints expose client_credentials_scopes, and assigning a non-empty value requires clientPrivileges to approve the new configure-client-credentials-scopes action. DCR, CIMD, and user-managed registration cannot assign this field; CIMD refresh preserves an existing administrator-owned value. Remove clientCredentialGrantDefaultScopes, backfill every existing client to [], configure [] as the default for new rows, then explicitly assign every approved machine scope after auditing the client.
feat(oauth-provider)!: model OAuth protected resources explicitly (#9648)
validAudiences is removed. Move each existing resource identifier into resources; link clients that should be limited to specific resources through oauthClientResource or Dynamic Client Registration resources.
Access-token issuance now applies resource policy to the requested RFC 8707 resource values. The OAuth provider narrows scopes to resource allowlists, uses the shortest configured TTL, strips reserved RFC 9068 claim names from custom claims, emits jti, and keeps repeated resource form parameters.
Refresh-token TTLs now use the shortest applicable lifetime. Deployments with a per-resource refreshTokenTtl longer than refreshTokenExpiresIn will see refresh tokens expire at the provider default instead of the longer resource value.
JWT signing can now honor per-resource pins. signJWT() accepts signingKeyId and signingAlgorithm; JWKS adapters expose getKeyById() and getLatestKeyByAlg(). The jwks table adds nullable alg and crv columns, and keyPairConfigs can provision multiple algorithms in one keyring.
After upgrading, run npx @better-auth/cli generate and apply the migration before deploying. The migration adds oauthResource, oauthClientResource, and the new jwks columns. Without it, resources using signingAlgorithm cannot 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/mcp now requires an explicit resource option. 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. Existing mcp({ loginPage, consentPage }) setups should add a protected MCP resource identifier, for example resource: "https://api.example.com/mcp".
For detailed changes, see CHANGELOG
@better-auth/electronfix(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_method parameter is gone and every authorization code is verified by hashing the verifier with SHA-256. The server no longer trusts an electron-origin header to set the request Origin. The Electron client now sends a real Origin (for example myapp:/), so upgrade the @better-auth/electron client and server together and make sure your app's scheme is in trustedOrigins. The unused disableOriginOverride option is removed.
Custom-scheme entries in trustedOrigins now match by scheme and authority instead of string prefix. A host-less entry such as myapp:// or exp:// still trusts every host of that scheme, but a host-bearing entry such as myapp://callback matches that host exactly, so it is no longer satisfied by myapp://callback.attacker.tld.
refactor(generic-oauth)!: rewrite as first-class social provider with RFC compliance (#9069)
Breaking changes:
signIn.oauth2({ providerId }) replaced by signIn.social({ provider })oauth2.link() replaced by linkSocial()/api/auth/oauth2/callback/:id to /api/auth/callback/:idgenericOAuthClient() removedpkce defaults to true (was false)authorizationUrlParams and tokenUrlParams only accept Record<string, string>issuer and requireIssuerValidation config fields removedmapProfileToUser profile typed as OAuth2UserInfo & Record<string, unknown>For detailed changes, see CHANGELOG
@better-auth/expofix(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 by signIn.social({ provider })oauth2.link() replaced by linkSocial()/api/auth/oauth2/callback/:id to /api/auth/callback/:idgenericOAuthClient() removedpkce defaults to true (was false)authorizationUrlParams and tokenUrlParams only accept Record<string, string>issuer and requireIssuerValidation config fields removedmapProfileToUser profile typed as OAuth2UserInfo & Record<string, unknown>For detailed changes, see CHANGELOG
@better-auth/stripeonSubscriptionCancel.event required (#9531)event (#9359)For detailed changes, see CHANGELOG
authgrantedScopes string[] (#9825)resolveModule for auth config loading (#9477)For detailed changes, see CHANGELOG
@better-auth/drizzle-adapterchore!: 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).
For detailed changes, see CHANGELOG
@better-auth/cimd ✨feat(oauth-provider)!: align MCP authorization with 2026-07-28 (#10577)
OAuthClient no longer has a catch-all string index. Model custom wire extensions explicitly with a named intersection such as OAuthClient & YourExtensionMetadata; legacy type and public fields no longer type-check as unknown baggage.
application_type to web. Client ID Metadata Documents preserve an omitted value as null.mcp() contributes its protected resource by default, so standards-based clients no longer need a resources extension.mcp() no longer enables unauthenticated Dynamic Client Registration. Compose mcp() with cimd() for Client ID Metadata Documents, or enable both DCR flags explicitly.This release requires a database migration. Add applicationType and nullable clientDiscoveryId; map old web and native values directly, map user-agent-based to NULL for manual reclassification, and never derive it from public. Set clientDiscoveryId only 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 deny client_credentials token issuance. Only the administrative create and update endpoints expose client_credentials_scopes, and assigning a non-empty value requires clientPrivileges to approve the new configure-client-credentials-scopes action. DCR, CIMD, and user-managed registration cannot assign this field; CIMD refresh preserves an existing administrator-owned value. Remove clientCredentialGrantDefaultScopes, backfill every existing client to [], configure [] as the default for new rows, then explicitly assign every approved machine scope after auditing the client.
s-maxage over max-age and Expires, honors s-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-keyFor detailed changes, see CHANGELOG
@better-auth/kysely-adapternode:sqlite, bun:sqlite, mysql2, pg) passed directly as database now 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 quickstart database: new Database(...) shape.For detailed changes, see CHANGELOG
@better-auth/i18nFor detailed changes, see CHANGELOG
@better-auth/mongo-adapterFor detailed changes, see CHANGELOG
@better-auth/passkeyFor detailed changes, see CHANGELOG
Thanks to everyone who contributed to this release:
Full changelog: v1.6.30...v1.7.0
better-authFor detailed changes, see CHANGELOG
@better-auth/sso409 with SSO_PROVIDER_CHANGED if the provider changes during DNS resolution so callers can reload and retry.For detailed changes, see CHANGELOG
Thanks to everyone who contributed to this release:
Full changelog: v1.6.29...v1.6.30
better-authdeleteSessions performance by running deletes in parallel instead of sequentially (#10805)For detailed changes, see CHANGELOG
@better-auth/sso409 with SSO_PROVIDER_CHANGED if the provider changes during DNS verification so callers can reload and retry.For detailed changes, see CHANGELOG
Thanks to everyone who contributed to this release:
Full changelog: v1.6.28...v1.6.29
better-authFor detailed changes, see the CHANGELOG.
@better-auth/electronFor detailed changes, see the CHANGELOG.
@better-auth/expoFor detailed changes, see the CHANGELOG.
Thanks to everyone who contributed to this release:
Full changelog: v1.6.27...v1.6.28
better-authFor detailed changes, see CHANGELOG
@better-auth/scimbetter-call (#10657)For detailed changes, see CHANGELOG
authFor detailed changes, see CHANGELOG
Thanks to everyone who contributed to this release:
Full changelog: v1.6.26...v1.6.27
better-authfindSessions to skip invalid secondary-storage session entries without discarding other valid sessions (#10580)jwtClient() collapsing createAuthClient type inference when combined with other client plugins (#10513)oAuthProxy to preserve Apple user data from form_post callbacks (#10599)oneTapClient() collapsing createAuthClient type inference when combined with other client plugins (#10635)nextCookies performance in instrumented Next.js applications by reusing the next/headers import promise (#10467)For detailed changes, see CHANGELOG
@better-auth/coreplaceholder.invalid domain (#10576)For detailed changes, see CHANGELOG
@better-auth/redis-storagelistKeys() and clear() to use SCAN instead of KEYS so large keyspaces no longer block the Redis server (#10507)For detailed changes, see CHANGELOG
Thanks to everyone who contributed to this release:
Full changelog: v1.6.25...v1.6.26
better-auth$fetch and $store not being exposed on the Solid client (#10444)modelName was set to another table's schema key (e.g. user.modelName = "account").For detailed changes, see CHANGELOG
Thanks to everyone who contributed to this release:
Full changelog: v1.6.24...v1.6.25
better-authctx) as a third argument to verifyIdToken, enabling custom ID token verifiers to read request headers (#10376)beforeStoreCookie option to the last-login-method plugin for GDPR compliance (#5753)get-session endpoint to include no-store cache control headers, preventing stale session data from being served (#10222)BIGINT as a valid number type, preventing spurious pending changes on rate limiter columns (#10316)useSession({ throw: true }) incorrectly excluding null from its data type (#9787)CookieAttributes index signature type to be more precise (#10442)user.modelName was set to a value that collides with another schema key (#10235)unique and index (#10357)Origin header on cookieless requests, preventing cross-origin abuse (#10368)username and displayUsername) in /sign-up/email and /update-user request bodies (#10453)organization.listMembers failing with "User not found for member" for organizations with more than ~100 members (#10342)advanced.database.generateId is configured, matching the behavior of other models (#10040)getDefaultModelName to prefer exact schema key matches over modelName aliases, preventing adapter queries from being misrouted when a built-in table's name collides with another schema keyFor detailed changes, see CHANGELOG
authunique and index (#10333)relationName values (#10352)auth generate failing when the config file imports the not-yet-generated output file (e.g. on a Convex first run) (#10302)For detailed changes, see CHANGELOG
@better-auth/electron>=36.0.0) (#10440)/electron/init-oauth-proxy forwarding multiple Set-Cookie headers as a single comma-joined string, which caused the browser to drop the transfer-token cookie during OAuth handoff (#9672)For detailed changes, see CHANGELOG
@better-auth/coreAsyncLocalStorage initialization on serverless cold starts (e.g. Cloudflare Workers) (#9862)For detailed changes, see CHANGELOG
@better-auth/ssoidpInitiatedCallbackUrl (#10388)For detailed changes, see CHANGELOG
@better-auth/stripebeforeDeleteOrganization and afterDeleteOrganization hooks not receiving the endpoint context as the second argument (#10190)For detailed changes, see CHANGELOG
Thanks to everyone who contributed to this release:
Full changelog: v1.6.23...v1.6.24
better-authFor detailed changes, see CHANGELOG
@better-auth/drizzle-adapterFor detailed changes, see CHANGELOG
@better-auth/stripeFor detailed changes, see CHANGELOG
authFor detailed changes, see CHANGELOG
Thanks to everyone who contributed to this release:
Full changelog: v1.6.22...v1.6.23
better-authFor detailed changes, see CHANGELOG
@better-auth/scimactive attribute (#10242)For detailed changes, see CHANGELOG
@better-auth/stripeFor detailed changes, see CHANGELOG
authFor detailed changes, see CHANGELOG
Thanks to everyone who contributed to this release:
Full changelog: v1.6.21...v1.6.22
better-authdeviceAuthorization() throwing a ZodError when called without a schema option under Zod v4 (#9939)input: false (#10196)TOO_MANY_ATTEMPTS_REQUEST_NEW_CODE (#10210)displayUsername fallbacks that pass username validation during email sign-up (#10182)For detailed changes, see CHANGELOG
@better-auth/ssojavascript: or data: (#10225)For detailed changes, see CHANGELOG
@better-auth/api-keyX-Forwarded-For spoofing in multi-hop proxy chains (#10203)For detailed changes, see CHANGELOG
authdisableMigration: true to be respected on plugin schema tables during generation and runtime migration (#10198)BETTER_AUTH_SECRET values with 32 characters instead of 16 (#10186)For detailed changes, see CHANGELOG
@better-auth/kysely-adapteradapter.update to return null when no matching row is found (#10180)For detailed changes, see CHANGELOG
@better-auth/stripeFor detailed changes, see CHANGELOG
Thanks to everyone who contributed to this release:
Full changelog: v1.6.20...v1.6.21
better-authAPIError properties (#8734)Max-Age to be capped at expiresIn (#9621)For detailed changes, see CHANGELOG
@better-auth/i18nFor detailed changes, see CHANGELOG
Thanks to everyone who contributed to this release:
Full changelog: v1.6.19...v1.6.20
better-authsendVerificationEmail errors not being surfaced to the client (#8863)last-login-method plugin (#9319)oauth-popup plugin leaking internal OAuth state keys into additionalData (#10067)For detailed changes, see CHANGELOG
authgenerate command not handling a directory path passed to --output (#9564)additionalField default values not being serialized correctly in the Drizzle schema generator (#10048)For detailed changes, see CHANGELOG
@better-auth/drizzle-adapterFor detailed changes, see CHANGELOG
@better-auth/mongo-adapterFor detailed changes, see CHANGELOG
@better-auth/passkeyFor detailed changes, see CHANGELOG
@better-auth/scimFor detailed changes, see CHANGELOG
Thanks to everyone who contributed to this release:
Full changelog: v1.6.18...v1.6.19
better-authgetCookieCache to return null for expired sessions instead of treating stale signed cookies as live sessions.<id>@reddit.invalid) to users with no email, preventing accidental matches with real mailboxes.internalAdapter.reserveVerificationValue to atomically record single-use markers, ensuring only one concurrent caller succeeds for replay-protected operations.incrementOne adapter method and SecondaryStorage.increment for atomic counter updates, enabling strict rate-limit and usage-counter enforcement under concurrent load./delete-user/callback to reject account deletion when the session has been revoked server-side (cookie-only session deployments are unaffected).consume method for custom storage backends to opt into strict enforcement.offline_access scope for refresh token usage.oneTap plugin or socialProviders.google) and reject tokens issued for other applications.set-active and revoke endpoints to only act on sessions the caller holds a signed cookie for, preventing unauthorized session manipulation./oauth2/endsession endpoint to reject cross-site logout requests that carry only a session cookie without a valid id_token_hint.mapProfileToUser available to supply a real one.For detailed changes, see CHANGELOG
@better-auth/ssotrustEmailVerified to treat only a boolean true or the string "true" as a verified email, rejecting the string "false" as unverified.For detailed changes, see CHANGELOG
@better-auth/memory-adapterupdate and delete no-ops on empty filters instead of modifying every row.For detailed changes, see CHANGELOG
@better-auth/oauth-providerFor detailed changes, see CHANGELOG
@better-auth/scimFor detailed changes, see CHANGELOG
@better-auth/api-keyFor detailed changes, see CHANGELOG
@better-auth/drizzle-adapterupdateMany to return the number of rows it affected, as the adapter contract specifies.For detailed changes, see CHANGELOG
@better-auth/electronFor detailed changes, see CHANGELOG
@better-auth/kysely-adapterconsumeOne compatibility with SQL Server.For detailed changes, see CHANGELOG
@better-auth/passkeyFor detailed changes, see CHANGELOG
@better-auth/prisma-adapterdelete operation to surface errors instead of silently reporting success when the failure is not a missing-record error.For detailed changes, see CHANGELOG
@better-auth/redis-storageincrement method for strict enforcement.For detailed changes, see CHANGELOG
Thanks to everyone who contributed to this release:
Full changelog: v1.6.17...v1.6.18
better-authoauthPopup plugin for popup-based OAuth sign-in, enabling sign-in inside cross-site iframes by completing the OAuth flow in a popup and passing the session token back via the bearer plugin (#9890)getCookieCache to return null for an expired session instead of stale data, so middleware no longer treats an expired signed cookie as a live session.<id>@reddit.invalid) to users with no email, instead of one on the real reddit.com domain, preventing accidental mailbox matches. The address stays unverified, and mapProfileToUser can supply a real email.internalAdapter.reserveVerificationValue for atomic single-use markers, ensuring exactly one concurrent caller succeeds and the rest see the marker as already taken, hardening replay protection across all verification flows. Database-backed storage is atomicincrementOne adapter method and SecondaryStorage.increment for atomic counter updates with conditional row guards, enabling strict enforcement of rate limits and usage counters. Adapters without native support fall back to a transaction-based approach./delete-user/callback to reject account deletion when the session has been revoked server-side, instead of proceeding within the cookie-cache window. Deployments that keep sessions only in the cookie are unaffected.consume method for strict enforcement.offline_access scope.addMember and add-team-member paths, preventing teams from exceeding their maximumMembersPerTeam cap, and ensured a rejected addMember does not create the organization member (#10002)sub or id field when mapProfileToUser derives the account id (#9987)admin.setUserPassword to create a credential account for users who only have social or magic-link accounts, enabling direct password assignment without manually modifying the account table (#9482)updateSession to accept custom session fields inferred from inferAdditionalFields (#9777)/get-session requests triggered by focus and other browser events, stabilized client hook data references to reduce unnecessary re-renders, and resolved session state getting stuck loading after unmounting during an in-flight request (#8760)id fields as required (#9704)updateMemberRole to reject unknown or malformed role values, validating them against configured static and dynamic roles (#9962)role.authorize control flow without changing existing authorization behavior (#9677)set-active and revoke endpoints to only act on sessions the caller holds a signed cookie for, preventing unauthorized activation or revocation of other sessions./oauth2/endsession endpoint to reject cross-site GET logout requests carrying only a session cookie, while leaving logout authenticated by a valid id_token_hint unaffected.For detailed changes, see CHANGELOG
@better-auth/api-keyFor detailed changes, see CHANGELOG
@better-auth/ssotrustEmailVerified to no longer treat the string "false" as a verified email, accepting only a boolean true or the string "true" as confirmation.For detailed changes, see CHANGELOG
auth$app/*, $env/*), Vite asset imports (?raw, ?url), and Cloudflare Workers (cloudflare:workers) virtual-module imports when loading the auth config (#9834)Unsupported() fields when regenerating the Prisma schema (#10011)BigInt or Int when bigint configuration changes (#9729)For detailed changes, see CHANGELOG
@better-auth/expotrustedOrigins (#9990)linkSocial (#9953)For detailed changes, see CHANGELOG
@better-auth/memory-adapterupdate and delete with an empty filter a no-op instead of affecting all rows, and made updateMany return the number of affected rows.For detailed changes, see CHANGELOG
@better-auth/scimFor detailed changes, see CHANGELOG
@better-auth/coreFor detailed changes, see CHANGELOG
@better-auth/drizzle-adapterupdateMany to return the number of rows it affected, as the adapter contract specifies.For detailed changes, see CHANGELOG
@better-auth/electronFor detailed changes, see CHANGELOG
@better-auth/kysely-adapterconsumeOne work on SQL Server.For detailed changes, see CHANGELOG
@better-auth/oauth-providerFor detailed changes, see CHANGELOG
@better-auth/passkeyFor detailed changes, see CHANGELOG
@better-auth/prisma-adapterdelete errors instead of silently reporting success when a deletion fails for any reason other than the record being absent.For detailed changes, see CHANGELOG
@better-auth/redis-storageincrement method to Redis secondary storage.For detailed changes, see CHANGELOG
@better-auth/stripereturnUrl against trustedOrigins, and checked all subscriptions on organization deletion (#9971)For detailed changes, see CHANGELOG
Thanks to everyone who contributed to this release:
Full changelog: v1.6.16...v1.6.17
better-authhd (hosted domain) enforcement to verify the hd claim on the verified ID token and callback profile, preventing accounts outside the configured Workspace domain from signing in.verifyAccessToken remote introspection to reject tokens with a missing or mismatching aud claim/admin/create-user and /admin/update-user, and prevent data from overriding protected fields. (#9974)Origin and Referer headers against trustedOrigins even when requests carry no cookies. (#9973)/update-session to reject plugin-managed fields (activeOrganizationId, activeTeamId, impersonatedBy) with a 400 error/update-session and account token routes to immediately reject deleted sessions when cookie cache is enabled alongside database or secondary storage. (#9967)/refresh-token to only trust the account cookie when its userId, providerId, and accountId match the resolved session user.sub.createInvitation and acceptInvitation to validate that all requested team IDs belong to the invitation's organization, preventing cross-organization team membership.oauth_client_id as the user email, preventing all users of the same app from sharing a single email addressdebug_token endpoint, requiring is_valid, a matching app_id, and a client secret for direct sign-in.For detailed changes, see CHANGELOG
@better-auth/oauth-providerauthorization_code from requesting client_credentials tokens./oauth2/continue to derive post-login gate completion from a server-issued session marker rather than the client-submitted postLogin flag.azp claim and a valid client on JWT access tokens, preventing session JWTs from being reported as active access tokens.For detailed changes, see CHANGELOG
@better-auth/ssoAuthnRequest handling to consume the request atomically, preventing replay attacks on concurrent requests. (#9972)For detailed changes, see CHANGELOG
@better-auth/api-key/api-key/create to verify the session against the authoritative store with disableCookieCache: true, preventing revoked sessions from being accepted within the cookie-cache window.For detailed changes, see CHANGELOG
@better-auth/electroncode_challenge_method values.For detailed changes, see CHANGELOG
@better-auth/scimlinkExistingUsers is set, changed org-scoped DELETE to deprovision the user rather than delete the global account, and added canGenerateToken to control SCIM token creation.For detailed changes, see CHANGELOG
Thanks to everyone who contributed to this release:
Full changelog: v1.6.15...v1.6.16
better-authlistSessions endpoint to properly enforce fresh-age session checks (#9865)unbanUser, setRole, and adminUpdateUser to return USER_NOT_FOUND instead of a generic 500 when the target user does not exist (#9875)For detailed changes, see CHANGELOG
@better-auth/oauth-providerPOST support to the /oauth2/userinfo endpoint, allowing the access token to be passed in the Authorization header (#9937)hooks.before and hooks.after to run correctly when OAuth authorization resumes after sign-in, account selection, or consent (#9919)For detailed changes, see CHANGELOG
@better-auth/kysely-adapterFor detailed changes, see CHANGELOG
@better-auth/passkeygetAuthenticatorName(aaguid) and commonAuthenticatorNames so passkeys can display a friendly provider name like "1Password" or "Google Password Manager" (#9927)For detailed changes, see CHANGELOG
@better-auth/ssoERR_SUBJECT_UNCONFIRMED errors caused by clockSkew not being forwarded to samlify's ServiceProvider when validating SAML responses (#9748)For detailed changes, see CHANGELOG
Thanks to everyone who contributed to this release:
Full changelog: v1.6.14...v1.6.15
advanced: {
database: {
joins: true,
},
}betterAuth({
baseURL: { allowedHosts: [...] },
advanced: {
trustedProxyHeaders: true,
},
});idToken: {
jwks: createRemoteJWKSet(new URL("https://issuer.example/.well-known/jwks.json")),
issuer: "https://issuer.example",
audience: clientId,
},advanced: {
database: {
joins: true,
},
}idToken: {
jwks: createRemoteJWKSet(new URL("https://issuer.example/.well-known/jwks.json")),
issuer: "https://issuer.example",
audience: clientId,
},samlConfig: {
idpMetadata: {
cert: [currentPem, nextPem],
},
}await authClient.signIn.sso({
providerId: "my-provider",
callbackURL: "/dashboard",
});advanced: {
database: {
joins: true,
},
}