通用 OAuth

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

Generic OAuth 插件允许你将任何 OAuth 2.1 或 OpenID Connect(OIDC)提供商添加到应用程序中。提供商会作为一等社交提供商进行注册,并使用标准的 signIn.social 流程,默认启用 PKCE 和发行者验证。

何时使用此插件

在以下情况下使用 Generic OAuth 插件:

  • 你的提供商不是内置社交提供商之一(Google、GitHub、Discord 等)
  • 你需要连接到企业身份提供商(Keycloak、Okta、Auth0、Microsoft Entra ID)
  • 你希望支持具有自定义或非标准 OAuth 端点的提供商

如果你的提供商已经内置,请改用身份验证配置中的 socialProviders;内置提供商支持移动端 id-token 登录,并具有针对特定提供商的优化。

自定义选项

OAuth 流程的每个方面都可以被覆盖:

需求配置字段
提供商使用非标准令牌交换方式(GET、自定义参数)getToken
提供商返回非标准用户资料getUserInfo
你需要将资料字段映射到用户模型mapProfileToUser
提供商需要额外的授权参数authorizationUrlParams
提供商需要额外的令牌参数tokenUrlParams
提供商需要自定义 HTTP 头discoveryHeadersauthorizationHeaders
提供商不支持 OIDC 发现显式设置 authorizationUrltokenUrluserInfoUrl
提供商配置共享同一个身份命名空间设置相同的 accountIssuer
提供商使用非标准的不可变用户标识符设置 accountSubject
提供商拒绝 PKCE设置 pkce: false

安装

将插件添加到身份验证配置中。

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
            ] 
        }) 
    ]
})

使用方法

Generic OAuth 提供商通过标准的社交登录流程使用:

登录

await authClient.signIn.social({
    provider: "provider-id",
    callbackURL: "/dashboard",
})

你也可以传入 additionalData(一个 Record<string, any>),在整个流程中往返传递客户端数据;在回调中通过 getOAuthState() 读取这些数据,并将其视为不受信任的数据。请参阅在 OAuth 流程中传递额外数据

使用 ID Token 登录

配置了 discoveryUrl 且其发现文档发布了 jwks_uri 的提供商,也接受客户端直接获取的 id_token,无需执行重定向流程。该令牌会先根据提供商的 JWKS 进行验证,然后才会信任其中的任何声明:

await authClient.signIn.social({
    provider: "provider-id",
    idToken: {
        token: idTokenFromProvider,
    },
})

使用显式端点而非 discoveryUrl 配置的提供商不支持此路径,并会返回 ID_TOKEN_NOT_SUPPORTED

关联账户

await authClient.linkSocial({
    provider: "provider-id",
    callbackURL: "/settings",
})

处理 OAuth 回调

该插件使用位于 /callback/:providerId 的核心 OAuth 回调路由。这意味着默认情况下会使用 ${baseURL}/api/auth/callback/:providerId 作为回调 URL。请确保你的 OAuth 提供商配置为使用此 URL。

与内置提供商不同,:providerId 参数是必需的,并且必须与您配置的提供商 ID 匹配。

验证 OAuth 用户信息

使用 user.validateUserInfo 在 Better Auth 创建用户(create-user)、关联新账户(link-account)或让现有用户重新登录(sign-in)之前拒绝 Generic OAuth 用户。在 sign-in 操作中,该回调会接收提供商最新的电子邮件和资料,因此可以通过域名或组织检查,拒绝其提供商身份后来超出范围的用户。检查 source.oauth?.providerId,将验证范围限定到特定的 Generic OAuth 提供商,并使用 source.oauth?.profile 获取提供商特定的资料字段。

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

