通用 OAuth

使用任何 OAuth 提供商进行用户身份验证

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

安装

在你的身份验证配置中添加插件

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

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

export const auth = betterAuth({
    // ... 其他配置选项
    plugins: [
        genericOAuth({ 
            config: [ 
                { 
                    providerId: "provider-id", 
                    clientId: "test-client-id", 
                    clientSecret: "test-client-secret", 
                    discoveryUrl: "https://auth.example.com/.well-known/openid-configuration", 
                    // ... 其他配置选项
                }, 
                // 根据需要添加更多提供商
            ] 
        }) 
    ]
})

添加客户端插件

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

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

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

使用方法

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

启动 OAuth 登录

启动 OAuth 登录流程:

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,});
Parameters
providerIdstringrequired

OAuth 提供商的 ID。

callbackURLstring

登录后重定向的 URL。

errorCallbackURLstring

发生错误时重定向的 URL。

newUserCallbackURLstring

新用户登录后重定向的 URL。

disableRedirectboolean

禁用重定向。

scopesstring[]

授权请求中传递给提供商的作用域。

requestSignUpboolean

显式请求注册。当该提供商启用 disableImplicitSignUp 时有用。

关联 OAuth 账号

将 OAuth 账号关联到现有用户:

POST/oauth2/link
const { data, error } = await authClient.oauth2.link({    providerId: "my-provider-id", // required    callbackURL: "/successful-link", // required});
Parameters
providerIdstringrequired

OAuth 提供商 ID。

callbackURLstringrequired

账号关联完成后重定向的 URL。

处理 OAuth 回调

插件挂载了一个用于处理 OAuth 回调的路由 /oauth2/callback/:providerId。默认情况下,回调 URL 是 ${baseURL}/api/auth/oauth2/callback/:providerId。请确保你的 OAuth 提供商配置了此回调 URL。

预配置的提供商助手

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

支持的提供商

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

例子:使用预配置提供商

auth.ts
import { betterAuth } from 'better-auth';
import {
	// 通用 OAuth 插件
	genericOAuth,
	// 提供商
	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 支持多个渠道(国家)- 使用不同的 providerId
				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)以及提供商特定字段:

  • Auth0:需要 domain(例如 dev-xxx.eu.auth0.com
  • HubSpot:没有额外必需字段。可选 scopes(默认 ["oauth"]
  • Keycloak:需要 issuer(例如 https://my-domain/realms/MyRealm
  • LINE:可选 providerId(默认为 "line")。LINE 需要为不同国家(日本、泰国、台湾等)使用单独渠道,因此你可以多次调用 line(),使用不同的 providerId 和凭证来支持多个国家
  • 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 - 登录时覆盖用户信息

配置

将插件添加到身份验证配置时,可以配置多个 OAuth 提供商。可以使用上面介绍的预配置提供商助手,也可以手动创建自定义配置。

手动配置

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

interface GenericOAuthConfig {
  providerId: string;
  discoveryUrl?: string;
  issuer?: string;
  requireIssuerValidation?: boolean;
  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>;
}

其他提供商配置说明

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

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

issuer:(可选)用于验证的预期发行者标识符。如果未提供但设置了 discoveryUrl,则从发现文档中获取。设置后,回调将验证 iss 参数是否匹配此值。

requireIssuerValidation:(可选)当为 true 时,如果配置了发行者,要求回调中必须有 iss 参数。这提供更严格的安全性,但可能与旧版 OAuth 服务器不兼容。默认值为 false

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"

prompt:(可选)控制认证体验(例如强制登录、同意等)。

pkce:(可选)是否启用 PKCE(Proof Key for Code Exchange)以增强安全性。默认为 false

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

getToken:(可选)自定义函数,用于用授权码交换令牌。如果提供,将替代默认的令牌交换逻辑。适用于使用 GET 请求或自定义参数的非标准令牌端点。

getUserInfo:(可选)自定义函数,使用 OAuth 令牌从提供商获取用户信息。如果未提供,则使用默认的获取方式。

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

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

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

disableImplicitSignUp:(可选)为 true 时,禁用新用户自动注册。必须显式请求带注册意图的登录。

disableSignUp:(可选)为 true 时,完全禁用新用户注册。只能登录已有用户。

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

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

authorizationHeaders:(可选)授权请求中包含的自定义头。适用于需要特殊头的提供商。

overrideUserInfo:(可选)为 true 时,每次登录时用提供商信息更新数据库中的用户信息。默认 false

安全性:发行者验证

Better Auth 验证 OAuth 提供商的发行者,以防止混淆攻击(RFC 9207)。混淆攻击发生在恶意授权服务器欺骗应用将授权码发往错误的令牌端点时。

工作原理

支持 RFC 9207 的 OAuth 提供商会在授权响应中包含 iss(发行者)参数。Better Auth 会验证该参数是否与预期发行者匹配,确保响应来自正确的提供商。

配置示例

自动发现(推荐给 OIDC 提供商):

genericOAuth({
  config: [{
    providerId: "my-provider",
    discoveryUrl: "https://auth.example.com/.well-known/openid-configuration",
    clientId: "...",
    clientSecret: "...",
    // 发行者自动从发现文档获取
  }]
})

手动配置发行者:

genericOAuth({
  config: [{
    providerId: "custom-oauth",
    authorizationUrl: "https://auth.example.com/authorize",
    tokenUrl: "https://auth.example.com/token",
    issuer: "https://auth.example.com",  // 手动指定预期发行者
    clientId: "...",
    clientSecret: "...",
  }]
})

严格模式(推荐现代提供商):

genericOAuth({
  config: [{
    providerId: "secure-provider",
    discoveryUrl: "https://auth.example.com/.well-known/openid-configuration",
    clientId: "...",
    clientSecret: "...",
    requireIssuerValidation: true,  // 若缺失 iss 参数则拒绝
  }]
})

验证行为

场景requireIssuerValidation结果
iss 匹配预期-成功
iss 不匹配-抛出 issuer_mismatch 错误
iss 缺失false(默认)成功(向后兼容)
iss 缺失true抛出 issuer_missing 错误

为实现最高安全性(适用于 Google、Auth0、Okta 等现代 OAuth/OIDC 提供商),推荐启用 requireIssuerValidation: true

高级用法

自定义令牌交换

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

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 }) => {
        // 示例:使用 GET 请求替代 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 中
          raw: data,
        };
      },
      getUserInfo: async (tokens) => {
        // 访问 raw 令牌数据中的提供商特定字段
        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,
        };
      },
    },
  ],
});

