通用 OAuth

通用 OAuth 插件提供了一种灵活的方式,将身份验证集成到任何 OAuth 提供商。它支持 OAuth 2.0 和 OpenID Connect (OIDC) 流程,使你可以轻松地将社交登录或自定义 OAuth 身份验证添加到你的应用中。

【The Generic OAuth plugin provides a flexible way to integrate authentication with any OAuth provider. It supports both OAuth 2.0 and OpenID Connect (OIDC) flows, allowing you to easily add social login or custom OAuth authentication to your application.】

安装

【Installation】

将插件添加到你的认证配置

要使用通用 OAuth 插件,请将其添加到你的认证配置中。

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

export const auth = betterAuth({
    // ... other config options
    plugins: [
        genericOAuth({ 
            config: [ 
                { 
                    providerId: "provider-id", 
                    clientId: "test-client-id", 
                    clientSecret: "test-client-secret", 
                    discoveryUrl: "https://auth.example.com/.well-known/openid-configuration", 
                    // ... other config options
                }, 
                // Add more providers as needed
            ] 
        }) 
    ]
})

添加客户端插件

在你的认证客户端实例中包含通用 OAuth 客户端插件。

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

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

用法

【Usage】

通用 OAuth 插件提供了用于启动 OAuth 流程和处理回调的端点。使用方法如下:

【The Generic OAuth plugin provides endpoints for initiating the OAuth flow and handling the callback. Here's how to use them:】

启动 OAuth 登录

【Initiate OAuth Sign-In】

开始 OAuth 登录流程:

【To start the OAuth sign-in process:】

POST
/sign-in/oauth2
const { data, error } = await authClient.signIn.oauth2({    providerId: "provider-id", // required    callbackURL: "/dashboard",    errorCallbackURL: "/error-page",    newUserCallbackURL: "/welcome",    disableRedirect: false,    scopes: ["my-scope"],    requestSignUp: false,});
PropDescriptionType
providerId
The provider ID for the OAuth provider.
string
callbackURL?
The URL to redirect to after sign in.
string
errorCallbackURL?
The URL to redirect to if an error occurs.
string
newUserCallbackURL?
The URL to redirect to after login if the user is new.
string
disableRedirect?
Disable redirect.
boolean
scopes?
Scopes to be passed to the provider authorization request.
string[]
requestSignUp?
Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider.
boolean

关联 OAuth 账户

【Linking OAuth Accounts】

将 OAuth 账户链接到现有用户:

【To link an OAuth account to an existing user:】

POST
/oauth2/link
const { data, error } = await authClient.oauth2.link({    providerId: "my-provider-id", // required    callbackURL: "/successful-link", // required});
PropDescriptionType
providerId
The OAuth provider ID.
string
callbackURL
The URL to redirect to once the account linking was complete.
string

处理 OAuth 回调

【Handle OAuth Callback】

该插件挂载了一个路由来处理 OAuth 回调 /oauth2/callback/:providerId。这意味着默认情况下 ${baseURL}/api/auth/oauth2/callback/:providerId 将被用作回调 URL。请确保你的 OAuth 提供商已配置为使用此 URL。

【The plugin mounts a route to handle the OAuth callback /oauth2/callback/:providerId. This means by default ${baseURL}/api/auth/oauth2/callback/:providerId will be used as the callback URL. Make sure your OAuth provider is configured to use this URL.】

预配置的提供者助手

【Pre-configured Provider Helpers】

Better Auth 提供了针对流行 OAuth 提供商的预配置辅助函数。这些辅助函数处理特定提供商的配置,包括发现 URL 和用户信息端点。

【Better Auth provides pre-configured helper functions for popular OAuth providers. These helpers handle the provider-specific configuration, including discovery URLs and user info endpoints.】

支持的提供商

【Supported Providers】

  • Auth0 - auth0(options)
  • HubSpot - hubspot(options)
  • Keycloak - keycloak(options)
  • 线 - line(options)
  • Microsoft Entra ID(Azure AD) - microsoftEntraId(options)
  • Okta - okta(options)
  • Slack - slack(options)
  • Patreon - patreon(options)

示例:使用预配置的提供者

【Example: Using Pre-configured Providers】

auth.ts
import { betterAuth } from 'better-auth';
import {
	// genericOAuth plugin
	genericOAuth,
	// providers
	auth0,
	gumroad,
	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,
				}),
				gumroad({
					clientId: process.env.GUMROAD_CLIENT_ID,
					clientSecret: process.env.GUMROAD_CLIENT_SECRET,
				}),
				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,
				}),
				// LINE supports multiple channels (countries) - use different providerIds
				line({
					providerId: 'line-jp',
					clientId: process.env.LINE_JP_CLIENT_ID,
					clientSecret: process.env.LINE_JP_CLIENT_SECRET,
				}),
				line({
					providerId: 'line-th',
					clientId: process.env.LINE_TH_CLIENT_ID,
					clientSecret: process.env.LINE_TH_CLIENT_SECRET,
				}),
				microsoftEntraId({
					clientId: process.env.MS_APP_ID,
					clientSecret: process.env.MS_CLIENT_SECRET,
					tenantId: process.env.MS_TENANT_ID,
				}),
				okta({
					clientId: process.env.OKTA_CLIENT_ID,
					clientSecret: process.env.OKTA_CLIENT_SECRET,
					issuer: process.env.OKTA_ISSUER,
				}),
				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,
				}),
			],
		}),
	],
});