export const auth = betterAuth({
  user: {
    validateUserInfo: ({ user, source }) => {
      if (source.oauth?.providerId !== "company-oauth") {
        return;
      }

      if (!user.email?.endsWith("@example.com")) {
        return {
          error: "email_not_allowed",
          errorDescription: "Use your example.com email to sign in",
        };
      }
    },
  },
  plugins: [
    genericOAuth({
      config: [
        {
          providerId: "company-oauth",
          clientId: process.env.COMPANY_OAUTH_CLIENT_ID!,
          clientSecret: process.env.COMPANY_OAUTH_CLIENT_SECRET!,
          discoveryUrl: "https://auth.example.com/.well-known/openid-configuration",
        },
      ],
    }),
  ],
});

不返回任何内容即可允许 OAuth 流程继续。返回带有 error 的对象即可拒绝该流程。基于重定向的 OAuth 流程会将拒绝结果发送到配置的错误 URL,而程序化流程会返回 403 API 错误。

RP 发起的注销

对于公开了 end_session_endpoint 的 OIDC 提供商,authClient.signOut() 会清除 Better Auth 会话,然后将浏览器重定向到提供商的注销端点。使用 discoveryUrl 时,会从提供商的发现文档中读取该端点。你也可以通过 endSessionEndpoint 手动配置。

await authClient.signOut()

传入 callbackURL,可以要求提供商在注销后将用户返回到你的应用。该 URL 必须在提供商处注册为注销后重定向 URI。

await authClient.signOut({
  callbackURL: "/login",
})

如果多个已关联的提供商支持 RP 发起的注销,Better Auth 会将用户重定向到最近更新账户所对应的提供商。浏览器导航一次只能完成一个提供商的注销。

如果要自行处理提供商导航,请设置 disableRedirect 并使用返回的 url

const { data } = await authClient.signOut({
  disableRedirect: true,
})

if (data?.url) {
  window.location.assign(data.url)
}

如果要使注销仅在 Better Auth 本地完成,请为该配置禁用提供商注销。

genericOAuth({
  config: [{
    providerId: "provider-id",
    discoveryUrl: "https://auth.example.com/.well-known/openid-configuration",
    clientId: "test-client-id",
    clientSecret: "test-client-secret",
    disableProviderLogout: true,
  }]
})

预配置的提供商辅助函数

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)
  • Yandex - yandex(options)

使用预配置提供程序的示例

