new·Earn with mozg — 20% of every monthSend somebody here and take a fifth of every plan payment they make, for as long as they keep paying — not a bounty on the first invoice. Your handle is the link, the window is thirty days, and the commission lands on your balance the second they pay. Free to join: if you have signed in, you already have the link. mozg.sh/earnall news →
mozg.beta
Sign in

Better Auth · Authentication · all subjects

social sign-auth: oauth setup patterns

33 notes, read out of this brain and free to use. Each one was extracted from a source and is re-checked against its exam.

Create Better Auth client instance

import { createAuthClient } from "better-auth/react" export const authClient = createAuthClient()

Better Auth route handler setup

import { auth } from "@/lib/auth"; import { toNextJsHandler } from "better-auth/next-js"; export const { POST, GET } = toNextJsHandler(auth)

Cache-Control headers for OAuth credential responses

OAuth and device-authorization responses that carry credentials send Cache-Control: no-store and Pragma: no-cache headers to prevent caching by proxies, CDNs, and browsers. This covers token, introspection, userinfo endpoints, dynamic and admin client registration, client secret rotation, and device code/token responses. Endpoints declare this with metadata: { noStore: true }, and the header set is exported as NO_STORE_HEADERS from @better-auth/core.

OAuth account creation in user transaction

New OAuth accounts are now created within the user creation transaction. Adapters with native transaction support roll back the user when the account write fails, while other adapters still perform writes sequentially.

Monotonic OAuth scope accumulation

OAuth account scopes now accumulate monotonically. The account.scope field is preserved across sign-in re-authentication and refresh-token requests. Newly granted scopes are merged in only when added via linkSocial, and providers returning a narrower scope claim than the user has granted no longer shrink the stored value.

Preserve OAuth user on null overrideUserInfo during account linking

The resolved OAuth user is now preserved when overrideUserInfo returns null during account linking, instead of being discarded.

Account identity by issuer and providerAccountId

Accounts now use the unique (issuer, providerAccountId) key instead of the previous (provider, accountId) identity. This means aliases for one OpenID Connect issuer deduplicate one external identity while equal subjects from different issuers remain separate. Account.accountId is renamed to Account.providerAccountId, and Account.issuer is required. Account-specific APIs select the local Account.id through accountId; 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. getUserInfo().user no longer carries provider identity, and mapProfileToUser cannot return id.

OAuth device grants via RFC 8628

Registered OAuth clients can now use the RFC 8628 device flow to obtain provider-managed OAuth tokens. Add deviceCodeGrant() alongside deviceAuthorization() and oauthProvider(); clients request a code at /device/code and exchange it at /oauth2/token after user approval. OAuth and OpenID discovery now advertise device_authorization_endpoint. Device authorization requests can bind RFC 8707 resource indicators. GET /device returns requesting client_id, scope, and resource values to authenticated user, and onDeviceAuthRequest receives resource as third argument. Token requests can reuse or narrow approved resource set, but requests adding resource are rejected. The deviceCode table adds optional resource field. Before upgrading from earlier 1.7 prerelease, let pending OAuth device codes expire or delete them.

Generic OAuth provider logout support

Generic OAuth users can now sign out from the configured OpenID provider when they call authClient.signOut(). When a provider exposes a discovered or configured logout endpoint, Better Auth redirects to it and includes the stored id_token_hint when available. Pass callbackURL or configure postLogoutRedirectURI for the return flow with optional state, or set disableRedirect to handle the returned url yourself. When multiple linked providers support logout, Better Auth selects the most recently updated account. Set disableProviderLogout: true to keep sign-out local.

MCP scope wall with RFC 6750 insufficient_scope challenge

MCP clients that hit a scope wall now learn exactly which scopes to ask for. Missing protected scopes produce a 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. Configure protected scopes with requiredScopes through RequireMcpAuthOptions or matching createMcpProtectedRequestHandler verifier option. Exact membership remains default; isScopeSatisfied can define hierarchical policies. Use createInsufficientScopeError when an operation determines its required scopes dynamically. Use createResourceServerChallenge to convert that signal and recognized token failures into safe RFC 6750 challenges. Use challengeScopes only as the unauthenticated challenge hint.

User provisioning validation gate

Add user.validateUserInfo provisioning gate that lets applications reject an identity before a user is created or a new account is linked. It runs once at 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 re-runs when existing OAuth or SSO user signs in again (source.action is 'sign-in'), receiving fresh provider email and profile so domain or org policy can reject users whose provider identity moved out of bounds. Non-provider returning sign-ins are not re-validated. Callback receives mapped user plus source describing action ('create-user', 'link-account', or 'sign-in'), method, and provider metadata: source.oauth for OAuth providers and source.sso for OIDC/SAML SSO providers. Return { error, errorDescription } to reject: browser flows redirect to error URL and programmatic flows return 403.

