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 · all subjects

plugin system/oauth-provider

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

Custom ID token claims configuration

Use `customIdTokenClaims` callback to attach claims to ID tokens. The callback receives `user`, `scopes`, and `metadata`. Claims added should be registered in `advertisedMetadata.claims_supported` for client validation. Example: returning `{ locale: 'en-GB' }` adds a locale claim to the ID token.

Custom access token claims configuration

Use `customAccessTokenClaims` callback to attach claims to access tokens. The callback receives `user`, `scopes`, `referenceId`, `resources`, and `metadata`. Claims are added inside the JWT payload. These functions can throw errors, such as when a user is no longer a member of an organization or lacks requested permissions.

Custom user info claims configuration

Use `customUserInfoClaims` callback to add claims to the `/oauth2/userinfo` endpoint response. The callback receives `user`, `scopes`, `requestedClaims`, and `jwt`. Claims can be conditional based on requested claims using `requestedClaims.includes()`.

Custom token response fields configuration

Use `customTokenResponseFields` callback to add fields to the token endpoint JSON response (alongside `access_token`, `token_type`, etc.). Unlike claims callbacks that add data inside JWT payloads, this adds top-level response fields. The callback receives `grantType`, `user` (undefined for `client_credentials`), `scopes`, `metadata`, and `verificationValue` (only for `authorization_code` grants). It is called before tokens are created, so throwing an error will not leave partially-applied state. Standard OAuth fields cannot be overridden.

Token expiration defaults

Default token expirations in OAuth Provider: `accessTokenExpiresIn` defaults to 1 hour; `m2mAccessTokenExpiresIn` defaults to 1 hour; `idTokenExpiresIn` defaults to 10 hours; `refreshTokenExpiresIn` defaults to 30 days; `refreshTokenReuseInterval` defaults to 0 seconds; `codeExpiresIn` defaults to 10 minutes; `assertionMaxLifetime` defaults to 5 minutes (maximum allowed lifetime for `private_key_jwt` client assertions).

Scope-based access token expiration

Use `scopeExpirations` to set lower expirations for access tokens based on scopes. This is useful for higher-privilege scopes that require shorter expiration times. The earliest expiration takes precedence. Values should be lower than the default `accessTokenExpiresIn` and `m2mAccessTokenExpiresIn`. Example: `scopeExpirations: { 'write:payments': '5m', 'read:payments': '30m' }`.

Dynamic client registration feature

Enable dynamic client registration with `allowDynamicClientRegistration: true`. This allows authorized registration of both public and confidential clients. Public clients are registered with `token_endpoint_auth_method: 'none'`. Confidential clients receive a one-time `client_secret` in the registration response.

Unauthenticated client registration

Enable unauthenticated client registration with `allowUnauthenticatedClientRegistration: true`. This allows clients to register without an authorization header at the `/oauth2/register` endpoint. Public clients are registered with `token_endpoint_auth_method: 'none'`. Confidential clients receive a one-time `client_secret` in the registration response.

Protected dynamic client registration with initial access tokens

Enable protected dynamic client registration by defining `validateInitialAccessToken` callback. This allows machine callers to register clients without a Better Auth user session. The callback receives `initialAccessToken` and `clientMetadata`. Return an object with `referenceId` to attach application ownership metadata, or return `false` to reject the token. Omitting `referenceId` creates an unowned client. The `clientMetadata` is self-asserted and not yet validated, so treat it as untrusted input.

Dynamic client registration secret expiration

Use `clientRegistrationClientSecretExpiration` to set an expiration time for dynamically registered confidential client secrets. By default, dynamically registered confidential clients do not expire. Example: `clientRegistrationClientSecretExpiration: '30d'`.

Client registration default scopes

Use `clientRegistrationDefaultScopes` to set the baseline capability list for clients. All values must be defined in `scopes`. This describes the scopes a client is capable of requesting; it is not a user authorization grant.

Client registration allowed scopes

Use `clientRegistrationAllowedScopes` to add capabilities to the default registration scopes. The effective set is the deterministic, deduplicated union of `clientRegistrationDefaultScopes` and `clientRegistrationAllowedScopes`. When both options are omitted, `scopes` is the effective set.

