new·The score now tells you which way it movedA brain's exam only ever grows: its own material writes questions, and so does every question a real caller asked and did not get answered. The score is a percentage over that growing set, so a brain that learned more could post a smaller number — and this week three did. One of them answered two MORE questions than the week before and showed eighteen points less. Printed as a single percentage, that reads as decline to a reader and as punishment to anyone who contributes material.all news →
mozg.beta
Sign in

Better Auth · all subjects

better auth/plugins

128 notes in this subject, read out of this brain and free to use. This is page 1 of 3.

Better Auth plugin system

Better Auth has a plugin system that allows extending functionality without forking or complex workarounds.

Plugins available for Auth0 migration

Three plugins are recommended for Auth0 migration: admin plugin (manage users, impersonation, app-level roles/permissions), twoFactor plugin (add two-factor authentication), username plugin (add username authentication). Import from 'better-auth/plugins' and pass to plugins array in betterAuth config.

Plugins available for Clerk migration to Better Auth

When migrating from Clerk to Better Auth, you can add the following optional plugins: Admin Plugin for managing users, impersonations, and roles/permissions; Two Factor Plugin for two-factor authentication; Phone Number Plugin for phone authentication; Username Plugin for username authentication. These are imported from 'better-auth/plugins' and passed to the plugins array in the auth configuration.

Authorize accepts form-encoded requests and rejects request objects

The authorization and userinfo endpoints now accept form-encoded (POST) requests. The authorization endpoint explicitly rejects the OIDC `request` and `request_uri` parameters it does not support. No action needed for standard clients.

`max_age` enforced in OAuth provider

When a client asks for `max_age` and the user's login is older than that, the provider sends them back to log in. Before, the request was ignored. Watch date columns: the `max_age` check reads a session's creation time back as a date. If custom schema stores session timestamps as text instead of a real date or integer-timestamp type, the check can misread the value and send users into a login loop. Store `user`, `session`, `account`, and `verification` timestamps with a date or timestamp type.

Token target locked to login in OAuth provider

The API a token is for is now captured at login and locked to that grant. A later request can narrow the target API but cannot widen it. Asking for an API the login did not cover is rejected. A custom-claims callback now receives a list of resources instead of one value. Run the migration to add the resource columns and update custom-claims callbacks to read the resource list. Make sure clients ask only for resources their login covered.

OAuth endpoints return standard error envelopes

Validation and malformed-request failures on OAuth endpoints (token, authorize, revoke, introspect, register, end-session) now return RFC 6749 `{ error, error_description }` envelopes instead of the previous generic validation-error shape. Update any client or tool that parsed the old error shape to read `error` and `error_description`.

UserInfo accepts bearer token in form body

The userinfo endpoint now accepts the access token in a form-encoded body and rejects a request that sends the token in both the header and the body. Send the access token in one place, the `Authorization` header or the form body, not both.

Server-side OAuth requests refuse redirects

Better Auth now refuses HTTP redirects on server-side OAuth requests: token exchange, token refresh, client credentials, token introspection, and JWKS requests. Conformant OAuth providers answer these endpoints directly. If a custom provider endpoint redirects, make it return the final response directly.

Sign-out revokes session tokens in OAuth provider

When a session ends, access tokens tied to it are now revoked. They read as inactive at introspection and userinfo. Before, they lived until expiry. Refresh tokens granted without `offline_access` are revoked too; `offline_access` refresh tokens are preserved so long-lived API access survives a browser sign-out. The server sends a logout message to each app that registered a logout URL. Run `generate` and `migrate` to add new columns. Expect session-bound tokens to stop working at sign-out. On serverless platforms, set `advanced.backgroundTasks.handler` so sending logout messages does not slow down sign-out.

`jwt.sign` callbacks must match configured algorithm

A custom `jwt.sign` callback is rejected when its algorithm differs from `keyPairConfig.alg` during ID-token issuance. Align custom signing algorithm with `keyPairConfig.alg`.

Refresh-token retries can be tolerated with reuseInterval

The OAuth provider can replay the same refresh response for duplicate refresh requests during `refreshTokenReuseInterval`. Strict refresh-token replay handling remains the default. Set `refreshTokenReuseInterval` only if a public or native client can retry a refresh request with an old token after another local session already rotated it. MCP defaults this window to 30 seconds for native and public clients. Set `refreshTokenReuseInterval: 0` to keep strict replay handling.

Extending OAuth provider with supported surface

If you added custom grants, claims, or client-authentication methods by patching or forking the OAuth provider, use the supported extension surface instead. Register contributions with `extendOAuthProvider(ctx, ...)` from the plugin's `init(ctx)` hook. Mint tokens with `provider.issueTokens(...)`, authenticate a client with `provider.authenticateClient(...)`, and hash a token with `provider.hashToken(...)`. Bind a token's audience by passing `resources` to `issueTokens`; the server owns the audience. A contribution written in an older or hand-rolled shape can fail silently: it may type-check and run while the grant or claim never reaches a token.

