Skip to main content
The Generic OAuth plugin provides a flexible way to integrate authentication with any OAuth 2.0 or OpenID Connect (OIDC) provider. Use it to add social login, enterprise IdP authentication, or any custom OAuth flow.

Installation

1

Add the plugin to your auth config

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

export const auth = betterAuth({
  plugins: [
    genericOAuth({
      config: [
        {
          providerId: "my-provider",
          clientId: process.env.PROVIDER_CLIENT_ID,
          clientSecret: process.env.PROVIDER_CLIENT_SECRET,
          discoveryUrl: "https://auth.example.com/.well-known/openid-configuration",
        },
      ],
    }),
  ],
})
2

Add the client plugin

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

export const authClient = createAuthClient({
  plugins: [
    genericOAuthClient()
  ]
})

Usage

Initiate sign-in

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

await authClient.signIn.oauth2({
  providerId: "my-provider",
  callbackURL: "/dashboard",
  errorCallbackURL: "/error",
  newUserCallbackURL: "/welcome",  // optional, for new users
  scopes: ["openid", "email", "profile"], // override default scopes
})
Add an OAuth provider to an existing user’s account:
await authClient.oauth2.link({
  providerId: "my-provider",
  callbackURL: "/settings/accounts",
})

Callback URL

The plugin automatically mounts a callback handler at:
{baseURL}/api/auth/oauth2/callback/{providerId}
Configure your OAuth provider to use this URL as the redirect URI.

Pre-configured providers

Better Auth ships with pre-configured helpers for popular providers:
auth.ts
import { betterAuth } from "better-auth"
import {
  genericOAuth,
  auth0,
  hubspot,
  keycloak,
  line,
  microsoftEntraId,
  okta,
  slack,
  patreon,
} from "better-auth/plugins"

export const auth = betterAuth({
  plugins: [
    genericOAuth({
      config: [
        auth0({
          clientId: process.env.AUTH0_CLIENT_ID,
          clientSecret: process.env.AUTH0_CLIENT_SECRET,
          domain: process.env.AUTH0_DOMAIN, // e.g. dev-xxx.eu.auth0.com
        }),
        hubspot({
          clientId: process.env.HUBSPOT_CLIENT_ID,
          clientSecret: process.env.HUBSPOT_CLIENT_SECRET,
          scopes: ["oauth", "contacts"],
        }),
        keycloak({
          clientId: process.env.KEYCLOAK_CLIENT_ID,
          clientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
          issuer: process.env.KEYCLOAK_ISSUER, // https://my-domain/realms/MyRealm
        }),
        microsoftEntraId({
          clientId: process.env.MS_APP_ID,
          clientSecret: process.env.MS_CLIENT_SECRET,
          tenantId: process.env.MS_TENANT_ID, // GUID, "common", "organizations", or "consumers"
        }),
        okta({
          clientId: process.env.OKTA_CLIENT_ID,
          clientSecret: process.env.OKTA_CLIENT_SECRET,
          issuer: process.env.OKTA_ISSUER, // https://dev-xxxxx.okta.com/oauth2/default
        }),
        slack({
          clientId: process.env.SLACK_CLIENT_ID,
          clientSecret: process.env.SLACK_CLIENT_SECRET,
        }),
        patreon({
          clientId: process.env.PATREON_CLIENT_ID,
          clientSecret: process.env.PATREON_CLIENT_SECRET,
        }),
        // LINE supports multiple channels for different countries
        line({
          providerId: "line-jp",
          clientId: process.env.LINE_JP_CLIENT_ID,
          clientSecret: process.env.LINE_JP_CLIENT_SECRET,
        }),
      ],
    }),
  ],
})

Configuration options

Each provider configuration object supports:
interface GenericOAuthConfig {
  // Required
  providerId: string           // unique identifier for this provider
  clientId: string             // OAuth client ID
  clientSecret: string         // OAuth client secret

  // Provider endpoints (auto-discovered if discoveryUrl is set)
  discoveryUrl?: string        // OIDC discovery document URL
  authorizationUrl?: string    // authorization endpoint
  tokenUrl?: string            // token endpoint
  userInfoUrl?: string         // userinfo endpoint
  issuer?: string              // expected issuer (for validation)