Generic OAuth id_token verification with JWKS

genericOAuth providers configured with a discoveryUrl now verify the provider's id_token against its published JWKS (signature, issuer, audience, and advertised algorithms). A sign-in whose id_token fails verification is rejected. These providers also accept client-submitted id_token sign-in through signIn.social({ idToken }), which previously returned ID_TOKEN_NOT_SUPPORTED. Providers configured with explicit endpoints instead of discoveryUrl are unchanged.

Unified social provider id_token verifier

Client-submitted id_token sign-in (signIn.social({ idToken }) and account linking) is verified by one function instead of per-provider verifyIdToken method. Each provider declares an idToken config with a JWKS source, issuer, and audience, and core verifier runs signature, issuer, audience, and nonce checks. A provider declaring no config rejects the client id_token path. PayPal previously accepted any decodable id_token without signature verification; PayPal now declares no idToken config and client id_token path returns ID_TOKEN_NOT_SUPPORTED. PayPal sign-in through redirect flow is unchanged. Custom providers implementing OAuthProvider directly replace removed verifyIdToken method with idToken config: { jwks: createRemoteJWKSet(...), issuer: 'https://...', audience: clientId }. For verification that cannot use local JWKS, pass idToken: { verify: async (token, nonce) => boolean }.

OAuth protected resources with custom JWT claims

OAuth provider now models protected resources explicitly. Configure them with resources or create them through oauthResource admin API. Each resource can define token TTLs, allowed scopes, custom JWT claims, and JWT signing pins. validAudiences is removed. Move each existing resource identifier into resources; link clients limited to specific resources through oauthClientResource or Dynamic Client Registration resources. Access-token issuance applies resource policy to requested RFC 8707 resource values. Provider narrows scopes to resource allowlists, uses shortest configured TTL, strips reserved RFC 9068 claim names from custom claims, emits jti, and keeps repeated resource form parameters. Refresh-token TTLs use shortest applicable lifetime. JWT signing honors per-resource pins: signJWT() accepts signingKeyId and signingAlgorithm; JWKS adapters expose getKeyById() and getLatestKeyByAlg(). jwks table adds nullable alg and crv columns; keyPairConfigs can provision multiple algorithms. After upgrading, run npx @better-auth/cli generate and apply migration before deploying. Migration adds oauthResource, oauthClientResource, and new jwks columns.

DPoP-bound access tokens RFC 9449

OAuth provider integrations can issue and verify DPoP sender-constrained tokens per RFC 9449. Clients request them with dpop_bound_access_tokens at registration, dpop_jkt on authorization request, or by targeting resource configured with dpopBoundAccessTokensRequired. Issued tokens carry cnf.jkt, return token_type: 'DPoP', and stay bound through refresh-token rotation, introspection, and userinfo. Resource servers verify DPoP requests with verifyAccessTokenRequest, which checks Authorization: DPoP scheme, proof, request target, access-token hash, and proof replay. MCP package advertises DPoP in protected resource metadata and verifies DPoP-bound requests. Proof replay rejected through database-backed verification store; anti-replay holds across instances. verifyAccessTokenRequest and requireMcpAuth use that store by default; build one with createDpopReplayStore(internalAdapter) or pass custom dpop.replayStore. Breaking: raw-token verifier verifyAccessToken renamed to verifyBearerToken (both better-auth/oauth2 and as oauthProviderResourceClient action), and rejects DPoP-bound tokens. Resource-request input type renamed from AccessTokenRequestInput to ResourceRequestInput. DPoP algorithm option is signingAlgorithms everywhere. Run schema migration for DPoP token-binding: confirmation column on access-token and refresh-token tables. DPoP-bound clients gain dpopBoundAccessTokens; resources gain dpopBoundAccessTokensRequired.

Electron OAuth flow PKCE S256 requirement

Electron sign-in flow now mandates PKCE S256. Plain PKCE is rejected: code_challenge_method parameter is gone and every authorization code is verified by hashing verifier with SHA-256. Server no longer trusts electron-origin header to set request Origin. Electron client now sends real Origin (e.g. myapp:/), so upgrade @better-auth/electron client and server together and ensure app's scheme is in trustedOrigins. Unused disableOriginOverride option is removed. Custom-scheme entries in trustedOrigins now match by scheme and authority instead of string prefix. Host-less entry (myapp:// or exp://) trusts every host of that scheme; host-bearing entry (myapp://callback) matches that host exactly.

OIDC user resolution with issuer and subject pair

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.