PKCE requirement by default

PKCE (Proof Key for Code Exchange) is required by default for all clients in the OAuth Provider plugin, following OAuth 2.1 specification. PKCE is always required for clients using `token_endpoint_auth_method: 'none'` and for authorization requests with the `offline_access` scope unless a confidential client has opted out of PKCE and the OIDC request includes both `openid` and `nonce`.

Admin-created client PKCE opt-out

Admin-created confidential clients can opt out of PKCE requirement using the `require_pkce: false` field when calling `auth.api.adminCreateOAuthClient()`. The `require_pkce` field defaults to `true`, applies only to confidential clients, and is ignored for public clients (which always require PKCE). When `offline_access` is requested without PKCE, the OIDC request must include both `openid` and `nonce`.

Dynamic client registration PKCE default

Use `clientRegistrationRequirePKCE: false` to allow dynamically registered confidential clients to opt out of PKCE. This does not apply to public clients (which always require PKCE). Confidential OIDC clients that request `offline_access` without PKCE must send both `openid` and `nonce`.

Unauthenticated client discovery via allowUnauthenticatedClientRegistration

When `allowUnauthenticatedClientRegistration: true` is set, anonymous callers can hit the `/oauth2/register` endpoint to create a client at request time. Confidential registrations receive a one-time `client_secret`; public registrations use `token_endpoint_auth_method: 'none'`. This is one mechanism for unauthenticated client discovery; the CIMD plugin is an alternative approach.

CIMD plugin for client discovery

The `@better-auth/cimd` plugin (Client ID Metadata Document) lets clients identify themselves by hosting a metadata document at an HTTPS URL. The URL itself becomes the `client_id`; the server fetches and validates the document. Generic discovery follows draft-02; the MCP 2026-07-28 profile explicitly pins draft-00 requirements. Use CIMD for MCP public-client identity to maintain the identity of public clients.

OAuth Provider extension with extendOAuthProvider

Use `extendOAuthProvider()` from a plugin `init()` hook to extend the OAuth provider without changing OAuth Provider core. This allows adding token grants, assertion-based client authentication methods, additive discovery metadata, token or UserInfo claims, and client-id discovery sources. Discovery sources should provide a stable, globally unique `id` that is persisted as client provenance.

Dispatched vs additive extension contributions

OAuth Provider extensions follow two disciplines: (1) Dispatched kinds (`grants`, `clientAuthentication`) must be disjoint across extensions—registering a duplicate grant type, `token_endpoint_auth_method`, or `client_assertion_type` is rejected at setup. (2) Additive kinds (`metadata`, `claims`) never override core—a metadata field or key that two extensions both contribute resolves to the first-registered extension.

Claims contributor restrictions

A claims contributor can add new claim names but never replaces an identity, authentication-context, reserved RFC 9068, or other provider-owned claim. To advertise claim names an extension emits, set `advertisedMetadata.claims_supported`—the provider owns `claims_supported` and does not infer it from contributors.

OAuth Provider API access in extensions

A grant handler receives a `provider` capability surface. A plugin that needs provider capabilities (`getClient`, `authenticateClient`, `issueTokens`, `hashToken`, `validateAccessToken`, `requireActiveAccessToken`) from its own endpoints obtains the same with `getOAuthProviderApi(ctx, opts, grantType?)`. Use `validateAccessToken` for introspection-style flows and `requireActiveAccessToken` for protected-resource endpoints that should reject inactive tokens with an OAuth bearer challenge.

Token sender constraint with RFC 7800 confirmation

To sender-constrain an issued token (RFC 7800 `cnf`), pass `confirmation` to `issueTokens` or return it from a `clientAuthentication` strategy. The provider stamps it as the access token's `cnf` and marks the response `token_type` accordingly. `cnf` is authorization-server-owned and cannot be set through a claim contributor.

Client assertion authentication obligations

A `clientAuthentication` strategy must verify the assertion against its own key source and return the client id it proved. After verifying the signature, it must enforce the same assertion hygiene as the built-in `private_key_jwt` method. Use the exported `consumeClientAssertion` helper to bind the assertion to the endpoint audience, require a bounded lifetime, and reject `jti` replays.

Claim precedence for access tokens