  // Behavior
  scopes?: string[]            // scopes to request
  redirectURI?: string         // custom redirect URI
  responseType?: string        // defaults to "code"
  responseMode?: string        // "query" | "form_post"
  prompt?: string              // controls auth experience
  pkce?: boolean               // enable PKCE (default: false)
  accessType?: string          // use "offline" for refresh tokens
  requireIssuerValidation?: boolean  // strict issuer check
  disableImplicitSignUp?: boolean    // require explicit sign-up intent
  disableSignUp?: boolean            // block all new sign-ups
  overrideUserInfo?: boolean         // update user info on each sign-in

  // Custom functions
  getToken?: (params: { code: string; redirectURI: string }) => Promise<OAuth2Tokens>
  getUserInfo?: (tokens: OAuth2Tokens) => Promise<User | null>
  mapProfileToUser?: (profile: Record<string, any>) => Record<string, any>
  authorizationUrlParams?: Record<string, string> | ((ctx) => Record<string, string>)
  tokenUrlParams?: Record<string, string> | ((ctx) => Record<string, string>)

  // Auth method for token requests: "basic" | "post" (default: "post")
  authentication?: "basic" | "post"
}

Advanced usage

Custom token exchange

For providers with non-standard token endpoints:
genericOAuth({
  config: [
    {
      providerId: "custom-provider",
      clientId: process.env.CUSTOM_CLIENT_ID,
      clientSecret: process.env.CUSTOM_CLIENT_SECRET,
      authorizationUrl: "https://provider.example.com/oauth/authorize",
      scopes: ["profile", "email"],
      getToken: async ({ code, redirectURI }) => {
        // Some providers use GET instead of POST
        const response = await fetch(
          `https://provider.example.com/oauth/token?` +
            `client_id=${process.env.CUSTOM_CLIENT_ID}&` +
            `client_secret=${process.env.CUSTOM_CLIENT_SECRET}&` +
            `code=${code}&redirect_uri=${redirectURI}&grant_type=authorization_code`,
          { method: "GET" }
        )
        const data = await response.json()
        return {
          accessToken: data.access_token,
          refreshToken: data.refresh_token,
          accessTokenExpiresAt: new Date(Date.now() + data.expires_in * 1000),
          scopes: data.scope?.split(" ") ?? [],
          raw: data,  // preserve provider-specific fields
        }
      },
    },
  ],
})

Custom user info fetching

genericOAuth({
  config: [
    {
      providerId: "custom-provider",
      // ...
      getUserInfo: async (tokens) => {
        const response = await fetch("https://provider.example.com/api/me", {
          headers: { Authorization: `Bearer ${tokens.accessToken}` },
        })
        const data = await response.json()
        return {
          id: data.user_id,
          email: data.email_address,
          name: data.display_name,
          image: data.avatar_url,
          emailVerified: data.email_verified,
        }
      },
    },
  ],
})

Profile field mapping

genericOAuth({
  config: [
    {
      providerId: "custom-provider",
      // ...
      mapProfileToUser: async (profile) => ({
        firstName: profile.given_name,
        lastName: profile.family_name,
        department: profile.custom_department,
      }),
    },
  ],
})

Accessing raw token data

The tokens object includes a raw field with the original token response:
getUserInfo: async (tokens) => {
  // Access provider-specific fields
  const userId = tokens.raw?.provider_user_id as string
  const customField = tokens.raw?.custom_provider_field as string
  return { id: userId, /* ... */ }
}

Security: issuer validation

Better Auth validates the OAuth provider’s issuer to protect against mix-up attacks (RFC 9207).
// Auto-discovery (issuer fetched from discovery document)
genericOAuth({
  config: [{
    providerId: "my-provider",
    discoveryUrl: "https://auth.example.com/.well-known/openid-configuration",
    clientId: "...",
    clientSecret: "...",
  }],
})

// Manual issuer with strict validation
genericOAuth({
  config: [{
    providerId: "secure-provider",
    discoveryUrl: "https://auth.example.com/.well-known/openid-configuration",
    clientId: "...",
    clientSecret: "...",
    requireIssuerValidation: true,  // reject if iss parameter is missing
  }],
})
ScenariorequireIssuerValidationResult
iss matches expectedSuccess
iss doesn’t matchissuer_mismatch error
iss missingfalse (default)Success
iss missingtrueissuer_missing error
For maximum security with modern OIDC providers (Google, Auth0, Okta), enable requireIssuerValidation: true.