auth.ts
import { betterAuth } from 'better-auth';
import {
	// 通用 OAuth 插件
	genericOAuth,
	// 提供程序
	auth0,
	gumroad,
	hubspot,
	keycloak,
	line,
	microsoftEntraId,
	okta,
	slack,
	patreon,
	yandex,
} 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,
				}),
				yandex({
					clientId: process.env.YANDEX_CLIENT_ID,
					clientSecret: process.env.YANDEX_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:需要具体的租户 GUID。对于多租户的 "common""organizations""consumers" authority,请使用内置的 Microsoft 社交提供商;这些 authority 需要通过 ID-token 声明验证来确定账户的实际发行者
  • Okta:需要 issuer(例如 https://dev-xxxxx.okta.com/oauth2/default
  • Slack:没有其他必填字段
  • Patreon:没有其他必填字段
  • Yandex:没有其他必填字段

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

  • clientSecret?: string - OAuth 客户端密钥
  • tokenEndpointAuth?: TokenEndpointAuth - 令牌端点请求的客户端身份验证配置
  • scopes?: string[] - 要请求的 OAuth 作用域数组
  • redirectURI?: string - 自定义重定向 URI
  • pkce?: boolean - 启用 PKCE(默认为 true
  • disableImplicitSignUp?: boolean - 禁用新用户的自动注册
  • disableSignUp?: boolean - 完全禁用注册
  • overrideUserInfo?: boolean - 登录时覆盖用户信息
  • endSessionEndpoint?: string - OIDC RP 发起的注销端点。可用时会从 end_session_endpoint 自动发现
  • postLogoutRedirectURI?: string - 提供商注销期间作为 post_logout_redirect_uri 传递的默认 URI
  • disableProviderLogout?: boolean - 禁用 authClient.signOut() 期间的自动提供商注销

配置

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

手动配置

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

import type {
  OAuthAccountKeyContext,
  OAuth2Tokens,
  TokenEndpointAuth,
} from "better-auth/oauth2";
import type { GenericOAuthUserInfo } from "better-auth/plugins/generic-oauth";

interface GenericOAuthConfig {
  providerId: string;
  accountSubject?: (
    context: OAuthAccountKeyContext<GenericOAuthUserInfo>,
  ) => string | number | Promise<string | number>;
  accountIssuer?:
    | string
    | ((context: OAuthAccountKeyContext<GenericOAuthUserInfo>) => string | Promise<string>);
  discoveryUrl?: string;
  requireIdTokenVerification?: boolean;
  authorizationUrl?: string;
  tokenUrl?: string;
  userInfoUrl?: string;
  endSessionEndpoint?: string;
  postLogoutRedirectURI?: string;
  disableProviderLogout?: boolean;
  clientId: string;
  clientSecret?: string;
  tokenEndpointAuth?: TokenEndpointAuth;
  scopes?: string[];
  redirectURI?: string;
  responseType?: string;
  prompt?: string;
  pkce?: boolean;
  accessType?: string;
  accessTokenExpiresIn?: number;
  getUserInfo?: (tokens: OAuth2Tokens) => Promise<GenericOAuthUserInfo | null>;
}

其他提供商配置说明

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

accountSubject:(可选)解析由提供商分配的不可变用户标识符。OpenID Connect 发现提供商默认使用经过验证的 sub 字段。普通 OAuth 提供商使用 id。Better Auth 不会在运行时在这两个字段之间切换。当提供商使用其他字段(例如 account_id)时,请设置此解析器。该解析器接收 OAuth 令牌和提供商原始资料,并可以返回字符串或数字。

mapProfileToUser 无法设置账户主题。这使本地资料映射与账户识别保持分离。

accountIssuer:(可选)与提供商账户主题配对的稳定权威命名空间。发现提供商默认使用其发现到的发行者。当显式端点提供商具有发行者、多个提供商配置代表同一权威,或经过验证的资料确定了特定租户的发行者时,请设置此字段。如果没有 discovery 或 accountIssuer,Better Auth 会使用 local:oauth:<encoded providerId>,其中提供商 ID 部分经过百分号编码。

该解析器接收经过验证的提供商数据。只能从提供商验证流程确定的声明或资料值中派生动态发行者;绝不要使用请求参数、电子邮件域名或其他由调用者控制的值。

具有相同 accountIssuer 和主题的提供商配置会对同一个外部身份进行去重。它们共享一条账户记录和一组令牌;别名不会创建独立的授权或提供商生命周期记录。

discoveryUrl:(可选)用于获取提供商 OAuth 2.0/OIDC 配置的 URL。如果提供,则会在服务器启动时自动发现 authorizationUrltokenUrluserInfoUrl 等端点。当发现文档发布 jwks_uri 时,提供商返回的 id_token 会根据该地址进行验证(包括签名、发行者、受众和发布的签名算法),之后才会使用其中的声明;验证失败的令牌会拒绝登录。发现提供商还会使用服务器生成的 OIDC nonceid_token 与授权请求绑定,并拒绝未回显该值的回调。如果发现无法建立有效配置,提供商初始化会失败,从而避免账户在其发现到的发行者与本地回退值之间切换。

requireIdTokenVerification:(可选)要求发现过程提供可用的发行者和 jwks_uri,然后才注册提供商。当 accountSubjectgetUserInfo 从 ID-token 声明中派生身份时,请启用此选项。如果发现不可用或不完整,初始化会失败,而不是回退到未经验证的令牌解码。microsoftEntraId 助手会自动启用此选项。

disableIdTokenNonceBinding:(可选)关闭发现提供商 id_token 的 OIDC nonce 绑定。默认启用绑定,并会拒绝未回显服务器生成的 nonceid_token(OIDC Core 1.0 §3.1.3.7)。只有不在授权码流程中返回 nonce 声明的提供商才应将其设置为 true;这会移除该提供商的 id_token 重放保护。

authorizationUrl:(可选)OAuth 提供商的授权端点。如果使用 discoveryUrl,则不必指定。

tokenUrl:(可选)OAuth 提供商的令牌端点。如果使用 discoveryUrl,则不必指定。

userInfoUrl:(可选)获取用户资料信息的端点。如果使用 discoveryUrl,则不必指定。

endSessionEndpoint:(可选)OIDC RP 发起的注销端点。如果使用 discoveryUrl 且提供商返回 end_session_endpoint,则不需要设置。

postLogoutRedirectURI:(可选)当 authClient.signOut() 将用户重定向到提供商时,作为 post_logout_redirect_uri 发送的默认 URI。该 URI 必须在提供商处注册。

disableProviderLogout:(可选)如果为 true,authClient.signOut() 只会清除 Better Auth 会话,不会重定向到提供商注销端点。

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

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

tokenEndpointAuth:(可选)令牌端点请求的客户端身份验证配置。对于 RFC 7523 客户端断言,请使用 { method: "private_key_jwt", getClientAssertion };对于基于密钥的客户端,请使用 { method: "client_secret_basic" }{ method: "client_secret_post" };对于公共客户端,请使用 { method: "none" }。基于密钥的方法要求提供 clientSecret;不要将 clientSecretprivate_key_jwtnone 组合使用。如果省略该配置,Better Auth 会在配置了 clientSecret 时发送基于密钥的令牌请求,未配置时发送公共客户端令牌请求。

scopes:(可选)要向提供商请求的作用域数组(例如 ["openid", "email", "profile"])。

redirectURI:(可选)OAuth 流程使用的重定向 URI。如果未设置,则会根据应用的基础 URL 构造默认值。必须包含 :providerId 占位符(例如 https://example.com/api/auth/callback/my-provider)。

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

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

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

pkce:(可选)启用 PKCE(Proof Key for Code Exchange),这是 OAuth 2.1 的要求。默认为 true。只有明确拒绝 PKCE 的提供商才应禁用此选项。

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

accessTokenExpiresIn:(可选)备用访问令牌有效期(秒),仅在提供商的令牌响应省略 expires_in 时使用。如果没有已知的过期时间,getAccessToken 无法判断令牌已过期,也不会刷新它。请将其设置为令牌的有效期,以便跟踪过期并在需要时刷新令牌。如果提供商返回 expires_in,则留空。

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

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

mapProfileToUser:(可选)一个将提供商资料映射到应用程序用户可变字段的函数。它无法设置提供商身份;对于非标准的不可变标识符,请使用 accountSubject

authorizationUrlParams:(可选)要添加到授权 URL 的其他查询参数。保留的 OAuth 密钥(stateclient_idredirect_uriresponse_typecode_challengecode_challenge_methodnoncescope)会被忽略,因此无法替换 Better Auth 为流程管理的值;其他任何键都会覆盖默认值。

tokenUrlParams:(可选)要添加到令牌 URL 的其他查询参数。Better Auth 已设置的参数会被保留。请使用 clientIdclientSecrettokenEndpointAuth 配置令牌端点客户端身份验证。

refreshTokenParams:(可选)刷新访问令牌时合并到令牌端点请求中的其他请求体参数。接受普通对象,或接受一个在刷新时运行的(同步或异步)函数,因此可以注入动态值,例如 scopeaudienceresource 或租户标识符,而不必重新进行授权重定向。示例包括切换工作区时使用 Zitadel 的 urn:zitadel:iam:org:id:{orgId} 作用域,或轮换 Auth0 的 audience。函数形式会接收触发请求中的请求元数据(头和 Cookie),因此可以直接读取请求范围内的数据——调用者必须在将任何从头或 Cookie 派生的值用作作用域、受众或租户声明之前,根据经过身份验证的用户权限对其进行验证。无法覆盖 grant_typerefresh_token;合并后,client_id 由配置的令牌端点身份验证设置,因此这里同样无法覆盖。

genericOAuth({
  config: [
    {
      providerId: "zitadel",
      // ...
      refreshTokenParams: (ctx) => {
        const activeOrg = ctx?.headers?.get("x-active-org");
        return activeOrg
          ? { scope: `openid profile email urn:zitadel:iam:org:id:${activeOrg}` }
          : undefined;
      },
    },
  ],
});

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

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

authentication:(可选)基于密钥的令牌请求身份验证。可以是 'basic''post'。默认为 'post''basic' 要求提供 clientSecret。对于超出客户端密钥范围的令牌端点方法,请配置 tokenEndpointAuth

对于 private_key_jwt,将 tokenEndpointAuth.method 设置为 "private_key_jwt"。你可以提供自己的 getClientAssertion 函数,也可以使用 createPrivateKeyJwtClientAssertionGetter 从私钥签署 RFC 7523 JWT 断言:

import { createPrivateKeyJwtClientAssertionGetter } from "better-auth/oauth2";

genericOAuth({
    config: [
        {
            providerId: "my-idp",
            clientId: "your-client-id",
            tokenUrl: "https://idp.example.com/oauth/token",
            authorizationUrl: "https://idp.example.com/oauth/authorize",
            tokenEndpointAuth: {
                method: "private_key_jwt",
                getClientAssertion: createPrivateKeyJwtClientAssertionGetter({
                    privateKeyJwk: { /* your JWK */ },
                    kid: "my-key-1",
                    algorithm: "RS256",
                }),
            },
            pkce: true,
        },
    ],
}),

你也可以从另一个提供商返回断言:

genericOAuth({
    config: [
        {
            providerId: "my-idp",
            clientId: process.env.IDP_CLIENT_ID!,
            discoveryUrl: "https://idp.example.com/.well-known/openid-configuration",
            tokenEndpointAuth: {
                method: "private_key_jwt",
                getClientAssertion: async ({ clientId, tokenEndpoint, grantType }) => {
                    return getWorkloadIdentityToken({
                        clientId,
                        audience: tokenEndpoint,
                        grantType,
                    });
                },
            },
            pkce: true,
        },
    ],
});

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

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

安全性:发行者验证

高级用法

自定义令牌交换

对于使用 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,
    // ...
  };
}

IDP 发起的流程

一些 OAuth 提供商(例如 Clever)允许身份提供商将用户重定向到你的回调 URL,而无需用户先点击应用中的“登录”按钮。这些回调包含 code 参数但没有 state,通常会被拒绝,因为缺少 state 会破坏标准的 CSRF 检查。

在提供商上设置 allowIdpInitiated: true,即可安全地接受这些流程:

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

export const auth = betterAuth({
    plugins: [
        genericOAuth({
            config: [
                {
                    providerId: "clever",
                    discoveryUrl: "https://clever.com/.well-known/openid-configuration",
                    clientId: process.env.CLEVER_CLIENT_ID,
                    clientSecret: process.env.CLEVER_CLIENT_SECRET,
                    allowIdpInitiated: true,
                },
            ],
        }),
    ],
});

当无状态回调访问 /callback/:providerId 时,Better Auth 会丢弃提供商签发的代码,并在服务器端重新启动流程:生成新的 state 和 PKCE 验证器,然后将用户重定向到提供商的授权端点。提供商会识别用户当前的会话并自动完成流程。整个过程中都会保留 CSRF 保护和 PKCE。

对于始终从你这一侧发起流程的提供商,请保持 allowIdpInitiated 关闭(默认值)。针对这些提供商的无状态回调表示请求格式错误或具有恶意,应当被拒绝。

启用 allowIdpInitiated 时,请设置全局 baseURL 选项。IDP 发起的回调不包含发起请求的请求体,因此跳转会使用 baseURL 作为登录后的目标地址。如果未设置 baseURL,跳转会失败并返回 CALLBACK_URL_REQUIRED

错误处理

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