MCP default refreshTokenReuseInterval

MCP defaults `refreshTokenReuseInterval` to 30 seconds for native and public clients. Set it to `0` if you want strict refresh-token replay handling.

Dynamic client registration enforces per-client resource access

If you accept dynamically registered clients in the OAuth provider, the resource model enforces per-client resource access by default. A dynamic client's token request can be rejected with `invalid_target`. Set `enforcePerClientResources: false` for that case, and register each client's `grant_types` explicitly, or the token endpoint rejects them with `unauthorized_client`.

Unauthenticated registration keeps client auth method

Dynamic Client Registration without a logged-in user no longer forces the client to be public. A client that omits `token_endpoint_auth_method` is now confidential with the RFC 7591 default `client_secret_basic` and a generated secret; it becomes public only when it registers `token_endpoint_auth_method: "none"`. If you relied on unauthenticated registrations being downgraded to public, register `token_endpoint_auth_method: "none"` explicitly.

Client authentication tied to grant in OAuth provider

A custom client-authentication method registered through the extension surface can now only prove which client is calling. The server decides what that client is allowed to do. Companion plugins that added a client-authentication method should rely on the server-resolved client rather than returning their own client decision.

ID tokens drop profile and email scope claims

ID tokens issued through the authorization-code flow no longer carry the profile and email scope claims. Those claims are available from the UserInfo endpoint. Read profile and email claims from UserInfo instead of the ID token.

Custom ID-token claims cannot override protocol claims

Custom ID-token claims can no longer set protocol claims that the standard reserves for the server: issuer, subject, audience, expiry, nonce, session binding, `auth_time`, `acr`, `amr`, and `azp`. Namespaced claims still appear. ID tokens report `acr: "0"` rather than a vendor-specific value. If a `customIdTokenClaims` callback, extension claim contributor, or per-issuance `idTokenClaims` sets a reserved claim, that value is ignored. Move data into a namespaced claim or rely on the server's value. The same rule applies to custom access-token claims: `customAccessTokenClaims`, per-resource `customClaims`, per-issuance `accessTokenClaims`, and extension contributors cannot set reserved names like `jti`, `client_id`, `auth_time`, `acr`, `amr`, and `cnf`.

Protected resources replace audience list in OAuth provider

Audiences are now resources in the OAuth provider. Each resource can have its own token lifetime, scopes, claims, and signing keys. The old `validAudiences` list is removed. Move each entry from `validAudiences` into `resources`. Link clients to specific resources through `oauthClientResource` or registration. Run `generate` and `migrate` to add the new tables and columns. Check refresh-token lifetimes: the shortest applicable lifetime now wins, so a per-resource value longer than the provider default is capped at the default.

PKCE requirements for confidential and OIDC clients

PKCE is always required for public clients. `clientRegistrationRequirePKCE` is a server-wide `oauthProvider()` option that defaults to `true`; setting it to `false` lets every confidential client registered through Dynamic Client Registration skip PKCE on the authorization-code flow. The setting is server-owned, so a client cannot opt itself out. Once a confidential client has opted out, a request carrying `offline_access` scope can use an OIDC `nonce` in place of PKCE; a confidential client that still requires PKCE is not exempted by `offline_access`. Set `clientRegistrationRequirePKCE: false` only if you accept confidential clients that cannot use PKCE.

Introspection returns consistent claims

`/oauth2/introspect` now returns the same claims for an opaque token as it does for a JWT, and a resource server can introspect a token issued to a different client. Expect richer, consistent introspection responses for opaque tokens.

DPoP proxy URL canonicalization needed

Native DPoP checks the proof's `htu` claim against the URL the token endpoint computes for itself. Behind a TLS-terminating proxy or a custom server, that computed URL can be the internal bind address like `http://0.0.0.0:3000` or the proxy's internal scheme and port. The client signed `htu` from the public discovery URL, so a valid proof can be rejected. Canonicalize the incoming request's scheme and host to the configured `baseURL` at the route boundary before the provider reads it.

MCP renames withMcpAuth to requireMcpAuth

`withMcpAuth` is renamed `requireMcpAuth`. This is not a pure rename: `requireMcpAuth` now passes the verified JWT claims to the handler, not an opaque session row. Read `jwt.sub`, `jwt.client_id`, and `jwt.scope` (a space-delimited string) in place of the old `session.userId`, `session.clientId`, and `session.scopes` array. `auth.api.getMcpSession` is removed.

MCP moves to own package in 1.7