每个提供者助手都接受通用的 OAuth 选项(扩展自 BaseOAuthProviderOptions)以及提供者特定的字段:

【Each provider helper accepts common OAuth options (extending BaseOAuthProviderOptions) plus provider-specific fields:】

  • Auth0:需要 domain(例如,dev-xxx.eu.auth0.com
  • HubSpot:无需额外必填字段。可选的“scopes”(默认为“[”oauth“]')
  • Keycloak:需要 issuer(例如,https://my-domain/realms/MyRealm
  • LINE:可选的 providerId(默认为 "line")。LINE 对不同国家(日本、泰国、台湾等)要求使用不同的渠道,因此你可以使用不同的 providerId 和凭据多次调用 line() 以支持多个国家
  • Microsoft Entra ID:需要 tenantId(可以是 GUID、"common""organizations""consumers"
  • Okta:需要 issuer(例如,https://dev-xxxxx.okta.com/oauth2/default
  • Slack:无需额外必填字段
  • Patreon:无需额外填写字段

所有提供者都支持相同的可选字段:

  • scopes?: string[] - 要请求的 OAuth 范围数组
  • redirectURI?: string - 自定义重定向 URI
  • pkce?: boolean - 启用 PKCE(默认为 false
  • disableImplicitSignUp?: boolean - 禁用新用户的自动注册
  • disableSignUp?: boolean - 完全禁用注册
  • overrideUserInfo?: boolean - 登录时覆盖用户信息

【All providers support the same optional fields:

  • scopes?: string[] - Array of OAuth scopes to request
  • redirectURI?: string - Custom redirect URI
  • pkce?: boolean - Enable PKCE (defaults to false)
  • disableImplicitSignUp?: boolean - Disable automatic sign-up for new users
  • disableSignUp?: boolean - Disable sign-up entirely
  • overrideUserInfo?: boolean - Override user info on sign in】

配置

【Configuration】

在将插件添加到你的身份验证配置时,你可以配置多个 OAuth 提供商。你可以使用预配置的提供商助手(如上所示),也可以手动创建自定义配置。

【When adding the plugin to your auth config, you can configure multiple OAuth providers. You can either use the pre-configured provider helpers (shown above) or create custom configurations manually.】

手动配置

【Manual Configuration】

每个提供者配置对象支持以下选项:

【Each provider configuration object supports the following options:】

interface GenericOAuthConfig {
  providerId: string;
  discoveryUrl?: string;
  authorizationUrl?: string;
  tokenUrl?: string;
  userInfoUrl?: string;
  clientId: string;
  clientSecret: string;
  scopes?: string[];
  redirectURI?: string;
  responseType?: string;
  prompt?: string;
  pkce?: boolean;
  accessType?: string;
  getUserInfo?: (tokens: OAuth2Tokens) => Promise<User | null>;
}

其他提供者配置

【Other Provider Configurations】

providerId:用于标识 OAuth 提供商配置的唯一字符串。

discoveryUrl:(可选)用于获取提供商 OAuth 2.0/OIDC 配置的 URL。如果提供,像 authorizationUrltokenUrluserInfoUrl 这样的端点可以自动发现。

authorizationUrl:(可选)OAuth 提供者的授权端点。如果使用 discoveryUrl,则不需要此项。

tokenUrl:(可选)OAuth 提供者的令牌端点。如果使用 discoveryUrl,则不需要此项。

userInfoUrl:(可选)获取用户资料信息的端点。如果使用 discoveryUrl,则不需要此项。

clientId:由你的提供商颁发的 OAuth 客户端 ID。

clientSecret:由你的提供商颁发的 OAuth 客户端密钥。

scopes:(可选)可向提供者请求的范围数组(例如,'[“openid”, “email”, “profile”]')。

redirectURI: (可选)用于 OAuth 流程的重定向 URI。如果未设置,将根据你的应用基础 URL 构建默认值。

responseType:(可选)OAuth 响应类型。默认值为 "code",用于授权码流程。

responseMode:(可选)授权码请求的响应模式,例如 "query""form_post"

提示:(可选)控制身份验证体验(例如,强制登录、同意等)。

pkce:(可选)如果为 true,则启用 PKCE(用于代码交换的证明密钥)以增强安全性。默认值为 false

accessType:(可选)授权请求的访问类型。使用 "offline" 来请求刷新令牌。

getToken:(可选)一个自定义函数,用于将授权代码交换为令牌。如果提供了此函数,将使用它来替代默认的令牌交换逻辑。这对于使用 GET 请求或自定义参数的非标准令牌端点的提供者非常有用。

getUserInfo: (可选)一个自定义函数,用于根据 OAuth 令牌从提供者处获取用户信息。如果未提供,则使用默认获取方式。

mapProfileToUser:(可选)一个将提供者的用户资料映射到你应用的用户对象的函数。适用于自定义字段映射或转换。

authorizationUrlParams:(可选)要添加到授权 URL 的额外查询参数。这些参数可以覆盖默认参数。你也可以提供一个返回参数的函数。

tokenUrlParams:(可选)要添加到令牌 URL 的额外查询参数。这些参数可以覆盖默认参数。你也可以提供一个返回参数的函数。

disableImplicitSignUp:(可选)如果为 true,则禁用新用户的自动注册。必须明确发起带有注册意图的登录请求。

disableSignUp:(可选)如果为 true,则完全禁用新用户注册。只有已有用户可以登录。

认证: (可选)令牌请求的认证方法。可以是 'basic''post'。默认值为 'post'

discoveryHeaders:(可选)在发现请求中包含的自定义头。对于需要特殊头的提供商非常有用。

authorizationHeaders:(可选)在授权请求中包含的自定义头。对于需要特殊头的提供者非常有用。

overrideUserInfo: (可选)如果为 true,每次用户登录时,你数据库中的用户信息将会被提供者的信息更新。默认为 false

高级用法

【Advanced Usage】

自定义令牌交换

【Custom Token Exchange】

对于使用 GET 请求或自定义参数的非标准令牌端点的提供者,你可以提供自定义的 getToken 函数:

【For providers with non-standard token endpoints that use GET requests or custom parameters, you can provide a custom getToken function:】

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"],
      // Custom token exchange for non-standard endpoints
      getToken: async ({ code, redirectURI }) => {
        // Example: GET request 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(" ") ?? [],
          // Preserve provider-specific fields in raw
          raw: data,
        };
      },
      getUserInfo: async (tokens) => {
        // Access provider-specific fields from raw token data
        const userId = tokens.raw?.user_id as string;

        const response = await fetch(
          `https://provider.example.com/api/user?` +
          `access_token=${tokens.accessToken}`
        );

        const data = await response.json();

        return {
          id: userId,
          name: data.display_name,
          email: data.email,
          image: data.avatar_url,
          emailVerified: data.email_verified,
        };
      },
    },
  ],
});

自定义用户信息获取

【Custom User Info Fetching】

你可以提供自定义的 getUserInfo 函数来处理特定提供商的要求:

【You can provide a custom getUserInfo function to handle specific provider requirements:】

genericOAuth({
  config: [
    {
      providerId: "custom-provider",
      // ... other config options
      getUserInfo: async (tokens) => {
        // Custom logic to fetch and return user info
        const userInfo = await fetchUserInfoFromCustomProvider(tokens);
        return {
          id: userInfo.sub,
          email: userInfo.email,
          name: userInfo.name,
          // ... map other fields as needed
        };
      }
    }
  ]
})

映射用户信息字段

【Map User Info Fields】

如果提供者返回的用户信息与预期格式不匹配,或者你需要映射其他字段,可以使用 mapProfileToUser

【If the user info returned by the provider does not match the expected format, or you need to map additional fields, you can use the mapProfileToUser:】

genericOAuth({
  config: [
    {
      providerId: "custom-provider",
      // ... other config options
      mapProfileToUser: async (profile) => {
        return {
          firstName: profile.given_name,
          // ... map other fields as needed
        };
      }
    }
  ]
})

访问原始令牌数据

【Accessing Raw Token Data】

tokens 参数包括一个 raw 字段,用于保留来自提供者的原始令牌响应。这对于访问提供者特定的字段非常有用:

【The tokens parameter includes a raw field that preserves the original token response from the provider. This is useful for accessing provider-specific fields:】

getUserInfo: async (tokens) => {
  // Access provider-specific fields
  const customField = tokens.raw?.custom_provider_field as string;
  const userId = tokens.raw?.provider_user_id as string;

  // Use in your logic
  return {
    id: userId,
    // ...
  };
}

错误处理

【Error Handling】

该插件内置了针对常见 OAuth 问题的错误处理。错误通常会被重定向到你应用的错误页面,URL 参数中会包含相应的错误信息。如果未提供回调 URL,用户将被重定向到 Better Auth 的默认错误页面。

【The plugin includes built-in error handling for common OAuth issues. Errors are typically redirected to your application's error page with an appropriate error message in the URL parameters. If the callback URL is not provided, the user will be redirected to Better Auth's default error page.】