> You are reading Better Auth documentation for `v1.6`. This is not the current stable release. APIs may differ from the latest stable version.

# OAuth 2.1 Provider (/docs/1.6/plugins/oauth-provider)

A Better Auth plugin that enables your auth server to serve as an OAuth 2.1 provider.



An **OAuth 2.1 Provider Plugin** that allows you to turn your authentication server into an OAuth provider with OIDC compatibility allowing users and other services to authenticate with your API.

The plugin has a secured configuration by default providing ease to users unfamiliar with the details of OAuth.

**Key Features**:

* **OAuth 2.1**: Restricted security practices to [OAuth 2.1](https://oauth.net/2.1/)
* **Issuer Validation**: Authorization responses include `iss` parameter to prevent [mix-up attacks](https://datatracker.ietf.org/doc/html/rfc9207)
* **MCP Enabled**: Support with [MCP authentication](#mcp)
* **OIDC compatibility**: [OIDC](https://openid.net/specs/openid-connect-core-1_0.html)-compliant with the `openid` scope
  * **UserInfo**: Endpoint providing current user details
  * **id\_token**: JWT-signed user information
  * **OIDC Logout**: [RP-initiated](https://openid.net/specs/openid-connect-rpinitiated-1_0.html)-compliant Logout
* **Dynamic Client Registration**: Allow clients to register clients dynamically.
  * **Public Clients**: Support public clients for native mobile clients and user-agent clients (like AI)
  * **Confidential Clients**: Supports confidential clients for web clients
  * **Trusted Clients**: Configure hard-coded trusted clients with optional consent bypass.
* **JWT Plugin compatibility**: required by default with an option to disable
  * **JWT Signing**: sign JWT tokens when requesting a `resource`
  * **JWKS Verifiable**: verify tokens remotely at the [`/jwks`](/docs/1.6/plugins/jwt#verifying-the-token) endpoint
* **Authorization Prompts**: prompts that initiate specific login flows
  * **Consent**: Ensure consent is granted for each scope. Forcible with `prompt=consent`.
  * **Select Account**: Ensure an account is selected prior when specific scopes being granted. Forcible with `prompt=select_account`.
* **Resource Endpoints**: Read and manage tokens.
  * **Introspection**: [RFC7662](https://datatracker.ietf.org/doc/html/rfc7662)-compliant Introspection.
  * **Revocation**: [RFC7009](https://datatracker.ietf.org/doc/html/rfc7009)-compliant Revocation.

**Grants Supported**

* **authorization\_code**: Code for user token exchange with PKCE and S256 requirements.
* **refresh\_token**: Issue refresh tokens and handle access token renewal using `offline_access` scope.
* **client\_credentials**: Machine to Machine tokens for API communication.

## Installation [#installation]

<Steps>
  <Step>
    ### Mount the Plugin [#mount-the-plugin]

    Add the OIDC plugin to your auth config. See [Configuration Section](#configuration) on how to configure the plugin.

    ```ts title="auth.ts"
    import { betterAuth } from "better-auth";
    import { jwt } from "better-auth/plugins";
    import { oauthProvider } from "@better-auth/oauth-provider"; // [!code highlight]

    const auth = betterAuth({
      disabledPaths: [
        "/token",
      ],
      plugins: [
        jwt(),
        oauthProvider({ // [!code highlight]
          loginPage: "/sign-in", // [!code highlight]
          consentPage: "/consent", // [!code highlight]
          // ...other options // [!code highlight]
        }) // [!code highlight]
      ],
    });
    ```
  </Step>

  <Step>
    ### Migrate the Database [#migrate-the-database]

    Run the migration or generate the schema to add the necessary fields and tables to the database.

    <Tabs items="[&#x22;migrate&#x22;, &#x22;generate&#x22;]">
      <Tab value="migrate">
        ```bash
        npx auth migrate
        ```
      </Tab>

      <Tab value="generate">
        ```bash
        npx auth generate
        ```
      </Tab>
    </Tabs>

    See the [Schema](#schema) section to add the fields manually.
  </Step>

  <Step>
    ### Confirm `/.well-known` endpoints [#confirm-well-known-endpoints]

    Better Auth serves the OAuth Authorization Server metadata and OpenID Connect discovery metadata from the auth handler automatically. If your framework only forwards requests under a catch-all auth route, make sure the issuer metadata URLs reach `auth.handler`.

    * OAuth Authorization Server metadata is available at both `{issuer}/.well-known/oauth-authorization-server` and `/.well-known/oauth-authorization-server/[issuer-path]`.
    * OpenID Connect discovery metadata is available at `{issuer}/.well-known/openid-configuration` when you use the `openid` scope.
    * If you are using the resource server (for example, for MCP), add the OAuth Protected Resource metadata endpoint to the API that receives access tokens.
  </Step>

  <Step>
    ### Create your first oauth client [#create-your-first-oauth-client]

    Create your first confidential oauth client.

    ```ts
    const client = await auth.api.createOAuthClient({
    		headers,
    		body: {
    			redirect_uris: [redirectUri],
    		}
    	});
    console.log(client); // If you wish, you may add the `client_id` to `cachedTrustedClients`
    ```

    <Callout type="info">
      To create a public client (ie. without a client secret), set `token_endpoint_auth_method: "none"`.
    </Callout>
  </Step>
</Steps>

## Client Plugins [#client-plugins]

There exists two clients. You may wish to add one or both depending on your setup.

### OAuth Client [#oauth-client]

The OAuth Client is the connecting `oauthClient` such a mobile or web application.

```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client";
import { oauthProviderClient } from "@better-auth/oauth-provider/client" // [!code highlight]

export const authClient = createAuthClient({
  plugins: [
    oauthProviderClient(), // [!code highlight]
  ],
});
```

### Resource Client [#resource-client]

The Resource Server is a client that operates on your API server to perform actions like token verification and provide metadata.

```ts title="server-client.ts"
import { auth } from "@/lib/auth";
import { createAuthClient } from "better-auth/client";
import { oauthProviderResourceClient } from "@better-auth/oauth-provider/resource-client" // [!code highlight]

export const serverClient = createAuthClient({
  plugins: [
    oauthProviderResourceClient(auth) // auth optional // [!code highlight]
  ],
});
```

## Usage [#usage]

The plugin operates as an OAuth 2.1 server with OIDC compatible endpoints and JWT verifiable access tokens. The following provides more detailed information about each endpoint.

### OAuth Clients [#oauth-clients]

In OAuth there are two types of clients:

* **Public Clients**: Cannot store a client secret such as native mobile clients and user-agent clients (like AI)
* **Confidential Clients**: Can store a client secret such as web clients

#### Get Client [#get-client]

To obtain client information owned by a specific user or organization use the following endpoint:

**Endpoint:** `GET /oauth2/get-client`

### Client Side

```ts
const { data, error } = await authClient.oauth2.getClient({
    query: {
        client_id, // required, The OAuth client's client_id
    },
});
```

### Server Side

```ts
const data = await auth.api.getOAuthClient({
    query: {
        client_id, // required, The OAuth client's client_id
    },
    // This endpoint requires session cookies.
    headers: await headers(),
});
```

### Type Definition

```ts
type getOAuthClient = {
  /**
   * The OAuth client's client_id
   */
  client_id: string,
}
```

#### Get Public Client [#get-public-client]

To obtain public client fields to display on login flow pages such as consent, use the following endpoint. Note: the user must be signed in to use this endpoint.:

**Endpoint:** `GET /oauth2/public-client`

### Client Side

```ts
const { data, error } = await authClient.oauth2.publicClient({
    query: {
        client_id, // required, The OAuth client's client_id
    },
});
```

### Server Side

```ts
const data = await auth.api.getOAuthClientPublic({
    query: {
        client_id, // required, The OAuth client's client_id
    },
    // This endpoint requires session cookies.
    headers: await headers(),
});
```

### Type Definition

```ts
type getOAuthClientPublic = {
  /**
   * The OAuth client's client_id
   */
  client_id: string,
}
```

#### Get Public Client Prelogin [#get-public-client-prelogin]

To obtain a public client prior to login, you must first enable the endpoint in your configuration:

```ts title="auth.ts"
oauthProvider({
  allowPublicClientPrelogin: true,
})
```

Then, the following endpoint will obtain public client information.

**Endpoint:** `POST /oauth2/public-client-prelogin`

### Client Side

```ts
const { data, error } = await authClient.oauth2.publicClientPrelogin({
    client_id, // required, The OAuth client's client_id
    oauth_query, // required, Valid oauth query parameters (Sent automatically when using the provided client)
});
```

### Server Side

```ts
const data = await auth.api.getOAuthClientPublicPrelogin({
    body: {
        client_id, // required, The OAuth client's client_id
        oauth_query, // required, Valid oauth query parameters (Sent automatically when using the provided client)
    },
});
```

### Type Definition

```ts
type getOAuthClientPublicPrelogin = {
  /**
   * The OAuth client's client_id
   */
  client_id: string,
  /**
   * Valid oauth query parameters (Sent automatically when using the provided client)
   */
  oauth_query: string
}
```

#### List Clients [#list-clients]

To obtain a list of clients owned by a specific user or organization, use the following endpoint:

**Endpoint:** `GET /oauth2/get-clients`

### Client Side

```ts
const { data, error } = await authClient.oauth2.getClients();
```

### Server Side

```ts
const data = await auth.api.getOAuthClients({
    // This endpoint requires session cookies.
    headers: await headers(),
});
```

### Type Definition

```ts
type getOAuthClients = {
}
```

#### Create Client [#create-client]

To create an oauth client tied to a specific user or organization, use the `/oauth2/create-client` endpoint (eg. `createOAuthClient`). The parameters are equivalent to the registration endpoint described by [RFC7591](https://datatracker.ietf.org/doc/html/rfc7591).

The following fields on the database are considered restricted and should only be editable by admin users.

* `client_secret_expires_at`: The expiration time for a secret of a confidential client
* `skip_consent`: Allows the ability to skip user consent flow. Useful for trusted clients.
* `enable_end_session`: Allows a user to logout of a session from the client via their `id_token` at the `/oauth2/end-session` endpoint. Used in OIDC-setups and specified trusted clients.
* `metadata`: Additional private metadata to attach to the client.

In some cases, you may wish to create logic to create oauth clients with restricted fields through custom APIs, company admin portals, or server initialization, you may use the following server-only endpoint:

```ts title="admin-create-oauth.ts"
import { auth } from "@/lib/auth"

await auth.api.adminCreateOAuthClient({
  headers,
  body: {
    redirect_uris: [redirectUri],
    client_secret_expires_at: 0, // [!code highlight]
    skip_consent: true, // [!code highlight]
    enable_end_session: true, // [!code highlight]
  }
});
```

#### Update Client [#update-client]

To update an oauth client tied to a specific user or organization, use the `/oauth2/update-client` endpoint (eg. `updateOAuthClient`). The parameters are equivalent to the registration endpoint described by [RFC7591](https://datatracker.ietf.org/doc/html/rfc7591).

**Endpoint:** `POST /oauth2/update-client`

### Client Side

```ts
const { data, error } = await authClient.oauth2.updateClient({
    client_id, // required, The OAuth client's client_id
    update, // required, The fields to update
});
```

### Server Side

```ts
const data = await auth.api.updateOAuthClient({
    body: {
        client_id, // required, The OAuth client's client_id
        update, // required, The fields to update
    },
    // This endpoint requires session cookies.
    headers: await headers(),
});
```

### Type Definition

```ts
type updateOAuthClient = {
  /**
   * The OAuth client's client_id
   */
  client_id: string,
  /**
   * The fields to update
   */
  update: OAuthClient,
}
```

Restrictions on this endpoint:

* You are unable to switch between confidential and public clients. The client type must be determined at creation.
* You cannot update the client secret. To rotate the `client_secret` use the rotate client secret endpoint.

In some cases, you may wish to create logic to update oauth clients with restricted fields through custom APIs, company admin portals, or server initialization, you may use the following server-only endpoint. The fields are described in the create section.:

```ts title="admin-update-oauth.ts"
import { auth } from "@/lib/auth"

await auth.api.adminUpdateOAuthClient({
  headers,
  body: {
    redirect_uris: [redirectUri],
    client_secret_expires_at: 0, // [!code highlight]
    skip_consent: true, // [!code highlight]
    enable_end_session: true, // [!code highlight]
  }
});
```

#### Rotate Client Secret [#rotate-client-secret]

<Callout type="warn">
  The current implementation rotates the client secret immediately and the previous secret is invalidated immediately.
</Callout>

To rotate a client secret, you must use the following endpoint:

**Endpoint:** `POST /oauth2/client/rotate-secret`

### Client Side

```ts
const { data, error } = await authClient.oauth2.client.rotateSecret({
    client_id, // required, The OAuth client's client_id
});
```

### Server Side

```ts
const data = await auth.api.rotateClientSecret({
    body: {
        client_id, // required, The OAuth client's client_id
    },
    // This endpoint requires session cookies.
    headers: await headers(),
});
```

### Type Definition

```ts
type rotateClientSecret = {
  /**
   * The OAuth client's client_id
   */
  client_id: string,
}
```

#### Delete Client [#delete-client]

To delete a user or organization's client, use the following endpoint:

**Endpoint:** `POST /oauth2/delete-client`

### Client Side

```ts
const { data, error } = await authClient.oauth2.deleteClient({
    client_id, // required, The OAuth client's client_id
});
```

### Server Side

```ts
const data = await auth.api.deleteOAuthClient({
    body: {
        client_id, // required, The OAuth client's client_id
    },
    // This endpoint requires session cookies.
    headers: await headers(),
});
```

### Type Definition

```ts
type deleteOAuthClient = {
  /**
   * The OAuth client's client_id
   */
  client_id: string,
}
```

### OAuth Consent [#oauth-consent]

Consent is required on all non-trusted clients, specifically those without `skip_consent`. The following endpoints allow users or `reference_id` manage their given consents.

#### Get Consent [#get-consent]

To obtain details of a specific consent, use the following endpoint:

**Endpoint:** `GET /oauth2/get-consent`

### Client Side

```ts
const { data, error } = await authClient.oauth2.getConsent({
    query: {
        id, // required, The consent id
    },
});
```

### Server Side

```ts
const data = await auth.api.getOAuthConsent({
    query: {
        id, // required, The consent id
    },
    // This endpoint requires session cookies.
    headers: await headers(),
});
```

### Type Definition

```ts
type getOAuthConsent = {
  /**
   * The consent id
   */
  id: string,
}
```

#### List Consent [#list-consent]

To obtain a list of user consents, use the following endpoint:

**Endpoint:** `GET /oauth2/get-consents`

### Client Side

```ts
const { data, error } = await authClient.oauth2.getConsents();
```

### Server Side

```ts
const data = await auth.api.getOAuthConsents({
    // This endpoint requires session cookies.
    headers: await headers(),
});
```

### Type Definition

```ts
type getOAuthConsents = {
}
```

#### Update Consent [#update-consent]

To update a specific consent, use the following endpoint:

**Endpoint:** `POST /oauth2/update-consent`

### Client Side

```ts
const { data, error } = await authClient.oauth2.updateConsent({
    id, // required, The consent id
    update, // required, The values to update
});
```

### Server Side

```ts
const data = await auth.api.updateOAuthClient({
    body: {
        id, // required, The consent id
        update, // required, The values to update
    },
    // This endpoint requires session cookies.
    headers: await headers(),
});
```

### Type Definition

```ts
type updateOAuthClient = {
  /**
   * The consent id
   */
  id: string,
  /**
   * The values to update
   */
  update: OAuthConsent,
}
```

#### Delete Consent [#delete-consent]

Revokes a user's consent for a specific client.

**Endpoint:** `POST /oauth2/delete-consent`

### Client Side

```ts
const { data, error } = await authClient.oauth2.deleteConsent({
    id, // required, The consent id
});
```

### Server Side

```ts
const data = await auth.api.deleteOAuthConsent({
    body: {
        id, // required, The consent id
    },
    // This endpoint requires session cookies.
    headers: await headers(),
});
```

### Type Definition

```ts
type deleteOAuthConsent = {
  /**
   * The consent id
   */
  id: string,
}
```

### Dynamic Registration Endpoint [#dynamic-registration-endpoint]

<Callout type="info">
  This endpoint supports [RFC7591](https://datatracker.ietf.org/doc/html/rfc7591) compliant client registration.
</Callout>

Once installed, you can utilize the OAuth Provider to manage authentication flows within your application.

After the client is created, you will receive a `client_id` and `client_secret` that you can display to the user. The `client_secret` can only be provided once, ensure the user saves it.

#### Setup [#setup]

To enable client registration set `allowDynamicClientRegistration: true` in your BetterAuth config.

```ts title="auth.ts"
oauthProvider({
  allowDynamicClientRegistration: true,
  // ... other options
})
```

To enable unauthenticated client registration which allows for dynamically registered public clients, additionally set `allowUnauthenticatedClientRegistration: true` in your auth config.

<Callout type="warn">
  Support for `allowUnauthenticatedClientRegistration` **will be deprecated** when the MCP protocol standardizes unauthenticated dynamic client registration. As of writing, both [Client ID Metadata Documents](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/991) and [`software_statement` and `jwks_uri`](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1032) are under debate.
</Callout>

```ts title="auth.ts"
oauthProvider({
  allowDynamicClientRegistration: true,
  allowUnauthenticatedClientRegistration: true,
  // ... other options
})
```

#### Basic Example [#basic-example]

To register a new OIDC client, use the `oauth2.register` method.

```ts
import { authClient } from "@/lib/auth-client"

const client = await authClient.oauth2.register({
  client_name: "My Client",
  redirect_uris: ["https://client.example.com/callback"],
});
```

For all endpoint parameters, see [RFC 7591 Registration](https://datatracker.ietf.org/doc/html/rfc7591#section-2).

Note the following parameters are not yet supported:

* `jwks`
* `jwks_uri`

### Authorize Endpoint [#authorize-endpoint]

An [OAuth 2.1 authorization endpoint](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13#name-authorization-endpoint). Since many of the details are not yet fully described, parts are adapted from the legacy [OAuth 2.0 Authorization Endpoint Section](https://datatracker.ietf.org/doc/html/rfc6749#section-3.1) but always implements the [differences from OAuth 2.0](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13#name-differences-from-oauth-20).

The Authorization Endpoint is the entry point for initiating an OAuth 2.1 authorization flows.

Important notes:

* In OAuth 2.1, only `response_type: "code"` is supported.
* `code_challenge_method: "plain"` will not be supported since this is a security vulnerability.
* All authorization responses (success and error) include the `iss` parameter for issuer validation ([RFC 9207](https://datatracker.ietf.org/doc/html/rfc9207)).

**State**

Clients should send a state value to mitigate cross-site request forgery (CSRF) attacks. This works by ensuring your client only responds to requests that your client initially requested.

Generate a state value from your client and store on your client such as in a secure, HTTP-only cookie or database.

The authorization server accepts requests without `state` for compatibility with OAuth and OpenID Connect, and echoes `state` back when it is provided. Better Auth's client helpers generate and validate `state` for you.

**Code Challenge**

Code challenges helps protect the authorization `code` returned from the authorization endpoint.

To do so, a code challenge is derived from a code verifier and sent in a [Proof Key for Code Exchange (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636) to the Authorization Server.

Now at your `redirect_uri` (ie callback), check to see if the returned state matches the initial state, use the `authorization_code` grant and original code verifier at the [Token Endpoint](#token-endpoint) to obtain the tokens.

### Token Endpoint [#token-endpoint]

By default, the token endpoint supports providing tokens for the following grants:

* "authorization\_code"
* "client\_credentials"
* "refresh\_token"

#### Authorization code grant [#authorization-code-grant]

The authorization code grant enables clients to obtain access user access tokens and optionally refresh tokens (with the "offline\_access" scope).

#### Client credentials grant [#client-credentials-grant]

The client credentials grant enables clients to obtain machines to obtain access tokens.

#### Refresh token grant [#refresh-token-grant]

The refresh token grant enables clients to update their access token without needing the user to login again.

This implementation currently issues a new refresh token for every refresh request.

### Consent Endpoint [#consent-endpoint]

Accept or deny user consent for a set of scopes. Note that when denying scopes, the consent cancels and pre-existing consent remains. To remove consent, delete that user's "oauthConsent" for that client.

**Endpoint:** `POST /oauth2/consent`

### Client Side

```ts
const { data, error } = await authClient.oauth2.consent({
    accept, // required, Accept or deny user consent for a set of scopes
    scope, // Space-separated list of accepted scopes. If not provided, the originally requested scopes are accepted.
});
```

### Server Side

```ts
const data = await auth.api.oauth2Consent({
    body: {
        accept, // required, Accept or deny user consent for a set of scopes
        scope, // Space-separated list of accepted scopes. If not provided, the originally requested scopes are accepted.
    },
    // This endpoint requires session cookies.
    headers: await headers(),
});
```

### Type Definition

```ts
type oauth2Consent = {
  /**
   * Accept or deny user consent for a set of scopes
   */
  accept: boolean,
  /**
   * Space-separated list of accepted scopes. If not provided, the originally requested scopes are accepted.
   */
  scope?: string,
}
```

### Continue Endpoint [#continue-endpoint]

Sign up registration pages must be [configured](#sign-up-account-screen) to perform account registration steps.
Account selection must be [configured](#select-account-screen) to perform account selection.
Post login must be [configured](#post-login-screen) to perform post login selection.

**Endpoint:** `POST /oauth2/continue`

### Client Side

```ts
const { data, error } = await authClient.oauth2.continue({
    selected, // Confirms an account was selected.
    created, // Confirms an account was registered
    postLogin, // Confirms completion of post login activity
});
```

### Server Side

```ts
const data = await auth.api.oauth2Continue({
    body: {
        selected, // Confirms an account was selected.
        created, // Confirms an account was registered
        postLogin, // Confirms completion of post login activity
    },
    // This endpoint requires session cookies.
    headers: await headers(),
});
```

### Type Definition

```ts
type oauth2Continue = {
  /**
   * Confirms an account was selected.
   */
  selected?: boolean,
  /**
   * Confirms an account was registered
   */
  created?: boolean,
  /**
   * Confirms completion of post login activity
   */
  postLogin?: boolean,
}
```

### Introspect Endpoint [#introspect-endpoint]

[RFC7662](https://datatracker.ietf.org/doc/html/rfc7662)-compliant Introspection.

This endpoint provides details of the provided token. If the token is additionally tied to a session, the endpoint will ensure the session is `active`.

To provide resource specific claims via `customAccessTokenClaims`, store the allowed resources that a confidential client can use in its `resources` field.

### Revoke Endpoint [#revoke-endpoint]

[RFC7009](https://datatracker.ietf.org/doc/html/rfc7009)-compliant Revocation.

This endpoint revokes the provided token.

* opaque `access_token`: immediately removes that `access_token` from the database. `refresh_token` is still valid.
* JWT `access_token`: verifies that token is safe to remove from client storage.
* `refresh_token`: removes all `access_tokens` granted using that `refresh_token` and removes the `refresh_token` to prevent further token issuance.

For an `access_token` type,

### End Session Endpoint [#end-session-endpoint]

[RP-initiated](https://openid.net/specs/openid-connect-rpinitiated-1_0.html)-compliant Logout

This endpoint allows specified trusted clients to logout remotely.

To allow rp-initiated logout, a trusted client must specifically be created to perform session logout.

```ts title="admin-create-oauth.ts"
import { auth } from "@/lib/auth"

await auth.api.adminCreateOAuthClient({
  headers,
  body: {
    redirect_uris: [redirectUri],
    enable_end_session: true, // [!code highlight]
  }
});
```

<Callout type="info">
  If `disableJwtPlugin: true`, public clients will never be able to logout using this endpoint since no `id_token` is sent.
</Callout>

### UserInfo Endpoint [#userinfo-endpoint]

The UserInfo Endpoint provides [OIDC](https://openid.net/specs/openid-connect-core-1_0.html)-compliant user information. Available at `/oauth2/userinfo`, the endpoint requires a valid access token with at least the scope `openid`.

```ts
// Example of how a client would use the UserInfo endpoint
const response = await fetch('https://your-domain.com/api/auth/oauth2/userinfo', {
  headers: {
    'Authorization': 'Bearer ACCESS_TOKEN'
  }
});

const userInfo = await response.json();
// userInfo contains user details based on the scopes granted
```

The UserInfo endpoint returns different claims based on the scopes that were granted during authorization:

* `openid`: Returns the user's ID (`sub` claim)
* `profile`: Returns `name`, `picture`, `given_name`, `family_name`
* `email`: Returns `email` and `email_verified`

The `customUserInfoClaims` function receives the user object, requested scopes array, and the passed access token, allowing you to add additional information to the response.

### Well-Known [#well-known]

#### OpenID Configuration [#openid-configuration]

Provides [OpenID Connect discovery metadata](https://openid.net/specs/openid-connect-discovery-1_0.html) at `{issuer}/.well-known/openid-configuration`.

This endpoint requires the scope `openid`.

The OAuth Provider plugin serves this endpoint automatically from the Better Auth handler. If you do not set a custom issuer, the issuer path is your basePath, such as `/api/auth`.

For issuers with paths, OpenID Connect uses path appending. For example, issuer `https://example.com/api/auth` uses `/api/auth/.well-known/openid-configuration`.

If your framework route does not forward this URL to `auth.handler`, add a route at the issuer path:

```ts title="[issuer-path]/.well-known/openid-configuration/route.ts"
import { oauthProviderOpenIdConfigMetadata } from "@better-auth/oauth-provider";
import { auth } from "@/lib/auth";

export const GET = oauthProviderOpenIdConfigMetadata(auth);
```

<Callout type="info">
  If you get a CORS issue when testing locally such as with the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector), this is due to the frontend calling the endpoint instead of the backend. Add `Access-Control-Allow-Methods": "GET"` and `"Access-Control-Allow-Origin": "*"` for testing.
</Callout>

#### OAuth Authorization Server [#oauth-authorization-server]

Provides [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414)-compliant metadata for the authorization server.

The OAuth Provider plugin serves both path-prefixed issuer aliases automatically from the Better Auth handler:

* `{issuer}/.well-known/oauth-authorization-server`
* `/.well-known/oauth-authorization-server/[issuer-path]`

For example, issuer `https://example.com/api/auth` can use `/api/auth/.well-known/oauth-authorization-server` or `/.well-known/oauth-authorization-server/api/auth`. Both return the same metadata when the request reaches `auth.handler`.

If your framework route does not forward one of these URLs to `auth.handler`, add a route and call the helper:

```ts title="/.well-known/oauth-authorization-server/[issuer-path]/route.ts"
import { oauthProviderAuthServerMetadata } from "@better-auth/oauth-provider";
import { auth } from "@/lib/auth";

export const GET = oauthProviderAuthServerMetadata(auth);
```

<Callout type="info">
  If you get a CORS issue when testing locally such as with the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector), this is due to the frontend calling the endpoint instead of the backend. Add `Access-Control-Allow-Methods": "GET"` and `"Access-Control-Allow-Origin": "*"` for testing.
</Callout>

## API Server [#api-server]

This section shows how your API should verify tokens received from your clients.

### Verification [#verification]

Verification can be performed using `verifyAccessToken` available through the `oauthProviderResourceClient` plugin or `better-auth/oauth2` package.

With `better-auth` package:

```ts title="api/[endpoint].ts"
import { verifyAccessToken } from "better-auth/oauth2";

export const GET = async (req: Request) => {
  const authorization = req.headers?.get("authorization") ?? undefined;
  const accessToken = authorization?.startsWith("Bearer ")
    ? authorization.replace("Bearer ", "")
    : authorization;
  const payload = await verifyAccessToken(
    accessToken, {
      verifyOptions: {
        issuer: "https://auth.example.com",
        audience: "https://api.example.com",
      },
      scopes: ["read:post"], // optional
    }
  );
  // ...continue
}
```

With `oauthProviderResourceClient` plugin:

```ts title="api/[endpoint].ts"
import { serverClient } from "@/lib/server-client";

export const POST = async (req: Request) => {
  const authorization = req.headers?.get("authorization") ?? undefined;
  const accessToken = authorization?.startsWith("Bearer ")
    ? authorization.replace("Bearer ", "")
    : authorization;
  const payload = await serverClient.verifyAccessToken(
    accessToken, {
      verifyOptions: {
        issuer: "https://auth.example.com",
        audience: "https://api.example.com",
      },
      scopes: ["write:post"], // optional
    }
  );
  // ...continue
}
```

#### JWT Verification [#jwt-verification]

* Verify the token is valid:
  * Validate the *signature* using the JWKS.
  * Check the `iss` (issuer) and `aud` (audience) claims.
  * Verify the `exp` (expiration) and (if sent) `nbf` claim.
* Validate the appropriate `scope` for each endpoint.

#### Opaque Access Tokens [#opaque-access-tokens]

* Send the received token to `/oauth2/introspect` and assert that `active: true` is returned.
* Validate the appropriate `scope` for each endpoint.

#### Recommendations [#recommendations]

The simplest approach is to *only accept JWT-formatted access tokens* for your API and deny opaque tokens.

**Benefits**:

* **Fast**: locally verifiable, no network call required.
* **Future-proof**: independent of the authorization server after issuance.
* **No client secret needed**: the API can validate tokens without confidential client credentials.

Accepting *opaque access tokens in addition to JWT tokens* is possible, but comes with trade-offs.

**Benefits**:

* Immediate token and client validation.
* Client does not require a `resource` parameter (depending on authorization server configuration).

**Drawbacks**:

* **DOS**: If the client is external (ie external APIs, MCP agents), opaque `access_token` verifications can overload your authorization server.
* **Performance**: Every received opaque `access_token` requires a network call to the introspection endpoint.
* **Secret required**: Introspection typically requires a `client_secret`, which public clients cannot safely provide.
  * NOTE: Introspection bearer token and Private Key JWT methods are not yet implemented.

### Scopes vs. Permissions [#scopes-vs-permissions]

* **Scopes** define what a client application *requests* on behalf of a user. They are usually coarse-grained labels included in an access token.
* **Permissions** define the fine-grained actions a user (or service) is actually allowed to perform on resources, typically enforced at the resource server.

In practice, you may also combine approaches depending on system complexity and how your resource server handles authorization.

**Scopes and Permissions are the Same**

Each scope directly represents a permission.

* Example: A scope `read:post` corresponds exactly to the permission `read:post`.

*Pros*:

* Simple to implement and reason about.
* No extra mapping logic required.

*Cons*:

* Access tokens can become large if permissions are very detailed, especially with JWTs.
* Limited flexibility for future, more granular permissions.

**Scopes and Permissions are Different**

Scopes represent high-level access categories, and each scope maps to one or more underlying permissions.

* **Example:** A scope `view:post` could map to:
  * `read:post:content`
  * `read:post:metadata` (but only for posts the user owns)

*Pros*:

* Flexible and scalable for complex systems.
* Tokens remain compact, since only scopes are included, not all permissions.

*Cons*:

* The resource server must resolve scopes into permissions for each request.
* Adds complexity to implementation and authorization checks.

## Configuration [#configuration]

### Redirect Screens [#redirect-screens]

During the OAuth flow, users are likely redirected between pages. For example, a user may start on a login screen then redirect to a consent screen before returning to the application. The following outlines possible login flows and configurations needed to provide each flow.

To process each redirect step in the login flow, we verify the signed query provided in the initial `/oauth2/authorize` redirect. All parameters sent to the authorize endpoint (including any custom ones), are signed and verified.

If your sign-in pages include custom page query parameters, they may coexist in the URL, but they should not be added to the signed `oauth_query`. The client plugin forwards only the parameters declared by the signed redirect.

If you utilize the Client Plugin `oauthProviderClient`, then the `oauth_query` parameter is automatically sent to every endpoint that requires it. If you have custom sign-in endpoints, you would need to manually add the window's signed query in the request body `oauth_query`. This should only include the signed query parameters.

#### Login Screen [#login-screen]

When a user is redirected to the OIDC provider for authentication, if they are not already logged in, they will be redirected to the login page. You can customize the login page by providing a `loginPage` option during initialization.

```ts title="auth.ts"
oauthProvider({
  loginPage: "/sign-in" // [!code highlight]
})
```

You don't need to handle anything from your side; when a new session is created, the plugin will handle continuing the authorization flow.

#### Consent Screen [#consent-screen]

When a user is redirected to the OIDC provider for authentication, they may be prompted to authorize the application to access their data.

**Note**: Trusted clients with `skipConsent: true` will bypass the consent screen entirely, providing a seamless experience for first-party applications.

```ts title="auth.ts"
oauthProvider({
  consentPage: "/consent" // [!code highlight]
})
```

The plugin will redirect the user to the specified path with `client_id` and `scope` query parameters. You can use this information to display a custom consent screen. Once the user consents, you can call `oauth2.consent` to complete the authorization.

```ts title="consent-page.ts"
import { authClient } from "@/lib/auth-client"

const res = await authClient.oauth2.consent({
	accept: true,
  // optional scopes accepted (if not sent, accepted scopes matches the original request)
  scope: "openid profile email"
});
```

#### Sign Up Account Screen [#sign-up-account-screen]

To direct users from the client to a sign up page using `prompt: create`, use `signup`.

```ts title="auth.ts"
oauthProvider({
  signUp: {
    page: "/sign-up", // [!code highlight]
  }
})
```

To stop sign in process to complete registration forms, use the `shouldRedirect` function.

```ts title="auth.ts"
import { userRegistered } from "@lib/registered";

oauthProvider({
  signUp: {
    page: "/sign-up",
    shouldRedirect: async ({ headers }) => { // [!code highlight]
      const isUserRegistered = await userRegistered(headers);
      return isUserRegistered ? false : "/setup";
    },
  }
})
```

#### Select Account Screen [#select-account-screen]

When a user is redirected to the select account page during authentication, they may be prompted to select an account before consenting. To enable account selection, you must add the following configuration to your settings.

The following example uses the multi-session plugin and automatically redirects to the select-account page if more than one session is logged in:

```ts title="auth.ts"
oauthProvider({
  selectAccount: {
    page: "/select-account", // [!code highlight]
    shouldRedirect: async ({ headers }) => { // [!code highlight]
      const allSessions = await auth.api.listDeviceSessions({
        headers,
      })
      return allSessions?.length >= 1;
    },
  }
})
```

The plugin will redirect the user to the `selectAccount.page`. This page should prompt for account selection and upon completion of selection, should call `oauth2Continue`.

```ts title="select-account.ts"
import { authClient } from "@/lib/auth-client"

await authClient.multiSession.setActive({
  sessionToken,
});
await client.oauth2.oauth2Continue({
  selected: true,
});
```

#### Post Login Screen [#post-login-screen]

If a requested scope requires an organization. You would need to provide all of the following options to tie the `reference_id` (ie organization id, team id) to the login flow. This step occurs post login and prior to consent.

The following example uses the organization plugin to automatically redirect to the select-organization page for organization specific scopes.

```ts title="auth.ts"
oauthProvider({
  scopes: ["openid", "profile", "email", "read:organization"]
  postLogin: {
    page: "/select-organization", // [!code highlight]
    shouldRedirect: async ({ session, scopes, headers }) => { // [!code highlight]
      const userOnlyScopes = ["openid", "profile", "email", "offline_access"];
      if (scopes.every((sc) => userOnlyScopes.includes(sc))) {
        return false;
      }
      const organizations = await auth.api.listOrganizations({
        headers,
      });
      return organizations.length > 1 || !(
        organizations.length === 1 && organizations.at(0)?.id === session.activeOrganizationId
      )
    },
    consentReferenceId: ({ session, scopes }) => { // [!code highlight]
      if (scopes.includes("read:organization")) {
        const activeOrganizationId = (session?.activeOrganizationId ?? undefined) as string | undefined;
        if (!activeOrganizationId) {
          throw new APIError("BAD_REQUEST", {
            error: "set_organization",
            error_description: "must set organization for these scopes",
          })
        }
        return activeOrganizationId;
      } else {
        return undefined;
      }
    },
  }
})
```

The plugin will redirect the user to the `postLogin.page` to provide a prompt for account selection. Upon completion, you should call `oauth2Continue`.

```ts title="select-organization.ts"
import { authClient } from "@/lib/auth-client"

await authClient.organization.setActive({
  organizationId,
});
await client.oauth2.oauth2Continue({
  postLogin: true,
});
```

### Cached Trusted Clients [#cached-trusted-clients]

For first-party applications and internal services, you can cache trusted clients for better performance. Values are cached in memory for all mentioned clients. Additionally, they prevent changes through the CRUD endpoints.

```ts title="auth.ts"
oauthProvider({
  // List of clientIds of the clients
  cachedTrustedClients: new Set([
    "internal-dashboard",
    "mobile-app",
  ]),
})
```

### Valid Audiences [#valid-audiences]

A list of valid audiences (ie resources) for this oauth server. If not specified, the default audience is the baseUrl. It is recommended to specify an audience other than the baseUrl such as your API.

```ts title="auth.ts"
oauthProvider({
  validAudiences: [
    "https://api.example.com",
    "https://api.example.com/mcp",
  ]
})
```

### Scopes [#scopes]

Scopes allow clients specific access to specific resources.
By default, we support the following scopes are supported:

* `openid`: Returns the user's ID (`sub` claim).
* `profile`: Returns name, picture, given\_name, family\_name
* `email`: Returns email and email\_verified
* `offline_access`: Returns a refresh token

The scopes configuration can contain as many or as few scopes as you wish! Note that `openid` is required to be considered an OIDC server, otherwise this is a standard OAuth 2.1 server. All supported scopes must be in this array.

```ts title="auth.ts"
oauthProvider({
  scopes: [ "openid", "profile", "offline_access", "read:post", "write:post" ],
})
```

### Claims [#claims]

Internally, we support the following claims are supported: \["sub", "iss", "aud", "exp", "iat", "sid", "scope", "azp"].

Id token and user info claims should be namespaced when possible to avoid potential future conflicts.

Claims added inside `customIdTokenClaims` and `customUserInfoClaims` should be added to the `advertisedMetadata.claims_supported` so clients can validate that claim received. In the following example, it would be the base claims plus `locale` and `https://example.com/org`.

Pro tip: these functions can may also throw errors such as a user is no longer a member of the organization or no longer has the requested permissions.

```ts title="auth.ts"
oauthProvider({
  // Attach claims to id tokens
  customIdTokenClaims: ({ user, scopes, metadata }) => {
    return {
      locale: "en-GB",
    };
  },
  // Attach claims to access tokens
  customAccessTokenClaims: ({ user, scopes, referenceId, resource, metadata }) => {
    return {
      "https://example.com/org": referenceId,
      "https://example.com/roles": ["editor"],
    };
  },
  // Additional user info claims
  customUserInfoClaims: ({ user, scopes, jwt }) => {
    return {
      locale: "en-GB",
    };
  },
})
```

#### Custom Token Response Fields [#custom-token-response-fields]

Unlike the claim callbacks above (which add data *inside* JWT payloads), `customTokenResponseFields` adds fields to the **token endpoint JSON response** alongside `access_token`, `token_type`, etc. Standard OAuth fields cannot be overridden.

```ts title="auth.ts"
oauthProvider({
  customTokenResponseFields: ({ grantType, user, scopes, metadata, verificationValue }) => {
    // Add tenant context for authorization_code grants
    if (grantType === "authorization_code" && verificationValue?.referenceId) {
      return { tenant_id: verificationValue.referenceId };
    }
    return {};
  },
})
```

The callback receives the grant type, user (undefined for `client_credentials`), scopes, parsed client metadata, and the verification value (only for `authorization_code` grants). It is called before any tokens are created, so throwing an error will not leave partially-applied state.

### Expirations [#expirations]

Each token type and grant type can independently can set a default expiration.

* `accessTokenExpiresIn` defaults 1 hour
* `m2mAccessTokenExpiresIn` defaults 1 hour
* `idTokenExpiresIn` defaults 10 hours
* `refreshTokenExpiresIn` defaults 30 days
* `codeExpiresIn` defaults 10 minutes

Additionally, Access Tokens can set lower expirations based on scopes. This is useful for higher-privilege scopes that require shorter expiration times. The earliest expiration will take precedence. If not specified, the default will take place. Note: values should be lower than the defaults `accessTokenExpiresIn` and `m2mAccessTokenExpiresIn`.

```ts title="auth.ts"
oauthProvider({
  scopeExpirations: {
    "write:payments": "5m",
    "read:payments": "30m",
  },
})
```

### Registration [#registration]

#### Dynamic Client Registration [#dynamic-client-registration]

Dynamic registration allows for authorized registration of both public and confidential clients.

```ts title="auth.ts"
oauthProvider({
  allowDynamicClientRegistration: true, // [!code highlight]
})
```

Unauthenticated client registration additionally allows for public clients (never confidential) to register without an authorization header. This is especially useful for an MCP to dynamically register themselves as a public client.

```ts title="auth.ts"
oauthProvider({
  allowDynamicClientRegistration: true,
  allowUnauthenticatedClientRegistration: true, // [!code highlight]
})
```

<Callout type="warn">
  Support for `allowUnauthenticatedClientRegistration` **will be deprecated** when the MCP protocol standardizes unauthenticated dynamic client registration. As of writing, both [Client ID Metadata Documents](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/991) and [`software_statement` and `jwks_uri`](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1032) are under debate.
</Callout>

#### Dynamic Client Registration Expiration [#dynamic-client-registration-expiration]

You can set an expiration time for how long a dynamically registered confidential client should last for. By default, dynamically registered confidential clients do not expire.

```ts title="auth.ts"
oauthProvider({
  allowDynamicClientRegistration: true,
  clientRegistrationClientSecretExpiration: "30d", // [!code highlight]
})
```

#### Dynamic Client Registration Scopes [#dynamic-client-registration-scopes]

To set a list of default scopes for newly registered clients when scopes parameter is not sent, set the `clientRegistrationDefaultScopes` field. All scopes must be defined in `scopes`.

```ts title="auth.ts"
oauthProvider({
  scopes: ["reader", "editor"],
  clientRegistrationDefaultScopes: ["reader"], // [!code highlight]
})
```

To also set a list of allowed scopes for newly registered clients when scopes parameter is not sent, set the `clientRegistrationAllowedScopes` field. These are **in addition** to the `clientRegistrationDefaultScopes`. All scopes must be defined in `scopes`.

```ts title="auth.ts"
oauthProvider({
  scopes: ["reader", "editor"],
  clientRegistrationDefaultScopes: ["reader"],
  clientRegistrationAllowedScopes: ["editor"], // [!code highlight]
})
```

### PKCE Configuration [#pkce-configuration]

PKCE (Proof Key for Code Exchange) is a security mechanism that prevents authorization code interception attacks. This plugin follows the OAuth 2.1 specification, which requires PKCE by default for all authorization code flows.

#### Default Behavior [#default-behavior]

By default, PKCE is required for all clients. This provides maximum security and follows OAuth 2.1 best practices.

**PKCE is always required for:**

* Public clients (native/user-agent-based applications)
* Any authorization request with the `offline_access` scope (refresh tokens)

#### Per-Client PKCE Configuration [#per-client-pkce-configuration]

Individual clients can opt-out of PKCE requirement during registration if needed for compatibility:

```ts title="register-client.ts"
// Register a confidential client that doesn't support PKCE
const response = await auth.api.createOAuthClient({
  headers,
  body: {
    client_name: 'Legacy Backend Service',
    redirect_uris: ['https://app.example.com/callback'],
    token_endpoint_auth_method: 'client_secret_post',
    grant_types: ['authorization_code'],
    require_pkce: false, // Opt-out of PKCE requirement
  }
});
```

The `require_pkce` field:

* Defaults to `true` (PKCE required)
* Only applies to confidential clients
* Ignored for public clients (PKCE always required)
* Ignored for `offline_access` scope (PKCE always required)

**When to use `require_pkce: false`:**

* Migrating from OAuth 2.0 with legacy confidential clients that don't support PKCE
* Backend-to-backend integrations where updating the client is not feasible
* Temporary compatibility during a phased migration

**Recommendation:** Keep PKCE enabled (default) whenever possible. PKCE provides defense-in-depth even for confidential clients.

#### Migrating from oidc-provider [#migrating-from-oidc-provider]

If you're migrating from the deprecated `oidc-provider` plugin and have confidential clients that don't support PKCE:

1. **For legacy clients, opt-out per-client:**
   Set `require_pkce: false` when registering clients that cannot be updated to support PKCE.

2. **For new clients, use PKCE:**
   New client registrations should always use PKCE (the default) for better security.

3. **Phase out non-PKCE clients:**
   Plan to upgrade or replace clients that don't support PKCE over time.

4. **Monitor usage:**
   Track which clients have `require_pkce: false` for migration planning.

#### Security Considerations [#security-considerations]

PKCE prevents authorization code interception attacks. Even for confidential clients with client\_secret authentication, PKCE provides additional security:

* **Defense in depth**: Multiple security layers
* **Protection against misconfiguration**: Accidental secret exposure
* **Future-proof**: Aligns with OAuth 2.1 best practices

Only disable PKCE for confidential clients when absolutely necessary for legacy compatibility.

### Organizations [#organizations]

OAuth Clients are tied to either a user or `reference_id` at registration and is immutable. If you are utilizing the [organization plugin](/docs/1.6/plugins/organization), you must ensure that the [`activeOrganizationId`](/docs/1.6/plugins/organization#active-organization) is set on your active session when you create new clients.

```ts title="auth.ts"
oauthProvider({
  clientReference: ({ session }) => {
    return (session?.activeOrganizationId as string | undefined) ?? undefined;
  },
})
```

To set user-specific permissions and roles on tokens see [Claims](#claims).

### Client CRUD Privileges [#client-crud-privileges]

To determine whether a logged in user has the ability to perform specific actions in client creation, you can utilize the `clientPrivileges` configuration setting. By default, CRUD actions are allowed for users with matching `userId` or `clientReference`.

The following is a basic example that allows all OAuth Client CRUD actions for organization owners assuming ordinary users cannot create clients:

```ts title="auth.ts"
oauthProvider({
  clientPrivileges: async ({ action, headers, user, session }) => {
    if (!session?.activeOrganizationId) return false;
    const { data: member } = await auth.api.getActiveMember({
      headers,
    });
    return member.role === 'owner';
  },
})
```

### Storage [#storage]

By default all secrets are `hashed` by default on the database. This helps protect the `client_secret` in case of a database leak.

* **storeClientSecret**: the storage method of application `client_secrets`. Only when `disableJwtPlugin: true`, the client secret shall rather be `encrypted`.
* **storeTokens**: the storage method of token values, specifically session refresh tokens and opaque access tokens.

### Rate Limiting [#rate-limiting]

The OAuth Provider includes built-in rate limiting for all OAuth endpoints to protect against abuse and denial-of-service attacks.

<Callout type="info">
  Rate limiting is **per-IP per-endpoint**. Each client IP address has its own rate limit counter for each endpoint. Rate limits reset after the window period expires.
</Callout>

<Callout type="warn">
  These rate limits only apply when Better Auth's global rate limiting is enabled. By default, rate limiting is only enabled in production. See [Rate Limiting](/docs/1.6/concepts/rate-limit) for global configuration.
</Callout>

**Default limits:**

| Endpoint             | Window | Max Requests |
| -------------------- | ------ | ------------ |
| `/oauth2/token`      | 60s    | 20           |
| `/oauth2/authorize`  | 60s    | 30           |
| `/oauth2/introspect` | 60s    | 100          |
| `/oauth2/revoke`     | 60s    | 30           |
| `/oauth2/register`   | 60s    | 5            |
| `/oauth2/userinfo`   | 60s    | 60           |

You can customize the rate limits for each endpoint:

```ts title="auth.ts"
oauthProvider({
  rateLimit: {
    token: { window: 60, max: 20 },        // 20 requests per minute
    authorize: { window: 60, max: 30 },    // 30 requests per minute
    introspect: { window: 60, max: 100 },  // 100 requests per minute
    revoke: { window: 60, max: 30 },       // 30 requests per minute
    register: { window: 60, max: 5 },      // 5 requests per minute
    userinfo: { window: 60, max: 60 },     // 60 requests per minute
  },
})
```

To remove the per-endpoint rate limit override and fall back to global rate limits, set it to `false`:

```ts title="auth.ts"
oauthProvider({
  rateLimit: {
    introspect: false, // Uses global rate limits instead of per-endpoint limits
  },
})
```

<Callout type="info">
  Setting an endpoint to `false` removes the OAuth Provider's stricter per-endpoint limit. The endpoint will still be subject to Better Auth's global rate limiting if enabled.
</Callout>

### Refresh Token Customization [#refresh-token-customization]

You can choose to format your session tokens in a different string format using the `formatRefreshToken`.

These functions allow you to add additional functionality on the refresh token itself such as refresh token encryption.

Example with change in refresh token format with backwards compatibility with original token-only format:

```ts title="auth.ts"
oauthProvider({
  formatRefreshToken: {
    encrypt: (token, sessionId) => {
      const res = sessionId ? `1.${token}.${sessionId}` : token;
      return res;
    },
    decrypt: (token) => {
      const tokenSplit = token.split('.');
      if (tokenSplit.length === 3 && tokenSplit.at(0) === '1') {
        return {
          token: tokenSplit.at(1),
          sessionId: tokenSplit.at(2),
        };
      }
      return { token };
    },
  }
})
```

Pseudocode for a token encryption method:

```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { CompactEncrypt, compactDecrypt } from 'jose'
import { oauthProvider } from "@better-auth/oauth-provider"; 

const secret = "SOME_SECRET_OR_KEY"
const alg = "A256KW"
const enc = "A256GCM"

const auth = betterAuth({
  plugins: [
    oauthProvider({
    formatRefreshToken: {
      encrypt: (token, sessionId) {
        const value = JSON.stringify({
          sessionId,
          token,
        });
        const jwe = await new CompactEncrypt(Buffer.from(value))
          .setProtectedHeader({ alg, enc })
          .encrypt(secret);
        return jwe;
      },
      decrypt: (token) {
        const { plaintext } = await compactDecrypt(token, secret);
        const payload = new TextDecoder().decode(plaintext);
        return JSON.parse(payload);
      },
    }
  })
]
})
```

### Advertised Metadata [#advertised-metadata]

The metadata endpoint can be customized so that the publicized scopes and claims differ from those which the server can deliver. This can prevent showcasing all your supported scopes and claims on your metadata endpoint.

All scopes inside the advertisedMetadata section MUST be listed in `scopes` otherwise initialization will fail.

#### Scopes [#scopes-1]

```ts title="auth.ts"
oauthProvider({
  scopes: ["openid", "profile", "email", "offline_access", "read:post"],
  advertisedMetadata: {
    scopes_supported: ["openid", "profile", "read:post"],
  },
})
```

#### Claims [#claims-1]

Claims are in addition to the internally supported claims which are automatically determined by `scopes`. Claims are only applicable for the OIDC (ie "openid" scope).

```ts title="auth.ts"
oauthProvider({
  advertisedMetadata: {
    claims_supported: ["https://example.com/roles"],
  },
})
```

### Disable JWT Plugin [#disable-jwt-plugin]

By default, access and id tokens can be issued and verified through the JWT plugin.

You can disable the JWT requirement in which access tokens will always be opaque and id tokens are always signed in `HS256` using the `client_secret`. Note that disabling the JWT Plugin is still OIDC compliant, `/userinfo` still works and signed `id_token` is still provided.

Key Differences:

* Providing a valid `resource` will always provide you with an opaque access token instead of an JWT formatted token.
* `id_token` is not returned for public clients, but the `access_token` returned can still utilize the `/oauth2/userinfo` endpoint to obtain the user data.
* `id_token` for a confidential client is signed by their `client_secret`.

```ts title="auth.ts"
oauthProvider({
  disableJwtPlugin: true, // [!code highlight]
})
```

### Pairwise Subject Identifiers [#pairwise-subject-identifiers]

By default, the `sub` (subject) claim in tokens uses the user's internal ID, which is the same across all clients. This is the **public** subject type per [OIDC Core Section 8](https://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes).

You can enable **pairwise** subject identifiers so each client receives a unique, unlinkable `sub` for the same user. This prevents relying parties from correlating users across services.

```ts title="auth.ts"
oauthProvider({
  pairwiseSecret: "your-256-bit-secret", // [!code highlight]
})
```

When `pairwiseSecret` is configured, the server advertises both `"public"` and `"pairwise"` in the discovery endpoint's `subject_types_supported`. Clients opt in by setting `subject_type: "pairwise"` at registration.

#### Per-Client Configuration [#per-client-configuration]

```ts title="register-client.ts"
const response = await auth.api.createOAuthClient({
  headers,
  body: {
    client_name: 'Privacy-Sensitive App',
    redirect_uris: ['https://app.example.com/callback'],
    token_endpoint_auth_method: 'client_secret_post',
    subject_type: 'pairwise', // Enable pairwise sub for this client
  }
});
```

#### How It Works [#how-it-works]

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 always 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 `sub` appears in:

* `id_token`
* `/oauth2/userinfo` response
* Token introspection (`/oauth2/introspect`)

JWT access tokens always use the real user ID as `sub`, since resource servers may need to look up users directly.

<Callout type="warn">
  **Limitations:**

  * `sector_identifier_uri` is not yet supported. All `redirect_uris` for a pairwise client must share the same host. Clients with redirect URIs on different hosts will be rejected at registration.
  * `pairwiseSecret` must be at least 32 characters long.
  * Rotating `pairwiseSecret` will change all pairwise `sub` values, breaking existing RP sessions. Treat this secret as permanent once set.
</Callout>

### MCP [#mcp]

You can easily make your APIs [MCP-compatible](https://modelcontextprotocol.io/specification/draft/basic/authorization) simply by adding a resource server which directs users to this OAuth 2.1 authorization server.

<Callout type="info">
  If you are using "openid" and confidential MCP clients, you cannot disable the JWT plugin since `id_token` verification may not necessarily be supported via a `client_secret`.
</Callout>

#### Installation [#installation-1]

<Steps>
  <Step>
    ### Ensure Well Known Paths are correct [#ensure-well-known-paths-are-correct]

    See [well-known endpoints](#well-known).
  </Step>

  <Step>
    ### Add Resource Server Client [#add-resource-server-client]

    (Optional) If you have your auth configuration available locally, add the configuration as a parameter to the client to fill in these values and warn you about configuration errors. You can always override these values in the function call. If this is not supplied, typescript will guide you with the minimal configuration values needed.

    ```ts title="server-client.ts"
    import { auth } from "@/lib/auth";
    import { createAuthClient } from "better-auth/client";
    import { oauthProviderResourceClient } from "@better-auth/oauth-provider/resource-client"

    export const serverClient = createAuthClient({
      plugins: [oauthProviderResourceClient(auth)], // auth optional
    });
    ```
  </Step>

  <Step>
    ### Add OAuth Protected Resource Metadata to your API [#add-oauth-protected-resource-metadata-to-your-api]

    ```ts title="/.well-known/oauth-protected-resource/[resource-path]/route.ts"
    import { serverClient } from "@/lib/server-client";

    export const GET = async () => {
      const metadata = await serverClient.getProtectedResourceMetadata({
        resource: "https://api.example.com", // `aud` claim
        authorization_servers: ["https://auth.example.com"],
      })

      return new Response(JSON.stringify(metadata), {
        headers: {
          "Content-Type": "application/json",
          "Cache-Control":
            "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400",
        },
      });
    };
    ```
  </Step>

  <Step>
    If you use `allowUnauthenticatedClientRegistration`, you must ensure that your API Server is a confidential client itself:

    ```ts
    await auth.api.createOAuthClient({
      headers,
      body: {
        redirect_uris: [redirectUri],
      }
    });
    ```

    These values should be used in the verify options `remoteVerify.clientId` and `remoteVerify.clientSecret`. Additionally, `remoteVerify.introspectUrl` would be something like `${BASE_URL}/${AUTH_PATH}/oauth2/introspect`.

    <Callout type="info">
      If you choose to not support `allowUnauthenticatedClientRegistration` (and only `allowDynamicClientRegistration`), the MCP client (ie. ChatGPT, Anthropic, Gemini) would need to allow you to put in a public client\_id in their UI or at runtime while chatting with the AI.
    </Callout>
  </Step>

  <Step>
    ### Handle MCP Errors for your API [#handle-mcp-errors-for-your-api]

    Always verify against a specified `audience`, the default will compare against all `validAudiences` or `baseUrl`.

    * Using the client `verifyAccessToken` function

    See [Verification](#verification) for verification examples.

    * With auth available, use the client `verifyAccessToken` function to automatically determine endpoints

    ```ts title="api/[endpoint].ts"
    import { auth } from "@/lib/auth";
    import { serverClient } from "@/lib/server-client";

    export const GET = async (req: Request) => {
      const authorization = req.headers?.get("authorization") ?? undefined;
      const accessToken = authorization?.startsWith("Bearer ")
        ? authorization.replace("Bearer ", "")
        : authorization;
      const payload = await serverClient.verifyAccessToken(
        accessToken, {
          verifyOptions: {
            audience: "https://api.example.com",
          }
        }
      );
      // ...continue
    }
    ```

    * Using `mcpHandler` helper

    ```ts title="api/[transport]/route.ts"
    import { createMcpHandler } from "mcp-handler";
    import { mcpHandler } from "@better-auth/oauth-provider";
    import { z } from "zod";

    const handler = mcpHandler({
      jwksUrl: "https://auth.example.com/api/auth/jwks",
      verifyOptions: {
        issuer: "https://auth.example.com",
        audience: "https://api.example.com",
      },
    }, (req, jwt) => {
      return createMcpHandler(
        (server) => {
          server.registerTool(
            "echo", {
              description: "Echo a message",
              inputSchema: {
                message: z.string(),
              },
            },
            async ({ message }) => {
              return {
                content: [
                  {
                    type: "text",
                    text: `Echo: ${message}${
                      jwt?.sub
                        ? ` for user ${jwt.sub}`
                        : ""
                    }`,
                  },
                ],
              };
            }
          );
        }, {
          serverInfo: {
            name: "demo-better-auth",
            version: "1.0.0",
          }
        }, {
          basePath: "/api",
          maxDuration: 60,
          verboseLogs: true,
        }
      )(req);
    });

    export { handler as GET, handler as POST, handler as DELETE };
    ```
  </Step>
</Steps>

## Schema [#schema]

The OAuth Provider plugin adds the following tables to the database:

### OAuth Client [#oauth-client-1]

Table Name: `oauthClient`



<DatabaseTable name="oauthClient" fields="oauthClientTableFields" />

### OAuth Refresh Token [#oauth-refresh-token]

Table Name: `oauthRefreshToken`



<DatabaseTable name="oauthRefreshToken" fields="oauthRefreshTokenTableFields" />

### OAuth Access Token [#oauth-access-token]

Table Name: `oauthAccessToken`



<DatabaseTable name="oauthAccessToken" fields="oauthAccessTokenTableFields" />

### OAuth Consent [#oauth-consent-1]

Table Name: `oauthConsent`



<DatabaseTable name="oauthConsent" fields="oauthConsentTableFields" />

## Options [#options]

### Prefix [#prefix]

Add a `prefix` to opaque access tokens, refresh tokens, or client secrets. This is useful for Secret Scanners (ie. [GitHub Secret Scanners](https://docs.github.com/code-security/secret-scanning), [GitGuardian](https://www.gitguardian.com/solutions/secrets-scanning), [Trufflehog](https://github.com/trufflesecurity/trufflehog)) that may rely on the prefix to help determine the token format.

We recommend to add a prefix to each of the following prior to your first production deployment. Once deployed consider them immutable, otherwise the following generate functions as specified:

The following are available under the `prefix` configuration setting:

* **opaqueAccessToken**: `string | undefined` - add a prefix onto opaque access tokens. If previously deployed, utilize `generateOpaqueAccessToken` to perform this functionality instead.
* **refreshToken**: `string | undefined` - add a prefix onto refresh tokens.  If previously deployed, utilize `generateRefreshToken` to perform this functionality instead.
* **clientSecret**:: `string | undefined` - add a prefix onto client secrets.  If previously deployed, utilize `generateClientSecret` to perform this functionality instead.

## Optimizations [#optimizations]

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

## Migrations [#migrations]

### From OIDC Provider Plugin [#from-oidc-provider-plugin]

See [OIDC Provider Plugin](/docs/1.6/plugins/oidc-provider) for the previous implementation.

#### Configuration [#configuration-1]

* **`idTokenExpiresIn`** now defaults to `10 hours` (previously `1 hour` through `accessTokenExpiresIn`)
* **`refreshTokenExpiresIn`** now defaults to `30 days` (previously `7 days`)
* **`advertisedMetadata`** (previously `metadata`) no longer supports changing metadata fields to prevent accidental misconfiguration.
* **`clientRegistrationDefaultScopes`** (previously `defaultScope`) is now in array format instead of a space-separated string
* **`consentPage`** is now required
* **`getConsentHTML`** is removed in favor of the `consentPage` as raw html is not a response type supported by the authorize endpoint in OAuth
* **`requirePKCE`** (global option) is removed. PKCE is now required by default per OAuth 2.1. Individual clients can opt-out using `require_pkce: false` during registration if needed for legacy compatibility.
* **`allowPlainCodeChallengeMethod`** is removed as the `plain` code challenge is considered less secure than the default `S256` method
* **`customUserInfoClaims`** (previously `getAdditionalUserInfoClaim`) passes the jwt payload instead of the client of the access token used in the request.
* **`storeClientSecret`** now defaults to `hashed`, or `encrypted` if `disableJwtPlugin: true` (previously `plain`).
* JWT plugin now is enabled by default. To disable the plugin, set `disableJwtPlugin: true`.
* Authorization query `code_challenge_method` "S256" must be in caps as described by OAuth 2.1

#### Database [#database]

##### Table: `oauthClient` [#table-oauthclient]

Previously `oauthApplication`

* If `storeClientSecret` was unset or `plain`, you must hash all the stored `clientSecret` values into its "SHA-256" representation then convert it into base64Url format or use another storage method specified by `storeClientSecret`.
  The following function will convert a `plain` representation into the default hash:

```ts
import { createHash } from "@better-auth/utils/hash";
import { base64Url } from "@better-auth/utils/base64";

const defaultHasher = async (value: string) => {
	const hash = await createHash("SHA-256").digest(
		new TextEncoder().encode(value),
	);
	const hashed = base64Url.encode(new Uint8Array(hash), {
		padding: false,
	});
	return hashed;
};
```

* `type` field is no longer a required field. Instead, the schema requires `public` of type `boolean`. Migrate with the following rules:
  * Clients with `type: "public"`: set `type: undefined`, `public: true`, and `clientSecret: undefined`
  * Clients with `type: "native"`: set `public: true` and `clientSecret: undefined`
  * Clients with `type: "user-agent-based"`: set `public: true` and `clientSecret: undefined`
  * Clients with `clientSecret: undefined`: set `public: true`
* `redirectURLs` renamed to `redirectUris`
* `requirePkce` field added (optional, defaults to `true`). For existing confidential clients that don't support PKCE, set `requirePkce: false`.
* `metadata` is now stored in database as individual fields instead of a JSON object. Parse the metadata into their respective fields. The OIDC plugin did not utilize this field but this OAuth plugin may utilize them in the future.

##### Table: `oauthAccessToken` [#table-oauthaccesstoken]

Option 1 (simple):

You may choose to opt-out of this table conversion with minimal impact. By doing so, users of the existing application will simply need to login again. Simply delete the existing table `oauthAccessToken`.

Option 2 (more complex):

Migrate all tables (you may need to create a clone of `oauthAccessToken` into `oauthRefreshToken` before a migration).

* Convert `oauthAccessToken` with `refreshToken` field into a new `oauthRefreshToken` entry.

```ts
{
  token: defaultHasher(refreshToken),
  expiresAt: refreshTokenExpiresAt,
  clientId: clientId,
  scopes: scopes,
  userId: userId,
  createdAt: createdAt,
  updatedAt: updatedAt,
}
```

* Keep `oauthAccessToken` but reference new `oauthRefreshToken`.

```ts
{
  token: defaultHasher(accessToken),
  expiresAt: accessTokenExpiresAt,
  clientId: clientId,
  scopes: scopes,
  refreshId: oauthRefreshToken.id, // `undefined` if no refreshToken
  createdAt: createdAt,
  updatedAt: updatedAt,
}
```

### From MCP Plugin [#from-mcp-plugin]

See [MCP Plugin](/docs/1.6/plugins/mcp) for prior MCP-specific endpoints.

The MCP endpoints moved from `/mcp` to the `/oauth2` equivalent.

* `/oauth2/authorize` (previously `/mcp/authorize`)
* `/oauth2/token` (previously `/mcp/token`)
* `/oauth2/register` (previously `/mcp/register`)
* `/mcp/get-session` removed as not OAuth 2 compliant, use `/oauth2/introspect` instead
* `/.well-known/oauth-protected-resource` removed, use the helper `mcpHandler` (or manually with the server `api.oAuth2introspectVerify` or the resource client `verifyAccessToken`)
* Database changes are equivalent to the [From OIDC Provider Plugin](#from-oidc-provider-plugin) section.