The MCP plugin moves from `better-auth` into `@better-auth/mcp`, built on the OAuth provider. Install `@better-auth/mcp` and update imports: the server plugin and helpers from `@better-auth/mcp`, the client and adapters from `@better-auth/mcp/client` and `@better-auth/mcp/client/adapters`. Add the `jwt()` plugin, which is now required. Move options nested under `oidcConfig` to the top level of `mcp({ ... })` and add a resource identifier like `resource: "https://api.example.com/mcp"`. The OAuth endpoints move from `/mcp/*` to `/oauth2/*`. Discovery-based MCP clients find the new locations on their own.

Old `oidcProvider` plugin removed, migrate to `oauthProvider`

The old `oidcProvider` plugin is removed. Replace `oidcProvider` from `better-auth/plugins` with `oauthProvider` from `@better-auth/oauth-provider` and move the config across. The schema migration is not a drop-in: registered clients move from `oauthApplication` to a restructured `oauthClient` table and are not copied automatically. Follow the client-data step before cutover.

DPoP renames token verifier and adds new endpoint

The plain token-checking helper `verifyAccessToken` is renamed `verifyBearerToken` and now rejects DPoP tokens. Use the new `verifyAccessTokenRequest` on endpoints that may receive DPoP requests. Run `generate` and `migrate` to add the token-binding column. To support DPoP, configure database-backed verification storage.

`/oauth2/revoke` rejects valid JWT access tokens

Revoking a still-valid JWT access token now returns `400 unsupported_token_type`. Revoke refresh tokens or opaque access tokens; do not call revoke on JWT access tokens.

Client creation returns 201 in OAuth provider

Creating a client now returns `201 Created` instead of `200 OK`. The registration endpoint enforces the same permission checks as the manual create endpoints. Update any client that expects a `200` from client creation to accept `201`. To allow machine clients to register, configure `validateInitialAccessToken`.

Organization subscriptions require `organization.enabled`

`referenceMiddleware` now rejects organization-scoped subscriptions unless `organization: { enabled: true }` is set in the Stripe plugin config. Set `organization: { enabled: true }` in the `stripe()` plugin options for organization-scoped subscriptions. The organization plugin is still needed separately.

Registration requires reciprocal response and grant types

A registered client's `response_types` and `grant_types` must be reciprocal: a `code` response type requires the `authorization_code` grant, and a token grant requires its matching response type. Mismatched registrations are rejected. Register matching `response_types` and `grant_types` for each client.

`onSubscriptionCancel` event parameter required

The `event` parameter on the `onSubscriptionCancel` callback is now required. Update the callback to expect a non-optional `event`.

Plugin configuration in Better Auth

Plugins are configured via the plugins array in the auth instance. Import the plugin function, such as haveIBeenPwned from 'better-auth/plugins/haveibeenpwned', and add it to the plugins array.

Server plugin structure - basic plugin factory

A Better Auth server plugin is created by exporting a function that returns an object satisfying the BetterAuthPlugin type. The plugin object must have an id property. Example: export const birthdayPlugin = () => ({ id: "birthdayPlugin" } satisfies BetterAuthPlugin);

Plugin schema definition for user fields

Plugins can extend the user model by defining a schema object with a user property containing fields. Each field has a type (string, number, boolean, or date), a required property (boolean, default false), and a unique property (boolean, default false). Example schema: { schema: { user: { fields: { birthday: { type: "date", required: true, unique: false } } } } }

Plugin architecture - server and client pair

Better Auth plugins operate as a pair consisting of a server plugin that forms the foundation of the authentication system and a client plugin that provides convenient frontend APIs to interact with the server implementation.

Generate database schemas with CLI

After defining plugin schemas, run the CLI command 'npx auth@latest generate' to automatically generate the required database schema changes for the plugin fields.

Initializing client plugins in createAuthClient

Client plugins are initialized by passing an array of client plugin instances to the plugins option when calling createAuthClient(). Example: createAuthClient({ plugins: [birthdayClientPlugin()] })

Initializing plugins in betterAuth

Plugins are initialized on the server by passing an array of plugin instances to the plugins option when calling betterAuth(). Example: betterAuth({ plugins: [birthdayPlugin()] })

Client plugin structure with server plugin type inference

A Better Auth client plugin is created by exporting a function that returns an object satisfying BetterAuthClientPlugin type. It must have an id property matching the server plugin id and a $InferServerPlugin property that infers types from the server plugin. Example: { id: "birthdayPlugin", $InferServerPlugin: {} as ReturnType<BirthdayPlugin> }

APIError for throwing validation errors in hooks

Within plugin hook handlers, use createAuthMiddleware from better-auth/api to create async handlers that can throw APIError. APIError takes an error code (like "BAD_REQUEST") and an options object with a message property.

Plugin hooks for authorization logic

