# Source: https://www.better-auth.com/llms.txt/docs/plugins/oidc-provider.md

# OIDC Provider

Open ID Connect plugin for Better Auth that allows you to have your own OIDC provider.



<Callout type="warn">
  This plugin will soon be deprecated in favor of the [OAuth Provider Plugin](/docs/plugins/oauth-provider).
</Callout>

The **OIDC Provider Plugin** enables you to build and manage your own OpenID Connect (OIDC) provider, granting full control over user authentication without relying on third-party services like Okta or Azure AD. It also allows other services to authenticate users through your OIDC provider.

**Key Features**:

* **Client Registration**: Register clients to authenticate with your OIDC provider.
* **Dynamic Client Registration**: Allow clients to register dynamically.
* **Trusted Clients**: Configure hard-coded trusted clients with optional consent bypass.
* **Authorization Code Flow**: Support the Authorization Code Flow.
* **Public Clients**: Support public clients for SPA, mobile apps, CLI tools, etc.
* **JWKS Endpoint**: Publish a JWKS endpoint to allow clients to verify tokens. (Not fully implemented)
* **Refresh Tokens**: Issue refresh tokens and handle access token renewal using the `refresh_token` grant.
* **OAuth Consent**: Implement OAuth consent screens for user authorization, with an option to bypass consent for trusted applications.
* **UserInfo Endpoint**: Provide a UserInfo endpoint for clients to retrieve user details.

