OIDC Provider

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.

This plugin is in active development and may not be suitable for production use. Please report any issues or bugs on GitHub.

Installation

Mount the Plugin

Add the OIDC plugin to your auth config. See OIDC Configuration on how to configure the plugin.

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
    })]
})

Migrate the Database

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

npx @better-auth/cli migrate

See the Schema section to add the fields manually.

Add the Client Plugin

Add the OIDC client plugin to your auth client config.

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

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.

Simple Example

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

Full Method

POST
/oauth2/register
const { data, error } = await authClient.oauth2.register({
    redirect_uris: ["https://client.example.com/callback"], // required
    token_endpoint_auth_method: "client_secret_basic",
    grant_types: ["authorization_code"],
    response_types: ["code"],
    client_name: "My App",
    client_uri: "https://client.example.com",
    logo_uri: "https://client.example.com/logo.png",
    scope: "profile email",
    contacts: ["[email protected]"],
    tos_uri: "https://client.example.com/tos",
    policy_uri: "https://client.example.com/policy",
    jwks_uri: "https://client.example.com/jwks",
    jwks: {"keys": [{"kty": "RSA", "alg": "RS256", "use": "sig", "n": "...", "e": "..."}]},
    metadata: {"key": "value"},
    software_id: "my-software",
    software_version: "1.0.0",
    software_statement,
});
PropDescriptionType
redirect_uris
A list of redirect URIs.
string[]
token_endpoint_auth_method?
The authentication method for the token endpoint.
"none" | "client_secret_basic" | "client_secret_post"
grant_types?
The grant types supported by the application.
("authorization_code" | "implicit" | "password" | "client_credentials" | "refresh_token" | "urn:ietf:params:oauth:grant-type:jwt-bearer" | "urn:ietf:params:oauth:grant-type:saml2-bearer")[]
response_types?
The response types supported by the application.
("code" | "token")[]
client_name?
The name of the application.
string
client_uri?
The URI of the application.
string
logo_uri?
The URI of the application logo.
string
scope?
The scopes supported by the application. Separated by spaces.
string
contacts?
The contact information for the application.
string[]
tos_uri?
The URI of the application terms of service.
string
policy_uri?
The URI of the application privacy policy.
string
jwks_uri?
The URI of the application JWKS.
string
jwks?
The JWKS of the application.
Record<string, any>
metadata?
The metadata of the application.
Record<string, any>
software_id?
The software ID of the application.
string
software_version?
The software version of the application.
string
software_statement?
The software statement of the application.
string

This endpoint supports RFC7591 compliant client registration.

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

This Endpoint support RFC7591 compliant client registration.

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.

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.

GET
/oauth2/userinfo
client-app.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:

  • 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

The getAdditionalUserInfoClaim function receives the user object and the requested scopes array, 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.

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.

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 a client_id and scope query parameter. You can use this information to display a custom consent screen. Once the user consents, you can call oauth2.consent to complete the authorization.

POST
/oauth2/consent
server.ts
const res = await client.oauth2.consent({
	accept: true, // or false to deny
});

The client_id and other necessary information are stored in the browser cookie, so you don't need to pass them in the request. If they don't exist in the cookie, the consent method will return an error.

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.

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.

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 proper asymmetric key signing for ID tokens. When enabled, ID tokens will be signed using RSA/EdDSA keys and can be verified using the JWKS endpoint.

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

When useJWTPlugin: false (default), ID tokens are signed with the application secret.

Dynamic Client Registration

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

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

Field NameTypeKeyDescription
idstringDatabase ID of the OAuth client
clientIdstringUnique identifier for each OAuth client
clientSecretstringSecret key for the OAuth client. Optional for public clients using PKCE.
namestring-Name of the OAuth client
redirectURLsstring-Comma-separated list of redirect URLs
metadatastringAdditional metadata for the OAuth client
typestring-Type of OAuth client (e.g., web, mobile)
disabledboolean-Indicates if the client is disabled
userIdstringID of the user who owns the client. (optional)
createdAtDate-Timestamp of when the OAuth client was created
updatedAtDate-Timestamp of when the OAuth client was last updated

OAuth Access Token

Table Name: oauthAccessToken

Field NameTypeKeyDescription
idstringDatabase ID of the access token
accessTokenstring-Access token issued to the client
refreshTokenstring-Refresh token issued to the client
accessTokenExpiresAtDate-Expiration date of the access token
refreshTokenExpiresAtDate-Expiration date of the refresh token
clientIdstringID of the OAuth client
userIdstringID of the user associated with the token
scopesstring-Comma-separated list of scopes granted
createdAtDate-Timestamp of when the access token was created
updatedAtDate-Timestamp of when the access token was last updated

Table Name: oauthConsent

Field NameTypeKeyDescription
idstringDatabase ID of the consent
userIdstringID of the user who gave consent
clientIdstringID of the OAuth client
scopesstring-Comma-separated list of scopes consented to
consentGivenboolean-Indicates if consent was given
createdAtDate-Timestamp of when the consent was given
updatedAtDate-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[]) => 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.