Plugins use hooks to run code before or after actions. Hooks have a before array where each hook contains a matcher function that checks the context path and a handler using createAuthMiddleware. The matcher receives context and can match against context.path to intercept specific requests like signup endpoints.

dash() and sentinel() plugins can be used independently

The dash() and sentinel() plugins can be used independently. Using both together provides the full Better Auth Infrastructure experience.

dash() and sentinel() plugin example for server

Example server configuration using both dash() and sentinel() plugins: ```ts import { betterAuth } from "better-auth"; import { dash, sentinel } from "@better-auth/infra"; export const auth = betterAuth({ plugins: [ dash({ apiKey: process.env.BETTER_AUTH_API_KEY, }), sentinel({ apiKey: process.env.BETTER_AUTH_API_KEY, }) ], }); ```

sentinel() plugin configuration

The `sentinel()` plugin is available on pro plan or above. It is added to the Better Auth plugins array with an `apiKey` parameter set to `process.env.BETTER_AUTH_API_KEY`. It enables security checks and abuse protection.

dash() plugin configuration

The `dash()` plugin is added to the Better Auth plugins array with an `apiKey` parameter set to `process.env.BETTER_AUTH_API_KEY`. It enables analytics tracking, audit logging, dashboard admin APIs, and more.

Better Auth Infrastructure dash plugin installation example

To use Better Auth Infrastructure, import betterAuth from 'better-auth', import dash from '@better-auth/infra', and configure it as a plugin in the betterAuth configuration. The basic example shows: ```ts import { betterAuth } from "better-auth"; import { dash } from "@better-auth/infra"; export const auth = betterAuth({ plugins: [dash()], }); ```

Better Auth Infrastructure plugins available

Better Auth Infrastructure provides multiple plugins including: dash (dashboard), audit-logs (for tracking and querying authentication events), and sentinel (for abuse protection and security checks).

dash() plugin automatically collects audit logs

The dash() plugin hooks into Better Auth and automatically records authentication events without any additional configuration. Once dash() is active in the plugins array, audit logs are collected automatically for sign-ups, sign-ins, password changes, and other events.

Audit log identifier definition

An identifier is a unique value for an event, such as an email address or username.

Audit log tracked events - User category

User audit log events tracked: user_signed_up (new user registration), user_profile_updated (user updates profile), user_profile_image_updated (user changes avatar), user_email_verified (email verification completed), user_banned (user is banned), user_unbanned (user is unbanned), user_deleted (user account deleted).

Audit log tracked events - Security category

Security audit log events (tracked when using Sentinel plugin): security_blocked, security_allowed, security_credential_stuffing, security_impossible_travel, security_geo_blocked, security_bot_blocked, security_suspicious_ip, security_velocity_exceeded, security_free_trial_abuse, security_compromised_password, security_stale_account.

Audit log tracked events - Organization category

Organization audit log events (tracked when using organization plugin): organization_created, organization_updated, member_added, member_removed, member_role_updated, member_invited, invite_accepted, invite_rejected, invite_cancelled, team_created, team_updated, team_deleted, team_member_added, team_member_removed.

Audit log tracked events - Verification category

Verification audit log events tracked: password_reset_requested (password reset initiated), password_reset_completed (password reset finished), email_verification_sent (verification email sent).

Audit log tracked events - Account category

Account audit log events tracked: account_linked (social account linked), account_unlinked (social account unlinked), password_changed (password updated).

Audit log tracked events - Session category

Session audit log events tracked: user_signed_in (successful sign-in), user_signed_out (user signs out), session_created (new session created), session_revoked (single session revoked), sessions_revoked_all (all sessions revoked), user_impersonated (admin starts impersonating user), user_impersonation_stopped (admin stops impersonating).

Account events tracked by dash plugin

The dash plugin automatically tracks these account events: account_linked (social account linked), account_unlinked (social account unlinked), password_changed (password updated).

Session events tracked by dash plugin

The dash plugin automatically tracks these session events: user_signed_in (successful sign-in), user_signed_out (user signs out), session_created (new session created), session_revoked (single session revoked), sessions_revoked_all (all sessions revoked), user_impersonated (admin starts impersonating user), user_impersonation_stopped (admin stops impersonating).

User events tracked by dash plugin

The dash plugin automatically tracks these user events: user_signed_up (new user registration), user_profile_updated (user updates profile), user_profile_image_updated (user changes avatar), user_email_verified (email verification completed), user_banned (user is banned), user_unbanned (user is unbanned), user_deleted (user account deleted).

Activity tracking configuration

Activity tracking is configured as an object within DashOptions with two properties: enabled (boolean to enable/disable activity tracking) and updateInterval (timeout in ms, default 5 minutes or 300000 ms). When enabled, a lastActiveAt field is automatically added to the user schema and updated on user activity.

Give your agent this brain