Skip to main content
This plugin will soon be deprecated in favor of a newer OAuth Provider plugin. New projects should check the Better Auth documentation for the most up-to-date provider plugin.
The OIDC Provider plugin enables you to build and manage your own OpenID Connect (OIDC) provider using Better Auth. Other services can authenticate users through your OIDC provider instead of relying on third-party services like Okta or Azure AD. Key capabilities:
  • Client registration (static trusted clients and dynamic registration)
  • Authorization Code Flow
  • Refresh token support
  • OAuth consent screens (with bypass support for trusted apps)
  • UserInfo endpoint
  • JWKS endpoint integration via the JWT plugin
  • Custom claims
This plugin is in active development. Report any issues on GitHub.

Installation

1

Add the plugin to your auth config

auth.ts
import { betterAuth } from "better-auth"
import { oidcProvider } from "better-auth/plugins"

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

Migrate the database

npx auth migrate
3

Add the client plugin

auth-client.ts
import { createAuthClient } from "better-auth/client"
import { oidcClient } from "better-auth/client/plugins"

const authClient = createAuthClient({
  plugins: [
    oidcClient(),
  ],
})

Registering clients

Dynamic registration

Clients can register via the /oauth2/register endpoint (RFC 7591):
const result = await authClient.oauth2.register({
  redirect_uris: ["https://client.example.com/callback"],
  client_name: "My App",
  grant_types: ["authorization_code"],
  response_types: ["code"],
  token_endpoint_auth_method: "client_secret_basic",
  scope: "openid profile email",
})

// Save the returned client_id and client_secret
console.log(result.client_id, result.client_secret)
Dynamic registration requires authentication by default. To allow public registration:
oidc Provider({
  allowDynamicClientRegistration: true,
})

Trusted clients (static configuration)

For first-party applications, configure trusted clients directly. They bypass database lookups and can skip the consent screen:
auth.ts
oidcProvider({
  loginPage: "/sign-in",
  trustedClients: [
    {
      clientId: "internal-dashboard",
      clientSecret: "secure-secret",
      name: "Internal Dashboard",
      type: "web",
      redirectUrls: ["https://dashboard.company.com/auth/callback"],
      skipConsent: true,  // bypass consent for this trusted client
      disabled: false,
      metadata: { internal: true },
    },
    {
      clientId: "mobile-app",
      clientSecret: "mobile-secret",
      name: "Company Mobile App",
      type: "native",
      redirectUrls: ["com.company.app://auth"],
      skipConsent: false,
    },
  ],
})

OIDC endpoints

The plugin exposes these standard OIDC endpoints:
EndpointDescription
GET /api/auth/oauth2/authorizeAuthorization endpoint
POST /api/auth/oauth2/tokenToken endpoint
GET /api/auth/oauth2/userinfoUserInfo endpoint
POST /api/auth/oauth2/consentConsent submission
POST /api/auth/oauth2/registerDynamic client registration
GET /api/auth/.well-known/openid-configurationOIDC discovery document

UserInfo endpoint

The UserInfo endpoint returns claims based on the granted scopes:
ScopeClaims returned
openidsub (user ID)
profilename, picture, given_name, family_name
emailemail, email_verified
Server-side:
const userInfo = await auth.api.oAuth2userInfo({
  headers: { authorization: "Bearer ACCESS_TOKEN" },
})
External client:
const response = await fetch("https://your-domain.com/api/auth/oauth2/userinfo", {
  headers: { Authorization: "Bearer ACCESS_TOKEN" },
})
const userInfo = await response.json()

Custom claims

auth.ts
oidcProvider({
  loginPage: "/sign-in",
  getAdditionalUserInfoClaim: async (user, scopes, client) => {
    const claims: Record<string, any> = {}

    if (scopes.includes("profile")) {
      claims.department = user.department
      claims.job_title = user.jobTitle
    }

    if (client.metadata?.includeRoles) {
      claims.roles = user.roles
    }

    return claims
  },
})
Custom claims appear in both the UserInfo response and the ID token. By default, Better Auth shows a built-in consent screen. Customize it with a consentPage path:
auth.ts
oidcProvider({
  consentPage: "/oauth/consent",
})
Better Auth redirects to this path with consent_code, client_id, and scope query parameters. After the user consents, call:
// Method 1: pass consent_code from URL parameter
const params = new URLSearchParams(window.location.search)
await authClient.oauth2.consent({
  accept: true,
  consent_code: params.get("consent_code"),
})

// Method 2: cookie-based (simpler for web apps)
await authClient.oauth2.consent({
  accept: true,
})
Trusted clients with skipConsent: true bypass the consent screen entirely.

Handling login

When users are not signed in and reach the authorization endpoint, they are redirected to loginPage. After a new session is created, the plugin automatically continues the authorization flow.
auth.ts
oidcProvider({
  loginPage: "/sign-in",  // redirect here if user is not logged in
})

JWKS integration

Combine with the JWT plugin for asymmetric ID token signing:
auth.ts
import { betterAuth } from "better-auth"
import { jwt, oidcProvider } from "better-auth/plugins"

const auth = betterAuth({
  disabledPaths: ["/token"],  // disable JWT plugin's /token (conflicts with /oauth2/token)
  plugins: [
    jwt(),
    oidcProvider({
      useJWTPlugin: true,       // sign ID tokens with JWT plugin's keys
      loginPage: "/sign-in",
    }),
  ],
})
When useJWTPlugin is false (default), ID tokens are signed with HMAC-SHA256 using the application secret.

Customize OIDC metadata

auth.ts
oidcProvider({
  metadata: {
    issuer: "https://your-domain.com",
    authorization_endpoint: "/custom/oauth2/authorize",
    token_endpoint: "/custom/oauth2/token",
  },
})

Schema

oauthApplication

FieldTypeDescription
idstringPrimary key
clientIdstringUnique OAuth client identifier
clientSecretstringClient secret (optional for public clients)
namestringApplication name
redirectUrlsstringComma-separated redirect URLs
typestringClient type (web, native, etc.)
disabledbooleanWhether the client is disabled
userIdstringOwner user ID (optional)
iconstringApplication icon URL
metadatastringAdditional metadata (JSON)
createdAtDateCreation timestamp
updatedAtDateLast update timestamp

oauthAccessToken

FieldTypeDescription
accessTokenstringThe access token
refreshTokenstringThe refresh token
accessTokenExpiresAtDateAccess token expiration
refreshTokenExpiresAtDateRefresh token expiration
clientIdstringAssociated OAuth client
userIdstringAssociated user
scopesstringGranted scopes (comma-separated)

oauthConsent

FieldTypeDescription
userIdstringUser who gave consent
clientIdstringOAuth client
scopesstringConsented scopes (comma-separated)
consentGivenbooleanWhether consent was granted