Access token claim precedence (lowest to highest authority): extension `claims.accessToken` < per-issuance `accessTokenClaims` < `customAccessTokenClaims` < per-resource `customClaims`. Provider reserves RFC 9068 names (`iss`, `sub`, `aud`, `exp`, `iat`, `jti`, `client_id`, `scope`, `auth_time`, `acr`, `amr`), which are stripped before signing.

Claim precedence for ID tokens

ID token claim precedence: subject/authentication claims < `customIdTokenClaims`; extension and per-issuance `idTokenClaims` are reserved-filtered and additive. Provider reserves OIDC/JWT names (`iss`, `sub`, `aud`, `exp`, `nbf`, `iat`, `jti`, `nonce`, `sid`, `at_hash`, `c_hash`, `s_hash`, `auth_time`, `acr`, `amr`, `azp`) and scope-derived UserInfo claim names.

Claim precedence for UserInfo

UserInfo claim precedence: scope and `claims.userinfo` identity claims < extension `claims.userInfo` (additive only) < `customUserInfoClaims`. Provider reserves and re-pins `sub` claim last.

Per-issuance access token claims are JWT-only

Per-issuance `accessTokenClaims` are JWT-only: opaque access tokens persist no per-issuance claims, so they do not reappear at introspection. A claim that must be visible at opaque-token introspection should use a grant-type-stable `claims.accessToken` contributor, which the introspection path re-derives.

OAuth Client organization tying

OAuth Clients are tied to either a user or `reference_id` at registration and this is immutable. When using the organization plugin, ensure that the `activeOrganizationId` is set on the active session when creating new clients. Use `clientReference` callback to extract the reference ID from the session.

Client CRUD privileges configuration

Use `clientPrivileges` configuration to determine whether a logged-in user can perform specific actions in client creation. The callback receives `action`, `headers`, `user`, and `session`. By default, CRUD actions are allowed for users with matching `userId` or `clientReference`. Returns boolean indicating permission.

Client secret storage options

By default, all secrets are `hashed` on the database, protecting the `client_secret` in case of a database leak. Use `storeClientSecret` option to set the storage method of application `client_secrets`. Only when `disableJwtPlugin: true`, the client secret should be `encrypted` instead of hashed.

Token storage options

Use `storeTokens` option to set the storage method of token values, specifically session refresh tokens and opaque access tokens.

OAuth Provider rate limiting defaults

Default OAuth Provider rate limits (per-IP per-endpoint, 60-second windows): `/oauth2/token` 20 requests, `/oauth2/authorize` 30 requests, `/oauth2/introspect` 100 requests, `/oauth2/revoke` 30 requests, `/oauth2/register` 5 requests, `/oauth2/userinfo` 60 requests. Rate limiting is per-IP per-endpoint with each client IP address having its own rate limit counter. Rate limits only apply when Better Auth's global rate limiting is enabled (enabled by default in production).

OAuth Provider custom rate limits configuration

Customize rate limits for each OAuth endpoint using the `rateLimit` option. Example: `rateLimit: { token: { window: 60, max: 20 }, authorize: { window: 60, max: 30 }, introspect: { window: 60, max: 100 }, revoke: { window: 60, max: 30 }, register: { window: 60, max: 5 }, userinfo: { window: 60, max: 60 } }`. Set an endpoint to `false` to remove the per-endpoint override and fall back to global rate limits.

Refresh token format customization

Use `formatRefreshToken` option to customize refresh token format with `encrypt` and `decrypt` functions. The `encrypt` function receives `token` and `sessionId` and returns formatted token. The `decrypt` function receives the formatted token and returns an object with `token` and optional `sessionId`. This allows adding functionality like refresh token encryption or custom formatting with backwards compatibility.

Advertised metadata scopes configuration

Use `advertisedMetadata.scopes_supported` to customize which scopes are publicized on the metadata endpoint. All scopes inside the `advertisedMetadata` section MUST be listed in the `scopes` configuration, otherwise initialization will fail. This allows showing a subset of supported scopes.

Advertised metadata claims configuration

