> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/better-auth/better-auth/llms.txt
> Use this file to discover all available pages before exploring further.

# Generic OAuth

> Connect Better Auth to any OAuth 2.0 or OIDC provider with a unified interface and pre-configured helpers for popular providers.

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

<Steps>
  <Step>
    ### Add the plugin to your auth config

    ```ts title="auth.ts" theme={null}
    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",
            },
          ],
        }),
      ],
    })
    ```
  </Step>

  <Step>
    ### Add the client plugin

    ```ts title="auth-client.ts" theme={null}
    import { createAuthClient } from "better-auth/client"
    import { genericOAuthClient } from "better-auth/client/plugins"

    export const authClient = createAuthClient({
      plugins: [
        genericOAuthClient()
      ]
    })
    ```
  </Step>
</Steps>

## Usage

### Initiate sign-in

```ts theme={null}
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
})
```

### Link an OAuth account

Add an OAuth provider to an existing user’s account:

```ts theme={null}
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:

```ts title="auth.ts" theme={null}
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:

```ts theme={null}
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:

```ts theme={null}
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

```ts theme={null}
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

```ts theme={null}
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:

```ts theme={null}
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](https://datatracker.ietf.org/doc/html/rfc9207)).

```ts theme={null}
// 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
  }],
})
```

| Scenario               | `requireIssuerValidation` | Result                  |
| ---------------------- | ------------------------- | ----------------------- |
| `iss` matches expected | —                         | Success                 |
| `iss` doesn’t match    | —                         | `issuer_mismatch` error |
| `iss` missing          | `false` (default)         | Success                 |
| `iss` missing          | `true`                    | `issuer_missing` error  |

<Note>
  For maximum security with modern OIDC providers (Google, Auth0, Okta), enable `requireIssuerValidation: true`.
</Note>