<Callout type="warn">
  This plugin is in active development and may not be suitable for production use. Please report any issues or bugs on [GitHub](https://github.com/better-auth/better-auth).
</Callout>

## Installation

<Steps>
  <Step>
    ### 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 { oidcProvider } from "better-auth/plugins";

    const auth = betterAuth({
        plugins: [oidcProvider({
            loginPage: "/sign-in", // path to the login page
            // ...other options
        })]
    })
    ```
  </Step>

  <Step>
    ### Migrate the Database

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

    <Tabs items={["migrate", "generate"]}>
      <Tab value="migrate">
        <CodeBlockTabs defaultValue="npm" groupId="persist-install" persist>
          <CodeBlockTabsList>
            <CodeBlockTabsTrigger value="npm">
              npm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="pnpm">
              pnpm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="yarn">
              yarn
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="bun">
              bun
            </CodeBlockTabsTrigger>
          </CodeBlockTabsList>

          <CodeBlockTab value="npm">
            ```bash
            npx @better-auth/cli migrate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="pnpm">
            ```bash
            pnpm dlx @better-auth/cli migrate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="yarn">
            ```bash
            yarn dlx @better-auth/cli migrate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="bun">
            ```bash
            bun x @better-auth/cli migrate
            ```
          </CodeBlockTab>
        </CodeBlockTabs>
      </Tab>

      <Tab value="generate">
        <CodeBlockTabs defaultValue="npm" groupId="persist-install" persist>
          <CodeBlockTabsList>
            <CodeBlockTabsTrigger value="npm">
              npm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="pnpm">
              pnpm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="yarn">
              yarn
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="bun">
              bun
            </CodeBlockTabsTrigger>
          </CodeBlockTabsList>

          <CodeBlockTab value="npm">
            ```bash
            npx @better-auth/cli generate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="pnpm">
            ```bash
            pnpm dlx @better-auth/cli generate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="yarn">
            ```bash
            yarn dlx @better-auth/cli generate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="bun">
            ```bash
            bun x @better-auth/cli generate
            ```
          </CodeBlockTab>
        </CodeBlockTabs>
      </Tab>
    </Tabs>

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

  <Step>
    ### Add the Client Plugin

    Add the OIDC client plugin to your auth client config.

    ```ts
    import { createAuthClient } from "better-auth/client";
    import { oidcClient } from "better-auth/client/plugins"
    const authClient = createAuthClient({
        plugins: [oidcClient({
            // Your OIDC configuration
        })]
    })
    ```
  </Step>
</Steps>

## Usage

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

### Register a New Client

To register a new OIDC client, use the `oauth2.register` method on the client or `auth.api.registerOAuthApplication` on the server.


### Client Side

```ts
const { data, error } = await authClient.oauth2.register({
    redirect_uris, // client.example.com/callback"]
    token_endpoint_auth_method: client_secret_basic, // optional
    grant_types, // optional
    response_types, // optional
    client_name: My App, // optional
    client_uri: https://client.example.com, // optional
    logo_uri: https://client.example.com/logo.png, // optional
    scope: profile email, // optional
    contacts, // optional
    tos_uri: https://client.example.com/tos, // optional
    policy_uri: https://client.example.com/policy, // optional
    jwks_uri: https://client.example.com/jwks, // optional
    jwks, // optional
});
```

### Server Side

```ts
const data = await auth.api.registerOAuthApplication({
    body: {
        redirect_uris, // client.example.com/callback"]
        token_endpoint_auth_method: client_secret_basic, // optional
        grant_types, // optional
        response_types, // optional
        client_name: My App, // optional
        client_uri: https://client.example.com, // optional
        logo_uri: https://client.example.com/logo.png, // optional
        scope: profile email, // optional
        contacts, // optional
        tos_uri: https://client.example.com/tos, // optional
        policy_uri: https://client.example.com/policy, // optional
        jwks_uri: https://client.example.com/jwks, // optional
        jwks, // optional
    }
});
```

### Type Definition

```ts
type registerOAuthApplication = {
      /**
       * A list of redirect URIs. 
       */
      redirect_uris: string[] = ["https://client.example.com/callback"]
      /**
       * The authentication method for the token endpoint. 
       */
      token_endpoint_auth_method?: "none" | "client_secret_basic" | "client_secret_post" = "client_secret_basic"
      /**
       * The grant types supported by the application. 
       */
      grant_types?: ("authorization_code" | "implicit" | "password" | "client_credentials" | "refresh_token" | "urn:ietf:params:oauth:grant-type:jwt-bearer" | "urn:ietf:params:oauth:grant-type:saml2-bearer")[] = ["authorization_code"]
      /**
       * The response types supported by the application. 
       */
      response_types?: ("code" | "token")[] = ["code"]
      /**
       * The name of the application. 
       */
      client_name?: string = "My App"
      /**
       * The URI of the application. 
       */
      client_uri?: string = "https://client.example.com"
      /**
       * The URI of the application logo. 
       */
      logo_uri?: string = "https://client.example.com/logo.png"
      /**
       * The scopes supported by the application. Separated by spaces. 
       */
      scope?: string = "profile email"
      /**
       * The contact information for the application. 
       */
      contacts?: string[] = ["admin@example.com"]
      /**
       * The URI of the application terms of service. 
       */
      tos_uri?: string = "https://client.example.com/tos"
      /**
       * The URI of the application privacy policy. 
       */
      policy_uri?: string = "https://client.example.com/policy"
      /**
       * The URI of the application JWKS. 
       */
      jwks_uri?: string = "https://client.example.com/jwks"
      /**
       * The JWKS of the application. 
       */
      jwks?: Record<string, any> = {"keys": [{"kty": "RSA", "alg": "RS256", "use": "sig", "n": "...", "e": "..."
}
```


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

Once the application is created, you will receive a `client_id` and `client_secret` that you can display to the user.

### Trusted Clients

For first-party applications and internal services, you can configure trusted clients directly in your OIDC provider configuration. Trusted clients bypass database lookups for better performance and can optionally skip consent screens for improved user experience.

```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { oidcProvider } from "better-auth/plugins";

const auth = betterAuth({
    plugins: [
      oidcProvider({
        loginPage: "/sign-in",
        trustedClients: [
            {
                clientId: "internal-dashboard",
                clientSecret: "secure-secret-here",
                name: "Internal Dashboard",
                type: "web",
                redirectUrls: ["https://dashboard.company.com/auth/callback"],
                disabled: false,
                skipConsent: true, // Skip consent for this trusted client
                metadata: { internal: true }
            },
            {
                clientId: "mobile-app",
                clientSecret: "mobile-secret", 
                name: "Company Mobile App",
                type: "native",
                redirectUrls: ["com.company.app://auth"],
                disabled: false,
                skipConsent: false, // Still require consent if needed
                metadata: {}
            }
        ]
    })]
})
```

### UserInfo Endpoint

The OIDC Provider includes a UserInfo endpoint that allows clients to retrieve information about the authenticated user. This endpoint is available at `/oauth2/userinfo` and requires a valid access token.

<Endpoint path="/oauth2/userinfo" method="GET" />

#### Server-Side Usage

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

const userInfo = await auth.api.oAuth2userInfo({
  headers: {
    authorization: "Bearer ACCESS_TOKEN"
  }
});
// userInfo contains user details based on the scopes granted
```

#### Client-Side Usage (For Third-Party OAuth Clients)

Third-party OAuth clients can call the UserInfo endpoint using standard HTTP requests:

```ts title="external-client.ts"
const response = await fetch('https://your-domain.com/api/auth/oauth2/userinfo', {
  headers: {
    'Authorization': 'Bearer ACCESS_TOKEN'
  }
});

const userInfo = await response.json();
```

**Returned claims based on scopes:**

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

#### Custom Claims

The `getAdditionalUserInfoClaim` function receives the user object, requested scopes array, and the client, allowing you to conditionally include claims based on the scopes granted during authorization. These additional claims will be included in both the UserInfo endpoint response and the ID token.

```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { oidcProvider } from "better-auth/plugins";

export const auth = betterAuth({
    plugins: [
        oidcProvider({
            loginPage: "/sign-in",
            getAdditionalUserInfoClaim: async (user, scopes, client) => {
                const claims: Record<string, any> = {};
                
                // Add custom claims based on scopes
                if (scopes.includes("profile")) {
                    claims.department = user.department;
                    claims.job_title = user.jobTitle;
                }
                
                // Add claims based on client metadata
                if (client.metadata?.includeRoles) {
                    claims.roles = user.roles;
                }
                
                return claims;
            }
        })
    ]
});
```

### 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. This is known as the consent screen. By default, Better Auth will display a sample consent screen. You can customize the consent screen by providing a `consentPage` option during initialization.

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

```ts title="auth.ts"
import { betterAuth } from "better-auth";

export const auth = betterAuth({
    plugins: [oidcProvider({
        consentPage: "/path/to/consent/page"
    })]
})
```

The plugin will redirect the user to the specified path with `consent_code`, `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.

<Endpoint path="/oauth2/consent" method="POST" />

The consent endpoint supports two methods for passing the consent code:

**Method 1: URL Parameter**

```ts title="consent-page.ts"
// Get the consent code from the URL
const params = new URLSearchParams(window.location.search);

// Submit consent with the code in the request body
const consentCode = params.get('consent_code');
if (!consentCode) {
	throw new Error('Consent code not found in URL parameters');
}

const res = await client.oauth2.consent({
	accept: true, // or false to deny
	consent_code: consentCode,
});
```

**Method 2: Cookie-Based**

```ts title="consent-page.ts"
// The consent code is automatically stored in a signed cookie
// Just submit the consent decision
const res = await client.oauth2.consent({
	accept: true, // or false to deny
	// consent_code not needed when using cookie-based flow
});
```

Both methods are fully supported. The URL parameter method works well with mobile apps and third-party contexts, while the cookie-based method provides a simpler implementation for web applications.

### Handling Login

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"
import { betterAuth } from "better-auth";

export const auth = betterAuth({
    plugins: [oidcProvider({
        loginPage: "/sign-in"
    })]
})
```

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

## Configuration

### OIDC Metadata

Customize the OIDC metadata by providing a configuration object during initialization.

```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { oidcProvider } from "better-auth/plugins";

export const auth = betterAuth({
    plugins: [oidcProvider({
        metadata: {
            issuer: "https://your-domain.com",
            authorization_endpoint: "/custom/oauth2/authorize",
            token_endpoint: "/custom/oauth2/token",
            // ...other custom metadata
        }
    })]
})
```

### JWKS Endpoint

The OIDC Provider plugin can integrate with the JWT plugin to provide asymmetric key signing for ID tokens verifiable at a JWKS endpoint.

To make your plugin OIDC compliant, you **MUST** disable the `/token` endpoint, the OAuth equivalent is located at `/oauth2/token` instead.

```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { oidcProvider } from "better-auth/plugins";
import { jwt } from "better-auth/plugins";

export const auth = betterAuth({
    disabledPaths: [
        "/token",
    ],
    plugins: [
        jwt(), // Make sure to add the JWT plugin
        oidcProvider({
            useJWTPlugin: true, // Enable JWT plugin integration
            loginPage: "/sign-in",
            // ... other options
        })
    ]
})
```

<Callout type="info">
  When `useJWTPlugin: false` (default), ID tokens are signed with the application secret.
</Callout>

### Dynamic Client Registration

If you want to allow clients to register dynamically, you can enable this feature by setting the `allowDynamicClientRegistration` option to `true`.

```ts title="auth.ts"
const auth = betterAuth({
    plugins: [oidcProvider({
        allowDynamicClientRegistration: true,
    })]
})
```

This will allow clients to register using the `/register` endpoint to be publicly available.

## Schema

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

### OAuth Application

Table Name: `oauthApplication`

<DatabaseTable
  fields={[
 {
    name: "id",
    type: "string",
    description: "Database ID of the OAuth client",
    isPrimaryKey: true
 },
  { 
    name: "clientId", 
    type: "string", 
    description: "Unique identifier for each OAuth client",
    isPrimaryKey: true
  },
  { 
    name: "clientSecret", 
    type: "string", 
    description: "Secret key for the OAuth client. Optional for public clients using PKCE.",
    isOptional: true
  },
  { 
    name: "name", 
    type: "string", 
    description: "Name of the OAuth client",
    isRequired: true
  },
  { 
    name: "redirectUrls",
    type: "string", 
    description: "Comma-separated list of redirect URLs",
    isRequired: true
  },
  { 
    name: "metadata", 
    type: "string",
    description: "Additional metadata for the OAuth client",
    isOptional: true
  },
  { 
    name: "type", 
    type: "string",
    description: "Type of OAuth client (e.g., web, mobile)",
    isRequired: true
  },
  { 
    name: "disabled", 
    type: "boolean", 
    description: "Indicates if the client is disabled",
    isRequired: true
  },
  { 
    name: "userId", 
    type: "string", 
    description: "ID of the user who owns the client. (optional)",
    isOptional: true,
    references: { model: "user", field: "id" }
  },
  { 
    name: "createdAt", 
    type: "Date", 
    description: "Timestamp of when the OAuth client was created" 
  },
 {
    name: "updatedAt",
    type: "Date",
    description: "Timestamp of when the OAuth client was last updated"
 }
]}
/>

### OAuth Access Token

Table Name: `oauthAccessToken`

<DatabaseTable
  fields={[
  {
    name: "id",
    type: "string",
    description: "Database ID of the access token",
    isPrimaryKey: true
 },
  { 
    name: "accessToken", 
    type: "string", 
    description: "Access token issued to the client",
  },
  { 
    name: "refreshToken", 
    type: "string", 
    description: "Refresh token issued to the client",
    isRequired: true
  },
  { 
    name: "accessTokenExpiresAt", 
    type: "Date", 
    description: "Expiration date of the access token",
    isRequired: true
  },
  { 
    name: "refreshTokenExpiresAt", 
    type: "Date", 
    description: "Expiration date of the refresh token",
    isRequired: true
  },
  { 
    name: "clientId", 
    type: "string", 
    description: "ID of the OAuth client",
    isForeignKey: true,
    references: { model: "oauthApplication", field: "clientId" }
  },
  { 
    name: "userId", 
    type: "string", 
    description: "ID of the user associated with the token",
    isForeignKey: true,
    references: { model: "user", field: "id" }
  },
  { 
    name: "scopes", 
    type: "string", 
    description: "Comma-separated list of scopes granted",
    isRequired: true
  },
  { 
    name: "createdAt", 
    type: "Date", 
    description: "Timestamp of when the access token was created" 
  },
  {
    name: "updatedAt",
    type: "Date",
    description: "Timestamp of when the access token was last updated"
  }
]}
/>

### OAuth Consent

Table Name: `oauthConsent`

<DatabaseTable
  fields={[
  { 
    name: "id", 
    type: "string", 
    description: "Database ID of the consent",
    isPrimaryKey: true
  },
  { 
    name: "userId", 
    type: "string", 
    description: "ID of the user who gave consent",
    isForeignKey: true,
    references: { model: "user", field: "id" }
  },
  { 
    name: "clientId", 
    type: "string", 
    description: "ID of the OAuth client",
    isForeignKey: true,
    references: { model: "oauthApplication", field: "clientId" }
  },
  { 
    name: "scopes", 
    type: "string", 
    description: "Comma-separated list of scopes consented to",
    isRequired: true
  },
  { 
    name: "consentGiven", 
    type: "boolean", 
    description: "Indicates if consent was given",
    isRequired: true
  },
  { 
    name: "createdAt", 
    type: "Date", 
    description: "Timestamp of when the consent was given" 
  },
  {
    name: "updatedAt",
    type: "Date",
    description: "Timestamp of when the consent was last updated"
  }
]}
/>

## Options

**allowDynamicClientRegistration**: `boolean` - Enable or disable dynamic client registration.

**metadata**: `OIDCMetadata` - Customize the OIDC provider metadata.

**loginPage**: `string` - Path to the custom login page.

**consentPage**: `string` - Path to the custom consent page.

**trustedClients**: `(Client & { skipConsent?: boolean })[]` - Array of trusted clients that are configured directly in the provider options. These clients bypass database lookups and can optionally skip consent screens.

**getAdditionalUserInfoClaim**: `(user: User, scopes: string[], client: Client) => Record<string, any>` - Function to get additional user info claims.

**useJWTPlugin**: `boolean` - When `true`, ID tokens are signed using the JWT plugin's asymmetric keys. When `false` (default), ID tokens are signed with HMAC-SHA256 using the application secret.

**schema**: `AuthPluginSchema` - Customize the OIDC provider schema.