Skip to content

v1.7.0-rc.0

Pre-release
Pre-release

Choose a tag to compare

@better-release better-release released this 26 Jun 22:28
Immutable release. Only release title and notes can be modified.
b7239f3

better-auth

❗ Breaking Changes

  • feat(captcha)!: support wildcard endpoint matching (#10004)

  • feat(mcp)!: ship MCP as its own package built on the OAuth provider (#9992)

    The route helper is renamed requireMcpAuth (was withMcpAuth), and the remote client is createMcpResourceClient (was createMcpAuthClient). requireMcpAuth verifies the bearer token against the published JWKS and passes the verified JWT claims to your handler.

    To migrate, install @better-auth/mcp, add the jwt() plugin (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. Registering a backchannel_logout_uri rejects fragments, non-http(s) schemes, and non-HTTPS targets on confidential clients. Its SSRF host guard, which blocks private, reserved, tunneled, and cloud-metadata hosts, now also covers a private_key_jwt client's jwks_uri.

    Schema changes on @better-auth/oauth-provider:

    • oauthClient.backchannelLogoutUri: string | null
    • oauthClient.backchannelLogoutSessionRequired: boolean
    • oauthAccessToken.revoked: Date | null

    better-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(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"

    • Sets twoFactorEnabled: true immediately.
    • Returns { method: "otp" }.
    • Requires otpOptions.sendOTP to be configured on the server; rejects with OTP_NOT_CONFIGURED otherwise.

    method: "totp" (default)

    • Returns { method: "totp", totpURI, backupCodes }.
    • Rejects with TOTP_NOT_CONFIGURED if totpOptions.disable is set.

    Breaking changes

    • Removed skipVerificationOnEnable: use method: "otp" for immediate activation, or the standard TOTP verification flow.
    • 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(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(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()
    • Callback URL changed from /api/auth/oauth2/callback/:id to /api/auth/callback/:id
    • genericOAuthClient() removed; generic OAuth providers now use the standard social client APIs
    • pkce defaults to true (was false); set pkce: false for providers that reject PKCE
    • authorizationUrlParams and tokenUrlParams only accept Record<string, string>
    • issuer and requireIssuerValidation config fields removed; issuer validation is automatic via OIDC discovery
    • mapProfileToUser profile typed as OAuth2UserInfo & Record<string, unknown>
  • 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.

Features

  • feat: add clientAssertion support to the Microsoft Entra ID social provider (#9898)
  • feat: make Auth instance fetchable (#9431)
  • feat(auth): add per-provider requireEmailVerification for social sign-in (#9929)
  • feat(auth): add user.validateUserInfo provisioning gate (#9864)
  • feat(client): add hydrateSession for SSR session hydration (#8733)
  • feat(generic-oauth,sso): support IDP-initiated flows via secure bounce (#9301)
  • feat(generic-oauth): forward refreshTokenParams to token endpoint (#9948)
  • feat(generic-oauth): verify discovery id_tokens and enable id_token sign-in (#9966)
  • feat(oauth-provider): add DPoP support (#10039)
  • feat(oauth-provider): compute at_hash in id tokens per OIDC Core §3.1.3.6 (#9079)
  • feat(oauth): add private_key_jwt client authentication (RFC 7523) (#8836)
  • feat(oauth): enforce no-store on credential responses via a declarative flag (#10065)
  • feat(oauth): per-request additionalParams and loginHint (#9305)
  • feat(oauth): server-trusted state channel; fix anonymous cookieless linking (#9930)
  • feat(org): allow passing userId and organizationId to listUserTeams API (#8977)
  • feat(phone-number): add server-side OTP consumption API (#9766)
  • feat(session): support JWKS-backed JWT session cookie cache (#8931)
  • feat(username): add immutable username option (#9240)

Bug Fixes

  • 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.
  • fix(generic-oauth): bind id token nonce in redirect flow (#10095)
  • fix(oauth): create new oauth account in transaction (#10125)
  • fix(oauth): derive redirect URI from per-request baseURL (#10127)
  • fix(oauth): preserve account.scope across re-auth and refresh (#10128)
  • fix(oauth): preserve user on null profile override (#10124)
  • fix(session): fire session-delete hooks for preserved sessions on secondaryStorage (#9969)
  • refactor(oauth): single-source Basic credentials + getHttpTestInstance (#9657)

For detailed changes, see CHANGELOG

@better-auth/oauth-provider

❗ Breaking Changes

  • feat(mcp)!: ship MCP as its own package built on the OAuth provider (#9992)

    The route helper is renamed requireMcpAuth (was withMcpAuth), and the remote client is createMcpResourceClient (was createMcpAuthClient). requireMcpAuth verifies the bearer token against the published JWKS and passes the verified JWT claims to your handler.

    To migrate, install @better-auth/mcp, add the jwt() plugin (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. Registering a backchannel_logout_uri rejects fragments, non-http(s) schemes, and non-HTTPS targets on confidential clients. Its SSRF host guard, which blocks private, reserved, tunneled, and cloud-metadata hosts, now also covers a private_key_jwt client's jwks_uri.

    Schema changes on @better-auth/oauth-provider:

    • oauthClient.backchannelLogoutUri: string | null
    • oauthClient.backchannelLogoutSessionRequired: boolean
    • oauthAccessToken.revoked: Date | null

    better-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)!: enforce max_age (#9936)

  • feat(oauth-provider)!: make id-token claim authority explicit (#10140)

    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)

    An internal createOAuthEndpoint wrapper now translates zod validation failures into the envelope required by RFC 6749 §5.2, 7009 §2.2.1, 7662 §2.3, and 7591 §3.2.2. Failing issues are routed per field:

    • an absent required value maps to errorCodesByField[name].missing or the endpoint's defaultError.
    • an unsupported value (unknown enum member) maps to errorCodesByField[name].invalid or defaultError.
    • any other failure (wrong type, duplicated query params, invalid format, refinement) maps to defaultError, so RFC 6749 §3.1 malformed requests emit the endpoint's default code regardless of field.

    All six OAuth endpoints (/oauth2/token, /oauth2/authorize, /oauth2/revoke, /oauth2/introspect, /oauth2/register, /oauth2/end-session) now return RFC-compliant errors for malformed requests. /oauth2/authorize validation failures redirect to the relying party with error, error_description, echoed state, and iss whenever client_id and redirect_uri resolve against the registered client; requests without a trusted RP fall back to the server error page.

    Additional RFC compliance fixes on the same endpoints:

    • /oauth2/revoke and /oauth2/introspect now ignore an unknown token_type_hint instead of rejecting it. RFC 7009 §2.2.1 and RFC 7662 §2.1 reserve unsupported_token_type for the token itself, not the hint value; servers MAY ignore unrecognized hints and search across supported token types.
    • /oauth2/authorize error redirects now respect OIDC Core 1.0 §5 response modes. Errors for response_type=token or id_token are delivered in the URL fragment per RFC 6749 §4.2.2.1; an explicit response_mode=query overrides the default.
  • 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()
    • Callback URL changed from /api/auth/oauth2/callback/:id to /api/auth/callback/:id
    • genericOAuthClient() removed; generic OAuth providers now use the standard social client APIs
    • pkce defaults to true (was false); set pkce: false for providers that reject PKCE
    • authorizationUrlParams and tokenUrlParams only accept Record<string, string>
    • issuer and requireIssuerValidation config fields removed; issuer validation is automatic via OIDC discovery
    • mapProfileToUser profile typed as OAuth2UserInfo & Record<string, unknown>

Features

  • feat: add token endpoint client authentication (#9625)
  • feat(cimd): add Client ID Metadata Document plugin (#9159)
  • feat(oauth-provider): add DPoP support (#10039)
  • feat(oauth-provider): add extension surface (#10030)
  • feat(oauth-provider): add refresh token reuse interval (#10145)
  • feat(oauth-provider): allow confidential DCR clients without PKCE (#10146)
  • feat(oauth-provider): compute at_hash in id tokens per OIDC Core §3.1.3.6 (#9079)
  • feat(oauth-provider): consistent and audience-scoped token introspection (#10045)
  • feat(oauth-provider): expose sessionId to id_token claim contributors (#10113)
  • feat(oauth-provider): honor requested UserInfo claims via a claim registry (#10156)
  • feat(oauth-provider): support protected dynamic client registration (#10037)
  • feat(oauth): add private_key_jwt client authentication (RFC 7523) (#8836)
  • feat(oauth): enforce no-store on credential responses via a declarative flag (#10065)
  • feat(oauth): server-trusted state channel; fix anonymous cookieless linking (#9930)

Bug Fixes

  • fix(oauth-provider): accept UserInfo form-body tokens (#10155)
  • fix(oauth-provider): allow nonce-bound offline access without PKCE (#10153)
  • fix(oauth-provider): challenge invalid userinfo tokens (#10068)
  • fix(oauth-provider): handle OIDC authorization request inputs (#10151)
  • fix(oauth-provider): keep OIDC scope claims on UserInfo (#10152)
  • fix(oauth-provider): make private_key_jwt jti single-use atomic across processes (#9964)
  • fix(oauth-provider): make redirect_uri conditional at the token endpoint (#10159)
  • fix(oauth-provider): preserve dcr client key metadata (#10144)
  • fix(oauth-provider): redirect missing response_type errors (#10149)
  • fix(oauth-provider): reject authorization code replay correctly (#10150)
  • fix(oauth-provider): report unsupported_token_type for JWT access-token revocation (#9970)
  • fix(oauth-provider): return invalid_grant for cross-client refresh tokens (#10154)
  • refactor(oauth): single-source Basic credentials + getHttpTestInstance (#9657)

For detailed changes, see CHANGELOG

@better-auth/sso

❗ Breaking Changes

  • 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)

    Breaking Changes

    • 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.

    Bug Fixes

    • InResponseTo validation was completely non-functional — The code read 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.
    • Audience Restriction was never validated — SAML assertions issued for a different service provider were accepted without checking the <AudienceRestriction> element. Audience is now validated against the configured samlConfig.audience value per SAML 2.0 Core §2.5.1.
    • SessionIndex stored as object instead of string — samlify returns 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.

    Improvements

    • 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 removed from samlConfig.
    The ACS URL is now always derived from your baseURL and providerId. Remove callbackUrl from your SAML provider configuration. The post-login redirect destination is set per sign-in via callbackURL in signIn.sso():

    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.

    Bug fixes

    • Fix SLO SessionIndex matching: LogoutRequests with a SessionIndex were silently failing to delete the correct session.
    • Audience validation now defaults to the SP entity ID when audience is not configured, per SAML Core section 2.5.1.
    • Restore AllowCreate in AuthnRequests, required by IdPs that use JIT provisioning.
    • SP metadata endpoint now reflects actual SP capabilities (encryption, signing, SLO).

Features

  • feat(auth): add user.validateUserInfo provisioning gate (#9864)
  • feat(generic-oauth,sso): support IDP-initiated flows via secure bounce (#9301)
  • feat(oauth): add private_key_jwt client authentication (RFC 7523) (#8836)
  • feat(oauth): per-request additionalParams and loginHint (#9305)
  • feat(oauth): server-trusted state channel; fix anonymous cookieless linking (#9930)
  • feat(sso): support additionalFields on ssoProvider (#9445)

Bug Fixes

  • fix(sso): reject OIDC endpoint redirects portably (#10072)
  • fix(sso): update samlify to 2.13.1 for signed-assertion XML injection (#9821)
  • fix(sso): upgrade samlify to 2.12.0 with XPath injection and XXE fixes (#9121)
  • refactor(oauth): single-source Basic credentials + getHttpTestInstance (#9657)

For detailed changes, see CHANGELOG

@better-auth/mcp

❗ Breaking Changes

  • feat(mcp)!: ship MCP as its own package built on the OAuth provider (#9992)

    The route helper is renamed requireMcpAuth (was withMcpAuth), and the remote client is createMcpResourceClient (was createMcpAuthClient). requireMcpAuth verifies the bearer token against the published JWKS and passes the verified JWT claims to your handler.

    To migrate, install @better-auth/mcp, add the jwt() plugin (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)!: 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".

Features

  • feat(oauth-provider): add DPoP support (#10039)
  • feat(oauth-provider): add refresh token reuse interval (#10145)

For detailed changes, see CHANGELOG

@better-auth/scim

❗ Breaking Changes

  • feat(scim)!: isolate provider connections by organization (#10249)

    SCIM-managed accounts now use namespaced provider IDs (scim:{organizationId}:{providerId} or scim:{providerId} for app-level static providers). Migrate only known SCIM-managed account rows before upgrading; leave non-SCIM accounts unchanged even when they share the same provider ID.

    Organization-scoped active: false now makes a user inactive in that organization while keeping SCIM group and team associations available for reactivation. Use DELETE to fully deprovision organization-scoped SCIM state.

    defaultSCIM has been replaced by staticProviders. linkExistingUsers.trustedDomains has been removed; use requireExistingOrgMembership, shouldLinkUser, or explicit true instead.

  • fix(scim)!: always bind personal SCIM connections to their creator (#9840)

    generateSCIMToken now records the creator's userId on every personal connection. The generate-token, list-provider-connections, get-provider-connection, and delete-provider-connection endpoints grant access only to that owner. Organization-scoped connections keep their existing behavior and continue to use organization membership and the configured requiredRole checks.

    This release is breaking. It removes the providerOwnership option, and owner binding can no longer be disabled. The scimProvider.userId column is now a permanent part of the schema, so run a migration after upgrading with npx auth migrate or npx auth generate.

    Connections created before this release carry no owner. Access now fails closed, so those connections are no longer reachable through the management endpoints, including token regeneration. Reclaim them at the database level: delete scimProvider rows that have neither organizationId nor userId, or set userId to the intended owner, then regenerate tokens as needed. Organization-scoped connections are not affected.

Features

  • feat(auth): add user.validateUserInfo provisioning gate (#9864)
  • feat(scim): add durable group resources (#10018)

For detailed changes, see CHANGELOG

@better-auth/electron

❗ Breaking Changes

  • fix(electron)!: enforce S256 PKCE and harden origin checks (#9645)

    The Electron sign-in flow now mandates PKCE S256. Plain PKCE is rejected: the code_challenge_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()
    • Callback URL changed from /api/auth/oauth2/callback/:id to /api/auth/callback/:id
    • genericOAuthClient() removed; generic OAuth providers now use the standard social client APIs
    • pkce defaults to true (was false); set pkce: false for providers that reject PKCE
    • authorizationUrlParams and tokenUrlParams only accept Record<string, string>
    • issuer and requireIssuerValidation config fields removed; issuer validation is automatic via OIDC discovery
    • mapProfileToUser profile typed as OAuth2UserInfo & Record<string, unknown>

For detailed changes, see CHANGELOG

@better-auth/stripe

❗ Breaking Changes

  • fix(stripe)!: make onSubscriptionCancel.event required (#9531)
  • fix(stripe)!: remove optional marker from onSubscriptionCancel event (#9359)

For detailed changes, see CHANGELOG

@better-auth/core

❗ Breaking Changes

  • 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.

Features

  • feat: add clientAssertion support to the Microsoft Entra ID social provider (#9898)
  • feat(auth): add per-provider requireEmailVerification for social sign-in (#9929)
  • feat(auth): add user.validateUserInfo provisioning gate (#9864)
  • feat(generic-oauth,sso): support IDP-initiated flows via secure bounce (#9301)
  • feat(generic-oauth): forward refreshTokenParams to token endpoint (#9948)
  • feat(google): add includeGrantedScopes option (#10129)
  • feat(oauth-provider): add DPoP support (#10039)
  • feat(oauth): add private_key_jwt client authentication (RFC 7523) (#8836)
  • feat(oauth): enforce no-store on credential responses via a declarative flag (#10065)
  • feat(oauth): per-request additionalParams and loginHint (#9305)

Bug Fixes

  • fix(cimd): route client_id SSRF checks through the shared host classifier (#10126)
  • fix(oauth): derive redirect URI from per-request baseURL (#10127)
  • fix(oauth): preserve account.scope across re-auth and refresh (#10128)
  • refactor(oauth): single-source Basic credentials + getHttpTestInstance (#9657)

For detailed changes, see CHANGELOG

auth

❗ Breaking Changes

  • feat(oauth)!: accumulate granted scopes as grantedScopes string[] (#9825)

Features

  • feat(cli): add create-admin command (#9547)

Bug Fixes

  • refactor(cli): leverage c12 v4 resolveModule for auth config loading (#9477)
  • revert(oauth): remove granted scopes architecture (#10123)

For detailed changes, see CHANGELOG

@better-auth/api-key

❗ Breaking Changes

  • feat(auth)!: harden atomic state transitions (#10000)

Bug Fixes

  • chore: sync main to next (#9533)

For detailed changes, see CHANGELOG

@better-auth/expo

❗ Breaking Changes

  • 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()
    • Callback URL changed from /api/auth/oauth2/callback/:id to /api/auth/callback/:id
    • genericOAuthClient() removed; generic OAuth providers now use the standard social client APIs
    • pkce defaults to true (was false); set pkce: false for providers that reject PKCE
    • authorizationUrlParams and tokenUrlParams only accept Record<string, string>
    • issuer and requireIssuerValidation config fields removed; issuer validation is automatic via OIDC discovery
    • mapProfileToUser profile typed as OAuth2UserInfo & Record<string, unknown>

For detailed changes, see CHANGELOG

@better-auth/cimd

Features

  • feat(cimd): add Client ID Metadata Document plugin (#9159)

For detailed changes, see CHANGELOG

@better-auth/drizzle-adapter

Features

  • feat(drizzle-adapter): support Drizzle Relations v2 (#9489)

For detailed changes, see CHANGELOG

@better-auth/i18n

Features

  • feat(i18n): add built-in translations for 22 languages (#9157)

For detailed changes, see CHANGELOG

Contributors

Thanks to everyone who contributed to this release:

@adrianmxb, @app/better-release, @brentmitchell25, @bytaesu, @GautamBytes, @gustavovalverde, @ItalyPaleAle, @OscarCornish, @pi0, @ping-maxwell, @ruban-s, @sovetski, @yordis

Full changelog: v1.6.22...v1.7.0-rc.0