chore(deps): update dependency better-auth to v1.7.0 #28
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "renovate/all-minor-patch"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
This PR contains the following updates:
1.6.29→1.7.0Release Notes
better-auth/better-auth (better-auth)
v1.7.0Minor Changes
#8733
4e8e4c7Thanks @bytaesu! - AddhydrateSessionto seed the client with a server-fetched session souseSessionreturns data on the first render.#9930
0cbaf81Thanks @gustavovalverde! - Anonymous account linking now works after social and generic OAuth sign-in in Expo and other in-app browsers, where the OAuth callback returns without the session cookie.onLinkAccountfires and the anonymous user is migrated; before, it was silently skipped.Plugins can now carry server-trusted data across an OAuth redirect with the new
addOAuthServerContextAPI, read back on the callback viagetOAuthState().serverContext. UnlikeadditionalData, it cannot be set from the request body, so it is the right place for values the server must trust.For
@better-auth/oauth-provider, the post-login authorization query now travels through that server-only channel, so it can no longer be injected throughadditionalData.#10004
b36c38fThanks @bytaesu! - The captcha plugin now requires endpoint entries to match full auth paths unless they use wildcard patterns. This prevents requests like/sign-in//emailfrom bypassing captcha while preserving trailing-slash matches like/sign-in/email/. To protect multiple routes, replace partial paths like/sign-inwith explicit wildcards such as/sign-in/*or/sign-in/**.#10746
6782647Thanks @gustavovalverde! - OAuth device grants now useoauthDeviceAuthorization()alongsideoauthProvider()ormcp(). This single integration replaces both the standalonedeviceCodeGrant()plugin and the shared-grant configuration. Standalone Device Authorization no longer accepts or stores RFC 8707 resources, andonDeviceAuthRequestreceives onlyclientIdandscope. The OAuth integration rejects resource indicators that are not absolute, fragment-free URIs.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.#10402
763a267Thanks @gustavovalverde! - Plugin database schemas can now define named or generated table-level indexes across multiple fields. SQL migrations and generated Drizzle or Prisma schemas resolve configured table and column names consistently, while the MongoDB adapter creates the same indexes before the first index-enforcing write.#9766
bf39cbfThanks @GautamBytes! - Add a server-onlyauth.api.consumePhoneNumberOTPAPI for custom phone OTP flows that need to verify and consume a code without creating or updating users or sessions.#10330
081d3c3Thanks @ping-maxwell! - Allow the username plugin's separatedisplayUsernamefield to be omitted bysetting
displayUsername: falseon both the server and client plugins.#10059
49b5cf6Thanks @GautamBytes! - Device Authorization now creates unique database indexes fordeviceCodeanduserCode, so each generated code must be unique in its column. Existing installations on every adapter must resolve duplicate values before applying the migration. MySQL and SQL Server installations must also convert both columns to bounded strings and clean up values longer than 191 characters before running it.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.#9645
e014029Thanks @ping-maxwell! - Harden the Electron OAuth flow and tighten custom-scheme trusted-origin matching.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.#9948
3d04fabThanks @yordis! - feat(generic-oauth): addrefreshTokenParamsconfig to forward extra params on token refreshMulti-tenant OIDC providers (Zitadel multi-org, Auth0 with
audience) need to send extra body params on the refresh call to rescope tokens without a full authorization redirect. The generic-oauth plugin now accepts arefreshTokenParamsoption (object or sync/async function) that is merged into the refresh request body, withgrant_typeandrefresh_tokenprotected from override. The function form receives request metadata for the request that triggered the refresh, so request-scoped data (headers, cookies) is available without out-of-band state like AsyncLocalStorage.UpstreamProvider.refreshAccessTokennow accepts an optional secondctxargument; the change is backwards compatible because existing implementations that take onlyrefreshTokenremain valid. See #7554.#9069
c7d2253Thanks @gustavovalverde! - Rewrite the generic OAuth plugin as a first-class social provider with OAuth 2.1 security defaults. Providers now usesignIn.social+callback/:idinstead of dedicated plugin endpoints, with PKCE required by default (OAuth 2.1), RFC 9207 issuer validation, OIDC auto-discovery withopenidscope injection, and typed provider IDs.Breaking changes:
signIn.oauth2({ providerId })replaced bysignIn.social({ provider })oauth2.link()replaced bylinkSocial()/api/auth/oauth2/callback/:idto/api/auth/callback/:idgenericOAuthClient()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>#9966
ec8a38cThanks @gustavovalverde! - genericOAuth providers configured with adiscoveryUrlnow verify the provider'sid_tokenagainst its published JWKS (signature, issuer, audience, and advertised algorithms) and bind it to the authorization request with a server-generated OIDCnonce. A sign-in whoseid_tokenfails verification, or does not echo the expectednonce, is rejected.Set
disableIdTokenNonceBinding: trueon a provider that does not return thenonceclaim in the authorization-code flow.These providers also accept client-submitted id_token sign-in through
signIn.social({ idToken }), which previously returnedID_TOKEN_NOT_SUPPORTED.Providers configured with explicit endpoints instead of
discoveryUrlare unchanged.#9368
430c895Thanks @GautamBytes! - Generic OAuth users can now sign out from the configured OpenID provider when they callauthClient.signOut(). When a provider exposes a discovered or configured logout endpoint, Better Auth redirects to it and includes the storedid_token_hintwhen available. PasscallbackURLor configurepostLogoutRedirectURIfor the return flow, with optionalstate, or setdisableRedirectto handle the returnedurlyourself. When multiple linked providers support logout, Better Auth selects the most recently updated account. SetdisableProviderLogout: trueto keep sign-out local.#9431
523f95cThanks @pi0! - feat: makeAuthinstance fetchable#10577
5c45abcThanks @gustavovalverde! - MCP clients that hit a scope wall now learn exactly which scopes to ask for. Missing protected scopes produce a403with 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.requiredScopesthroughRequireMcpAuthOptionsor the matchingcreateMcpProtectedRequestHandlerverifier option. Exact membership remains the default;isScopeSatisfiedcan define hierarchical policies.createInsufficientScopeErrorwhen an operation determines its required scopes dynamically.createResourceServerChallengeconverts that signal and recognized token failures into safe RFC 6750 challenges.challengeScopesonly as the unauthenticated challenge hint.Handler-produced responses, ordinary permission denials, configuration failures, and unrelated thrown values keep their original status and identity.
#10403
dbd302eThanks @gustavovalverde! - Scope account identity by trusted issuer instead of provider configuration. Accounts now use the unique(issuer, accountId)key, so aliases for one OpenID Connect issuer deduplicate one external identity while equal subjects from different issuers remain separate. This identity deduplication does not introduce independent grant or provider lifecycle records for aliases.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.
#10359
8784c1cThanks @ping-maxwell! - Database joins have moved out ofexperimentalinto a stable option atadvanced.database.joins(default:false).If you previously set
experimental: { joins: true }, update your config to: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).#9992
e53582cThanks @gustavovalverde! - The MCP plugin moves out ofbetter-authinto its own package,@better-auth/mcp, built on@better-auth/oauth-provider. Import the authorization plugin and protected-request helpers from the package root. The in-core MCP client (createMcpAuthClientand its adapters) is removed; MCP protocol and transport clients come from the official version 2@modelcontextprotocol/clientand@modelcontextprotocol/serverpackages. The OAuth endpoints move from/mcp/*to/oauth2/*, with discovery at/.well-known/oauth-authorization-serverand protected resource metadata at/.well-known/oauth-protected-resource. Discovery-based MCP clients pick up the new locations on their own.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.#10204
0683a5fThanks @GautamBytes! - Microsoft sign-in now identifies Entra accounts with the stableoidclaim in both the built-inmicrosoftprovider and the Generic OAuthmicrosoftEntraIdhelper. Tokens without a validoidare rejected, and the Generic OAuth helper refuses to initialize unless Microsoft discovery provides ID-token verification metadata. Existing Microsoft account rows created fromsubmust be migrated before upgrading.#9305
e7eb45bThanks @gustavovalverde! - feat(oauth): per-requestadditionalParamsandloginHintparity acrosssignIn.social,linkSocial, andsignIn.ssoUnified escape hatch for customizing the provider authorization URL on a per-request basis. Previously, dynamic parameters like Google's
access_type=offline/prompt=consent, Cognito'sidentity_provider=Google, or Microsoft'sdomain_hintcould only be set as static server configuration.New capabilities
signIn.social,linkSocial, andsignIn.ssoacceptadditionalParams: Record<string, string>. Values are appended to the authorization URL as query parameters.linkSocialalso acceptsloginHint, matching the surface ofsignIn.socialandsignIn.sso.OAuthProvider.createAuthorizationURLgainsadditionalParamsin its input contract; every built-in provider forwards it to the shared helper.additionalParamswith the config-levelauthorizationUrlParams; call-time wins on key collision.identityProvider?: stringconfig option that maps to theidentity_providerquery parameter, avoiding magic strings.Security
createAuthorizationURLhelper silently drops any caller-supplied key inRESERVED_AUTHORIZATION_PARAMS(state,client_id,redirect_uri,response_type,code_challenge,code_challenge_method,nonce,scope). The request-body Zod schema rejects the same keys with 400, so misuse is visible at the edge rather than silently overriding security-critical parameters.nonceis reserved so a caller cannot replace the OIDC nonce Better Auth generates when binding a discovery provider'sid_tokento the authorization request.wechat→appid,tiktok→client_key) additionally filter those keys so a caller cannot swap the configured OAuth app.atlassian→audience,notion→owner) are merged last so caller-suppliedadditionalParamscannot override them. Configured defaults that represent operator intent (e.g. Googleinclude_granted_scopes, CognitoidentityProvider) remain caller-overridable.signIn.ssorejectsadditionalParamswith 400 when the resolved provider is SAML; the SAML AuthnRequest is signed and cannot carry caller-supplied query parameters, so silently dropping them would mislead integrators.OpenAPI
ZodRecordhandling to the OpenAPI generator soz.record()fields emittype: objectwith typedadditionalProperties. Incidentally fixes a long-standing bug whereadditionalDatawas rendered astype: string.Refactors
discord,roblox,zoom, andslackproviders now delegate to the sharedcreateAuthorizationURLhelper and inherit its RFC behavior and reserved-key guard.tiktokandwechatkeep their manual URL construction (non-standard OAuth2 parameter names and URL fragment requirements) but threadadditionalParamswith the same reserved-key filter.Closes #2351.
Closes #5441.
Closes #5592.
Closes #5604.
Supersedes #4992 and #5443.
#10127
7c7313cThanks @gustavovalverde! - OAuth sign-in, account linking, callback, and proxy flows now buildredirect_urifrom the current request base URL whenbaseURL.allowedHostsis configured. Built-in social providers and generic OAuth providers now use the resolved request host for redirects in multi-host deployments.Custom
OAuthProviderimplementations can omitcallbackPathwhen using the shared/callback/<provider-id>route. SetcallbackPathonly for custom callback routes.#10039
aedcb97Thanks @gustavovalverde! - feat(oauth-provider)!: DPoP-bound access tokens (RFC 9449)OAuth provider integrations can issue and verify DPoP sender-constrained tokens. Clients request them with
dpop_bound_access_tokensat registration,dpop_jkton the authorization request, or by targeting a resource configured withdpopBoundAccessTokensRequired. Issued tokens carrycnf.jkt, returntoken_type: "DPoP", and stay bound through refresh-token rotation, introspection, and userinfo.Resource servers verify DPoP requests with
verifyAccessTokenRequest, which checks theAuthorization: DPoPscheme, the proof, the request target, the access-token hash, and proof replay. The MCP package advertises DPoP in protected resource metadata and verifies DPoP-bound requests. Proof replay is rejected through the database-backed verification store, so anti-replay holds across instances.verifyAccessTokenRequestandrequireMcpAuthuse that store by default; build one withcreateDpopReplayStore(internalAdapter)or pass a customdpop.replayStore. This needs database-backed verification storage: a secondary-storage-only deployment rejects DPoP requests rather than skipping replay protection.Breaking: the raw-token verifier
verifyAccessTokenis renamed toverifyBearerToken, both inbetter-auth/oauth2and as theoauthProviderResourceClientaction, and it rejects DPoP-bound tokens. UseverifyAccessTokenRequeston any endpoint that may receive them. The resource-request input type is renamed fromAccessTokenRequestInputtoResourceRequestInput, and the DPoP algorithm option issigningAlgorithmseverywhere.Run a schema migration for the DPoP token-binding fields: the
confirmationcolumn on the access-token and refresh-token tables. DPoP-bound clients also gaindpopBoundAccessTokensand resourcesdpopBoundAccessTokensRequired. No dedicated replay table is added; proof replay reuses the verification store.#9828
4f53b61Thanks @gustavovalverde! - Verify social-provider id_tokens with a single shared verifier.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:For verification that cannot use a local JWKS, pass
idToken: { verify: async (token, nonce) => boolean }. TheverifyIdTokenanddisableIdTokenSignInprovider options are unchanged.#9079
6f2948eThanks @gustavovalverde! - feat(oauth-provider): computeat_hashin ID tokens per OIDC Core §3.1.3.6ID tokens issued alongside an access token now include the
at_hashclaim, which cryptographically binds the two tokens to prevent token substitution attacks. The hash algorithm is selected based on the actual signing key's algorithm (EdDSA/Ed25519 uses SHA-512, RS/ES/PS384 uses SHA-384, RS/ES/PS512 uses SHA-512, all others use SHA-256).A new
resolveSigningKey()export is available frombetter-auth/pluginsto resolve the current JWKS signing key (including its algorithm). When using a customjwt.signcallback, the signed ID token's header is validated against the declared algorithm to preventat_hashmismatches.#10135
f68044dThanks @brentmitchell25! - Registered OAuth clients can now use the RFC 8628 device flow to obtain OAuth access tokens. AddoauthDeviceAuthorization()alongsideoauthProvider()ormcp(), request a code at/device/code, and exchange it at/oauth2/tokenafter the user approves it. OAuth and OpenID discovery advertise thedevice_authorization_endpoint.Device authorization requests can bind RFC 8707 resource indicators.
GET /devicereturns the requested client, scopes, and resources to the authenticated user who owns the request. Token requests can reuse or narrow the approved resources, but cannot add new ones. Existing first-party device clients continue to receive Better Auth session tokens from/device/token.Enabling
oauthDeviceAuthorization()adds nullableoauthClientIdandresourcesfields todeviceCode. Regenerate and apply the database schema after adding the integration.Confidential clients authenticate at
/device/codewith their registered method, while public clients sendclient_id. Emptyclient_id,scope,user_id, and authentication values are treated as omitted; multiple non-empty values for any of these parameters returninvalid_request, while multipleresourcevalues remain supported. Unknown OAuth client IDs enter the standalone device flow only whenoauthDeviceAuthorization({ validateClient })accepts them.#9929
91f235fThanks @gustavovalverde! - AddrequireEmailVerificationto OAuth provider options, for built-in social providers and the Generic OAuth plugin. When a provider reports an unverified email, the user and account are still created or linked, but no session is issued: the OAuth callback redirects with?error=email_not_verified, and ID token and One Tap sign-in return403EMAIL_NOT_VERIFIED. Verification emails follow the existingemailVerification.sendOnSignUp/sendOnSignInsettings.It is opt-in per provider and does not inherit
emailAndPassword.requireEmailVerification, so existing social logins keep working. The gate checks the local user's verification state, so a user verified through another method keeps access. Only enable it for providers that report a trustworthyemail_verifiedsignal.#9648
d2a79baThanks @brentmitchell25! - OAuth provider now models protected resources explicitly. Configure them withresourcesor create them through theoauthResourceadmin API. Each resource can define token TTLs, allowed scopes, custom JWT claims, and JWT signing pins.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 auth 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".#10397
bb6c102Thanks @ping-maxwell! - Addorganization.getOrganization()to fetch organization metadata without members or invitations.#8931
34558bcThanks @GautamBytes! - Add opt-in JWKS-backed asymmetric JWT support forsession_datacookie cache tokens, so services can verify cookie-cache JWTs with public keys instead of shared secrets. Enable it withjwt({ sessionCookieCache: true })alongsidesession.cookieCache.strategy = "jwt".#8977
954b664Thanks @ruban-s! - allow passinguserIdandorganizationIdto thelistUserTeamsAPI.userIdlets callers list teams for another member of an organization (gated behind themember:updatepermission).organizationIdscopes the result to a specific organization without needing to switch the session's active organization, matching the pattern used byaddTeamMember/removeTeamMember.#9969
76a3342Thanks @gustavovalverde! - Signing out withsecondaryStorageandsession.preserveSessionInDatabasenow runs your configuredsession.deletehooks and marks the preserved session row as ended. OAuth Provider access and refresh tokens bound to that session are revoked and back-channel logout is dispatched on sign-out. Previously these hooks were skipped in this setup, so the tokens stayed valid until they expired.#9657
1e5b808Thanks @gustavovalverde! - Hardenprivate_key_jwtand token endpoint client authentication, and add the helpers that make the fix structural.@better-auth/core/oauth2now exposesencodeBasicCredentialsanddecodeBasicCredentials, a round-trip-tested pair that follows RFC 6749 §2.3.1 (application/x-www-form-urlencodedeach value, split on the first:only). The decoder accepts the scheme case-insensitively and tolerates one or more spaces before the credentials per RFC 7235 §2.1.client_secret_basicon the client side and the Better Auth OAuth provider on the server side both go through these helpers, so credentials containing reserved characters round-trip cleanly across the stack and headers likebasic xxxorBasic xxxare accepted.createPrivateKeyJwtClientAssertionGettervalidates options eagerly. Unsupported algorithms (HS256,none), a JWK with no key material, and disagreement between an explicitalgorithmand the JWK-embeddedalgall throw at construction rather than on the first token request.signPrivateKeyJwtClientAssertionenforces the same checks for direct callers. Breaking: configurations that paired an unsupported JWKalgwith a different explicitalgorithmused to silently sign with the explicit option; they now fail at construction.Breaking:
@better-auth/oauth-provideraccepts clientjwksmetadata only as an RFC 7517 JWK Set object with a non-emptykeysarray. Replacejwks: [key]withjwks: { keys: [key] }in DCR payloads, administrative and user client creation, Client ID Metadata Documents, test fixtures, and generated client code. Remotely fetchedjwks_uriresponses must use the same object shape. EC keys must use P-256, P-384, or P-521; OKP keys must useEd25519. When a key declaresalg, it must be a supportedprivate_key_jwtalgorithm that matches the key type and curve; omitalgwhen the client chooses the algorithm in its assertion header. OAuth client rows previously written throughoauthToSchemaare already stored as JWK Set objects, so this is a request, configuration, and type migration rather than another database rewrite; audit rows written outside Better Auth separately.The SSO
private_key_jwtflow redirects witherror_description=no_private_key_availablewhen aresolvePrivateKeycallback returns noprivateKeyJwkorprivateKeyPem. The redirect path previously short-circuited only when the resolver was absent entirely; an empty resolver return fell through into an internal signing error.better-auth/testaddsgetHttpTestInstance, a counterpart togetTestInstancethat binds a real HTTP listener on an OS-assigned port and constructs the auth instance against the discovered URL. It removes the temp-server-then-rebind race that test files have been individually copy-pasting.#8836
93d3871Thanks @gustavovalverde! - Add client authentication configuration for token endpoint requests across the stack, includingprivate_key_jwt(RFC 7523).Generic OAuth providers now accept
tokenEndpointAuthfor token endpoint client authentication. UsetokenEndpointAuth: { method: "private_key_jwt", getClientAssertion }for JWT client assertions,{ method: "none" }for public clients, and{ method: "client_secret_basic" }or{ method: "client_secret_post" }withclientSecretfor explicit secret-based client authentication. The existingauthentication: "basic" | "post"option remains available for secret-based token requests.Use
createPrivateKeyJwtClientAssertionGetter()to sign RFC 7523 assertions from a private key. The assertion getter receives{ clientId, tokenEndpoint, grantType }, so integrations do not duplicate client ID or token endpoint values inside assertion helpers. Core OAuth2 now exports private-key JWT-specific helpers and types:signPrivateKeyJwtClientAssertion,createPrivateKeyJwtClientAssertionGetter,PrivateKeyJwtSigningAlgorithm, andPRIVATE_KEY_JWT_SIGNING_ALGORITHMS.Token endpoint client authentication parameters are derived from
clientId,clientSecret, andtokenEndpointAuth. Configured token endpoint authentication requiresclientId; secret-based token endpoint authentication also requiresclientSecret. Custom token parameters are for provider-specific fields and do not replace the configured client authentication values.refreshAccessToken()now forwardsresourcevalues to refresh-token requests, so RFC 8707 resource indicators work through both the high-level refresh helper andrefreshAccessTokenRequest().The synchronous OAuth2 request builders
createAuthorizationCodeRequest,createRefreshAccessTokenRequest, andcreateClientCredentialsTokenRequesthave been removed. Use the asyncauthorizationCodeRequest,refreshAccessTokenRequest, andclientCredentialsTokenRequesthelpers instead.Servers verify JWT client assertions signed with asymmetric keys, and clients can use the same token endpoint authentication contract for authorization code, refresh, and client credentials token requests.
#9134
652fa53Thanks @gustavovalverde! - The dynamicbaseURLconfig now ignoresx-forwarded-hostandx-forwarded-protounless you setadvanced.trustedProxyHeaders: true.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:
#9240
729c00dThanks @adrianmxb! - feat(username): add immutable username optionThis allows users to set their username during sign-up or first update, but prevents changing it to a different value afterwards. Users can still update other profile fields.
#10031
6fe9faaThanks @gustavovalverde! - Remove the deprecatedoidcProviderplugin frombetter-auth/plugins. Migrate OIDC authorization-server integrations to@better-auth/oauth-provider.#10473
ed61b47Thanks @gustavovalverde! - Add transactional OIDC user resolution so applications can link verified issuer and subject pairs to exact existing users while preserving or updating the local profile.#10234
973fddeThanks @gustavovalverde! - The SIWE plugin now issues nonces before the wallet address or Chain ID is known.authClient.siwe.nonce()andauthClient.siwe.getNonce()no longer accept wallet fields,getNoncemust return an ERC-4361 nonce (8-250 alphanumeric characters), and SIWE verification now reads the wallet address and Chain ID from the signed ERC-4361 message.#9864
41cca60Thanks @GautamBytes! - Add auser.validateUserInfoprovisioning gate that lets applications reject an identity before a user is created or a new account is linked. It runs once at the creation step for every method that provisions a user (OAuth, SSO/SAML, email/password, magic link, email OTP, anonymous, SIWE, phone number, admin-created users, and SCIM), including stateless setups with no persistent database.It also re-runs when an existing OAuth or SSO user signs in again (
source.actionis"sign-in"), where it receives the fresh provider email and profile so a domain or org policy can reject a user whose provider identity moved out of bounds. Non-provider returning sign-ins are not re-validated.The callback receives the mapped
userplus asourcedescribing theaction(create-user,link-account, orsign-in), themethod, and provider metadata:source.oauthfor OAuth providers andsource.ssofor OIDC/SAML SSO providers. Return{ error, errorDescription }to reject: browser flows redirect to the error URL and programmatic flows return a403.#10036
ad35eadThanks @bytaesu! - Require Google One Tap server callbacks to resolve a Google client ID before verifying ID tokens. ConfigureoneTap({ clientId })orsocialProviders.google.clientIdwhen using the One Tap plugin.#9057
544f1c6Thanks @gustavovalverde! - feat(two-factor)!: add OTP-only enablement and a discriminated responseenableTwoFactornow accepts amethodparameter ("otp" | "totp", default"totp") and returns a discriminated response with amethodfield.method: "otp"twoFactorEnabled: trueimmediately.{ method: "otp" }.otpOptions.sendOTPto be configured on the server; rejects withOTP_NOT_CONFIGUREDotherwise.method: "totp"(default){ method: "totp", totpURI, backupCodes }.TOTP_NOT_CONFIGUREDiftotpOptions.disableis set.The existing
skipVerificationOnEnableoption remains supported for TOTP enrollment.Breaking changes
enableTwoFactorincludes amethodfield in the response ("otp"or"totp").Patch Changes
#10014
73541c1Thanks @gustavovalverde! - Cloudflare Workers apps can now start when importing Better Auth subpaths such asbetter-auth/db. Beta builds were crashing during module initialization before application code ran.#10299
cf8eaacThanks @momomuchu! - widen drizzle-kit peer dependency range#10501
65fc17cThanks @KingIronMan2011! - Expand the optionaldrizzle-ormpeer range to^0.45.2 || >=1.0.0-rc.1 <2.0.0, matching@better-auth/drizzle-adapterand allowing Drizzle ORM v1 RC installations without peer dependency warnings.#10622
ecd83daThanks @gustavovalverde! - Sign-up no longer deadlocks when session cookie caching uses the JWT strategy on a single-connection SQLite database with native transactions enabled. JWKS key lookups and creation now resolve the transaction-scoped adapter instead of always querying the root connection, so minting a signing key during sign-up joins the surrounding transaction instead of racing it for the only available connection. On multi-connection databases (Postgres, MySQL) this also fixes a silent atomicity gap where a JWKS key created mid-transaction could commit independently of the transaction it was minted in.#10293
fe4c820Thanks @gustavovalverde! -npx auth migratecan now add required columns with static defaults and nullableunique columns to existing SQLite, PostgreSQL, and MySQL tables. Required unique
columns still need distinct values to be backfilled manually before applying the
unique constraint.
#9898
7fe0e2bThanks @ItalyPaleAle! - AddclientAssertionsupport to the Microsoft Entra ID social provider.#9301
03e6c94Thanks @gustavovalverde! - AddallowIdpInitiatedtoGenericOAuthConfigand SSOOIDCConfigto support providers that initiate OAuth without astateparameter (e.g. Clever). When enabled, stateless callbacks restart the OAuth flow server-side with fresh state and PKCE, preserving CSRF protection. Also hardensparseStateagainst undefined request bodies on GET callbacks.#10065
2196ea6Thanks @gustavovalverde! - OAuth and device-authorization responses that carry credentials now consistently sendCache-Control: no-storeandPragma: no-cache, so proxies, CDNs, and browsers never cache them. This covers the token, introspection, and userinfo endpoints, dynamic and admin client registration, client secret rotation, and the device code and device token responses, including the error responses from those endpoints.Endpoints declare this with
metadata: { noStore: true }, and the header set is exported from@better-auth/coreasNO_STORE_HEADERSfor responses built by hand.#10124
06daf70Thanks @gustavovalverde! - Preserve the resolved OAuth user whenoverrideUserInforeturnsnullduring account linking.#9304
e0d2b9eThanks @gustavovalverde! - Propagate sign-out to every connected app and cut off API access immediately, via OIDC Back-Channel Logout 1.0.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 | nullbetter-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.#10125
a83152eThanks @gustavovalverde! - Create new OAuth accounts in the user creation transaction. Adapters with nativetransaction support roll back the user when the account write fails, while other
adapters still perform the writes sequentially.
#10128
97903c9Thanks @gustavovalverde! - Preserve previously granted OAuth scopes across sign-in re-authentication and refresh-token requests.account.scopenow accumulates monotonically: newly granted scopes are merged in only when added vialinkSocial, and providers returning a narrower scope claim than the user has granted no longer shrink the stored value.#10170
6ddb555Thanks @gustavovalverde! - 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.#10390
0de88f5Thanks @gustavovalverde! - SCIM connections can now provision Users, Groups, and direct memberships into application-defined provisioning domains without the organization or SSO plugins. Applications can map Group membership to validated custom roles through projections. The service also supports SCIM 2.0 discovery, filtering, pagination, response attribute selection, atomic PATCH operations, and common request patterns used by Microsoft Entra ID and Okta.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.
#10505
d701f90Thanks @gustavovalverde! - One Tap, Electron, and Expo client plugins now compose withcreateAuthClientwithout TypeScript errors, and the resulting client preserves each plugin's inferred actions.#10621
59c4c83Thanks @gustavovalverde! - Allow test instances to enable native database transactions for postgres and mysql.Updated dependencies [
5c45abc,763a267,5d38b13,692b22c,ea06c5a,3d04fab,430c895,de8394d,dbd302e,8784c1c,ecd83da,e4818b5,7fe0e2b,0683a5f,e7eb45b,7c7313c,aedcb97,03e6c94,4f53b61,2196ea6,91f235f,34558bc,1e5b808,93d3871,97903c9,ed61b47,0de88f5,3a79aff,41cca60,d701f90]:v1.6.30Compare Source
Patch Changes
07c1718]:Configuration
📅 Schedule: (UTC)
🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.
♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 Ignore: Close this PR and you won't be reminded about this update again.
This PR has been generated by Mend Renovate.
3ff9eb509247f3b1ead1chore(deps): update dependency better-auth to v1.6.30to chore(deps): update dependency better-auth to v1.7.0View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.Merge
Merge the changes and update on Forgejo.Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.