MCP package reorganization and route changes

The MCP plugin moves from better-auth into its own package @better-auth/mcp, built on @better-auth/oauth-provider. Import server plugin and helpers from @better-auth/mcp; remote client and adapters from @better-auth/mcp/client and @better-auth/mcp/client/adapters (previously better-auth/plugins and better-auth/plugins/mcp/client). OAuth endpoints move from /mcp/* to /oauth2/*, with discovery at /.well-known/oauth-authorization-server and protected resource metadata at /.well-known/oauth-protected-resource. Discovery-based MCP clients pick up new locations automatically. Route helper renamed requireMcpAuth (was withMcpAuth); remote client renamed createMcpResourceClient (was createMcpAuthClient). requireMcpAuth verifies bearer token against published JWKS and passes verified JWT claims to handler. Unused disableOriginOverride option removed. To migrate: install @better-auth/mcp, add jwt() plugin (now required), move oidcConfig-nested options to flat mcp({ ... }) options. Database models change: oauthApplication becomes oauthClient with new oauthRefreshToken and oauthClientAssertion tables. Regenerate or migrate schema with npx auth migrate or npx auth generate.

Generic OAuth refreshTokenParams for multi-tenant providers

Multi-tenant OIDC providers (Zitadel multi-org, Auth0 with audience) need extra body params on refresh call to rescope tokens without full authorization redirect. Generic-oauth plugin accepts refreshTokenParams option (object or sync/async function) merged into refresh request body, with grant_type and refresh_token protected from override. Function form receives request metadata for triggering request, so request-scoped data (headers, cookies) available without out-of-band state. UpstreamProvider.refreshAccessToken now accepts optional second ctx argument; change is backwards compatible.

OAuth state preservation with server context across redirects

Plugins can now carry server-trusted data across OAuth redirect with new addOAuthServerContext API, read back on callback via getOAuthState().serverContext. Unlike additionalData, it cannot be set from request body, so it is the right place for values server must trust. For @better-auth/oauth-provider, post-login authorization query now travels through that server-only channel, so it can no longer be injected through additionalData.

OAuth provider schema changes for backchannel logout

Schema changes on @better-auth/oauth-provider include: oauthClient.backchannelLogoutUri (string | null), oauthClient.backchannelLogoutSessionRequired (boolean), and oauthAccessToken.revoked (Date | null).

Discovery endpoints for backchannel logout support

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.

OIDC RP-initiated logout requires id_token_hint

The OIDC provider's RP-initiated logout endpoint (/oauth2/endsession) no longer logs a user out, or revokes their OAuth tokens, in response to a cross-site GET that carries only a session cookie. Logout authenticated by a valid id_token_hint is unaffected.

OAuth proxy profile callback state validation

Require OAuth proxy profile callbacks to match an issued OAuth state before creating sessions.

OAuth account linking and account linking proxy restrictions

The OAuth proxy, Google One Tap, and the Expo authorization proxy reject redirect and callback targets that are not in trustedOrigins.

SSO provider id separation from social providers

Separate SSO provider ids from the account-linking provider namespace used for social/OAuth providers. SSO registration now rejects provider ids that collide with a configured social provider, a trustedProviders entry, or a reserved built-in id. The OIDC and SAML callbacks no longer derive trust from a trustedProviders name match — SSO trust comes solely from verified domain ownership (domainVerified).

Generic OAuth userinfo handler mapProfileToUser parameter

handleOAuthUserInfo gains a trustProviderByName option (default true, preserving social-provider behavior) that the SSO plugin sets to false.

OAuth profile sync input: false field handling

OAuth sign-up and account-link profile sync now ignore provider profile values for user fields marked input: false. Input-allowed additional fields still persist from mapProfileToUser, and schema defaults still apply when OAuth creates a user. Apps that used mapProfileToUser to fill input: false fields should set those fields in server-side provisioning code instead.

PayPal user info validation against ID token

Validate PayPal user info against the verified ID token subject during social sign-in.

Stateless OAuth deployment account info persistence

Stateless OAuth deployments can now read account info, access tokens, and refresh tokens after different server instances handle sign-in and later requests. Session refresh also keeps the OAuth account cookie instead of clearing it in that case.

Verify ID token receives endpoint context

Pass the request endpoint context as a third argument to verifyIdToken, so custom ID token verifiers can read request headers (for example Apple's user-agent requirement).

MCP auth client 401 challenge headers exposure

Expose the remote MCP auth client's 401 challenge headers to browser clients using CORS.

OAuth state double-hashing fix when verification storeIdentifier is hashed

Fixed OAuth state double-hashing that occurred when verification storeIdentifier is set to hashed.

Give your agent this brain