自定义获取用户信息

你可以提供自定义的 getUserInfo 函数以满足特定提供商需求:

genericOAuth({
  config: [
    {
      providerId: "custom-provider",
      // ... 其他配置选项
      getUserInfo: async (tokens) => {
        // 自定义逻辑获取并返回用户信息
        const userInfo = await fetchUserInfoFromCustomProvider(tokens);
        return {
          id: userInfo.sub,
          email: userInfo.email,
          name: userInfo.name,
          // ... 根据需要映射其他字段
        };
      }
    }
  ]
})

映射用户信息字段

如果提供商返回的用户信息格式不符合预期,或需要映射额外字段,可以使用 mapProfileToUser

genericOAuth({
  config: [
    {
      providerId: "custom-provider",
      // ... 其他配置选项
      mapProfileToUser: async (profile) => {
        return {
          firstName: profile.given_name,
          // ... 根据需要映射其他字段
        };
      }
    }
  ]
})

访问原始令牌数据

tokens 参数包含一个 raw 字段,保存了提供商返回的原始令牌响应。此字段用于访问提供商特定字段:

getUserInfo: async (tokens) => {
  // 访问提供商特定字段
  const customField = tokens.raw?.custom_provider_field as string;
  const userId = tokens.raw?.provider_user_id as string;

  // 在你的逻辑中使用
  return {
    id: userId,
    // ...
  };
}

错误处理

插件内置了常见 OAuth 错误处理。错误通常会重定向到你的应用错误页面,并附带适当的错误信息 URL 参数。如果未提供错误回调 URL,用户将被重定向到 Better Auth 的默认错误页面。