Use `advertisedMetadata.claims_supported` to advertise custom claims. Claims are in addition to internally supported claims which are automatically determined by scopes. Claims are only applicable for OIDC (i.e., with 'openid' scope). Better Auth advertises `acr_values_supported: ['0']` where '0' means authentication did not meet ISO/IEC 29115 level 1.

Disable JWT Plugin option

Set `disableJwtPlugin: true` to disable JWT plugin requirement. When disabled, access tokens are always opaque and ID tokens are always signed in `HS256` using the `client_secret`. The configuration is still OIDC compliant: `/userinfo` still works and signed `id_token` is still provided. A valid `resource` always provides an opaque access token instead of JWT. Public clients do not receive `id_token`, but can use `/oauth2/userinfo` endpoint. Confidential client `id_token` is signed by their `client_secret`.

Pairwise subject identifiers configuration

Enable pairwise subject identifiers with `pairwiseSecret: 'your-256-bit-secret'`. By default, the `sub` (subject) claim uses the user's internal ID (public subject type). With pairwise enabled, each client receives a unique, unlinkable `sub` for the same user, preventing relying parties from correlating users across services. The server advertises both 'public' and 'pairwise' in `subject_types_supported`. Clients opt in by setting `subject_type: 'pairwise'` at registration.

Pairwise subject identifier computation

Pairwise identifiers are computed using HMAC-SHA256 over the sector identifier (the host of the client's first redirect URI) and the user ID, keyed with `pairwiseSecret`. This means: two clients with different redirect URI hosts receive different `sub` values for the same user; two clients sharing the same redirect URI host receive the same pairwise `sub` (per OIDC Core Section 8.1); the same client always receives the same `sub` for the same user (deterministic).

Pairwise subject identifier scope

Pairwise `sub` appears in: `id_token`, `/oauth2/userinfo` response, and token introspection (`/oauth2/introspect`). When a resource server introspects a token issued to another client, it gets the `sub` that the issuing client sees, not one computed for the resource server itself. JWT access tokens always use the real user ID as `sub`, since resource servers may need to look up users directly.

Pairwise subject identifier limitations

Limitations of pairwise identifiers: `sector_identifier_uri` is not yet supported; all `redirect_uris` for a pairwise client must share the same host, and clients with redirect URIs on different hosts are rejected at registration; `pairwiseSecret` must be at least 32 characters long; rotating `pairwiseSecret` changes all pairwise `sub` values, breaking existing RP sessions—treat this secret as permanent once set.

MCP plugin integration

Use the `@better-auth/mcp` plugin when an MCP server is one of your protected resources. The `mcp()` function is the OAuth Provider for that Better Auth instance, so do not register both `mcp()` and `oauthProvider()`. It accepts the OAuth Provider options directly. Use `requireMcpAuth` when the MCP route shares the auth instance, or `createMcpProtectedRequestHandler` when the resource server runs separately.

MCP device grant support

The MCP plugin can support a separate registered CLI through the device grant. MCP clients keep their discovery-driven authorization code flow, while the CLI asks the same provider for a resource-bound token through device authorization. See the MCP plugin documentation for adding device authorization for your own CLI.

oauthClient table schema

OAuth Client table (`oauthClient`) fields: `id` (string, primary key, database ID); `clientId` (string, unique, client identifier); `clientSecret` (string, optional, secret key for public clients using PKCE); `disabled` (boolean, optional, indicates if application is disabled); `skipConsent` (boolean, optional, allows skipping consent for trusted apps); `enableEndSession` (boolean, optional, allows logout via id_token for trusted apps); `subjectType` (string, optional, 'pairwise' for unique per-user sub or public default); `scopes` (string array, optional, allowed scopes); `userId` (string, optional, foreign key to user); `referenceId` (string, optional, reference of client owner if not user); `createdAt` (Date, optional); `updatedAt` (Date, optional); `name` (string, optional, client name); `uri` (string, optional, website URI for UI); `icon` (string, optional, website icon for UI); `contacts` (string array, optional, contact list for UI); `tos` (string, optional, terms of service); `policy` (string, optional, privacy policy); `softwareId` (string, optional, client-defined software identifier); `softwareVersion` (string, optional, software version); `softwareStatement` (string, optional, signed JWT of software metadata); `redirectUris` (string array, required, redirect URIs); `postLogoutRedirectUris` (string array, optional); `backchannelLogoutUri` (string, optional, RP URL for logout tokens); `backchannelLogoutSessionRequired` (boolean, optional, requires sid claim); `tokenEndpointAuthMethod` (string, optional, supports 'none', 'client_secret_basic', 'client_secret_post', 'private_key_jwt'); `grantTypes` (string array, optional, supports 'authorization_code', 'client_credentials', 'refresh_token'); `responseTypes` (string array, optional, supports 'code'); `applicationType` (string, optional, 'web' or 'native'); `clientDiscoveryId` (string, optional, discovery extension identifier); `requirePKCE` (boolean, optional, PKCE requirement); `dpopBoundAccessTokens` (boolean, optional, DPoP binding requirement); `metadata` (json, optional, additional metadata).

oauthRefreshToken table schema

OAuth Refresh Token table (`oauthRefreshToken`) fields: `id` (string, primary key, database ID); `token` (string, hashed/encrypted refresh token); `clientId` (string, foreign key to oauthClient); `sessionId` (string, optional, foreign key to session with onDelete 'set null'); `userId` (string, foreign key to user, token user); `referenceId` (string, optional, consented reference ID); `scopes` (string array, granted scopes); `revoked` (Date, optional, revocation timestamp); `rotatedAt` (Date, optional, consumption timestamp); `rotationReplayResponse` (string, optional, encrypted replay response during reuse interval); `rotationReplayExpiresAt` (Date, optional, replay expiration); `authTime` (Date, optional, original authentication time preserved across rotation); `createdAt` (Date, creation timestamp); `expiresAt` (Date, expiration timestamp); `confirmation` (json, optional, RFC 7800 cnf sender-constraint, carried forward on rotation).

oauthAccessToken table schema

OAuth Access Token table (`oauthAccessToken`) fields: `id` (string, primary key, database ID of opaque token); `token` (string, unique, hashed/encrypted access token); `clientId` (string, foreign key to oauthClient); `sessionId` (string, optional, foreign key to session with onDelete 'set null'); `refreshId` (string, optional, foreign key to oauthRefreshToken); `userId` (string, optional, foreign key to user); `referenceId` (string, optional, consented reference ID); `scopes` (string array, granted scopes); `createdAt` (Date, creation timestamp); `expiresAt` (Date, expiration timestamp); `confirmation` (json, optional, RFC 7800 cnf sender-constraint surfaced as cnf at introspection); `revoked` (Date, optional, revocation timestamp—populated on session end and by back-channel logout; introspection and token use reject revoked tokens).

oauthConsent table schema

OAuth Consent table (`oauthConsent`) fields: `id` (string, primary key, database ID); `userId` (string, foreign key to user); `clientId` (string, foreign key to oauthClient); `referenceId` (string, optional, consented reference ID); `scopes` (string array, consented scopes); `requestedUserInfoClaims` (string array, optional, OIDC UserInfo claim names consented to); `createdAt` (Date, consent timestamp); `updatedAt` (Date, last update timestamp).

oauthClientAssertion table schema

OAuth Client Assertion table (`oauthClientAssertion`) records each `private_key_jwt` client assertion `jti` to prevent replay. Records: `id` (string, primary key, digest of per-client assertion identifier formatted as `private_key_jwt:<clientId>:<jti>`); `expiresAt` (Date, when assertion expires and row becomes safe to delete). Replayed or concurrent assertions collide on the primary key, and the database rejects atomically. No scheduled job prunes these rows; remove expired rows with own cleanup if table grows.

Token prefix configuration

Add prefixes to opaque access tokens, refresh tokens, or client secrets using the `prefix` configuration. Available options: `opaqueAccessToken` (string | undefined), `refreshToken` (string | undefined), `clientSecret` (string | undefined). Recommended to add prefixes prior to first production deployment; once deployed, consider them immutable. If previously deployed, use `generateOpaqueAccessToken`, `generateRefreshToken`, and `generateClientSecret` functions instead.

Database adapter client_id field optimization

Database adapters may map the field `client_id` on the table `oauthClient` to `id` to improve lookup performance. Note that `id` should support strings formatted like UUIDs and URLs.

Give your agent this brain