OAuth

了解如何配置社交 OAuth 提供商、登录和关联账户、请求权限范围、传递额外数据、刷新访问令牌、映射用户资料以及自定义提供商选项。

Better Auth 内置支持 OAuth 2.0 和 OpenID Connect。这使得你可以通过流行的 OAuth 提供商(如 Google、Facebook、GitHub 等)进行用户身份验证。

如果你想使用的提供商没有直接支持,可以使用 Generic OAuth 插件 来实现自定义集成。

配置社交提供商

要启用社交提供商,需要为该提供商提供 clientIdclientSecret

下面是配置 Google 作为提供商的示例:

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

export const auth = betterAuth({
  // 其他配置...
  socialProviders: {
    google: {
      clientId: "YOUR_GOOGLE_CLIENT_ID",
      clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
    },
  },
});

用法

登录

要使用社交提供商登录,可以使用 signIn.social 函数(客户端)或 auth.api(服务器端)调用。

// 客户端用法
await authClient.signIn.social({
  provider: "google", // 或其它提供商 id
})
// 服务器端用法
await auth.api.signInSocial({
  body: {
    provider: "google", // 或其它提供商 id
  },
});

关联账户

要将账户关联到社交提供商,可以使用 linkAccount 函数(客户端)或 auth.api(服务器端)调用。

await authClient.linkSocial({
  provider: "google", // 或其它提供商 id
})

服务器端用法:

await auth.api.linkSocialAccount({
  body: {
    provider: "google", // 或其它提供商 id
  },
  headers: await headers() // 包含用户会话令牌的请求头
});

获取访问令牌

要获取社交提供商的访问令牌,可以使用 getAccessToken 函数(客户端)或 auth.api(服务器端)调用。如果访问令牌过期,将自动刷新。

const { accessToken } = await authClient.getAccessToken({
  providerId: "google", // 或其它提供商 id
  accountId: "accountId", // 可选,指定某个账户的访问令牌
})

服务器端用法:

await auth.api.getAccessToken({
  body: {
    providerId: "google", // 或其它提供商 id
    accountId: "accountId", // 可选,指定某个账户的访问令牌
    userId: "userId", // 可选,若未提供带身份认证的请求头
  },
  headers: await headers() // 包含用户会话令牌的请求头
});

获取提供商提供的账户信息

要获取提供商特定的账户信息,可以使用 accountInfo 函数(客户端)或 auth.api(服务器端)调用。

const info = await authClient.accountInfo({
  query: { accountId: "accountId" }, // 在此传入提供商提供的账户 id,提供商从账户 id 自动检测
})

服务器端用法:

await auth.api.accountInfo({
  query: { accountId: "accountId" },
  headers: await headers() // 包含用户会话令牌的请求头
});

请求额外权限范围

有时你的应用可能需要用户在已注册后再授予额外的 OAuth 权限范围(例如访问 GitHub 仓库或 Google Drive)。用户可能不想一开始就授予全部权限,更倾向于先授予最小权限,然后根据需要追加权限。

你可以用相同的提供商调用 linkSocial 方法请求额外权限,这会触发新的 OAuth 流程,申请额外权限,同时保持现有的账户关联。

const requestAdditionalScopes = async () => {
    await authClient.linkSocial({
        provider: "google",
        scopes: ["https://www.googleapis.com/auth/drive.file"],
    });
};

请确保你正在运行 Better Auth 1.2.7 或更高版本。较早版本(如 1.2.2)在尝试使用现有提供商关联以获取额外权限时,可能会显示 “Social account already linked” 错误。

通过 OAuth 流传递额外数据

Better Auth 允许你在 OAuth 流程中传递额外数据,但不将其存储到数据库。这适用于跟踪推荐码、分析来源或其他应在认证时处理但无需持久保存的临时数据。

发起 OAuth 登录或关联账户时,传递额外数据:

// 客户端:带额外数据登录
await authClient.signIn.social({
  provider: "google",
  additionalData: {
    referralCode: "ABC123",
    source: "landing-page",
  },
});

// 客户端:带额外数据关联账户
await authClient.linkSocial({
  provider: "google",
  additionalData: {
    referralCode: "ABC123",
  },
});

// 服务器端:带额外数据登录
await auth.api.signInSocial({
  body: {
    provider: "google",
    additionalData: {
      referralCode: "ABC123",
      source: "admin-panel",
    },
  },
});

在钩子中访问额外数据

额外数据可以通过 getOAuthState 在 OAuth 回调时钩子中访问。

这通常适用于 /callback/:id 路径以及通用 OAuth 插件回调路径(/oauth2/callback/:providerId)。

使用 after 钩子的示例:

auth.ts
import { betterAuth } from "better-auth";
import { getOAuthState } from "better-auth/api";

export const auth = betterAuth({
  // 其他配置...
  hooks: {
    after: [
      {
        matcher: () => true,
        handler: async (ctx) => {
          // 额外数据仅在 OAuth 回调时可用
          if (ctx.path === "/callback/:id") {
            const additionalData = await getOAuthState<{
              referralCode?: string;
              source?: string;
            }>();

            if (additionalData) {
              // 重要:使用前应验证并清理数据
              // 此数据来自客户端,不可完全信任

              // 示例:验证并处理推荐码
              if (additionalData.referralCode) {
                const isValidFormat = /^[A-Z0-9]{6}$/.test(additionalData.referralCode);
                if (isValidFormat) {
                  // 验证推荐码是否存在数据库中
                  const referral = await db.referrals.findByCode(additionalData.referralCode);
                  if (referral) {
                    // 可安全使用已验证的推荐码
                    await db.referrals.incrementUsage(referral.id);
                  }
                }
              }

              // 跟踪分析(低风险使用)
              if (additionalData.source) {
                await analytics.track("oauth_signin", {
                  source: additionalData.source,
                  userId: ctx.context.session?.user.id,
                });
              }
            }
          }
        },
      },
    ],
  },
});

使用数据库钩子的示例:

auth.ts
 // 你也可以在数据库钩子中访问额外数据
  databaseHooks: {
    user: {
      create: {
        before: async (user, ctx) => {
          if (ctx.path === "/callback/:id") {
            const additionalData = await getOAuthState<{ referredFrom?: string }>();
            if (additionalData?.referredFrom) {
              return {
                data: {
                  referredFrom: additionalData.referredFrom,
                },
              };
            }
          }
        },
      },
    },
  },

默认情况下,OAuth state 包含以下数据:

  • callbackURL - OAuth 流程的回调 URL
  • codeVerifier - OAuth 流程的 code verifier
  • errorURL - OAuth 流程的错误 URL
  • newUserURL - OAuth 流程的新用户 URL
  • link - OAuth 流程的关联链接(邮箱和用户 id)
  • requestSignUp - 是否为 OAuth 流程请求注册
  • expiresAt - OAuth state 的过期时间
  • [key: string]: 你在 OAuth 流程中传递的任何额外数据

处理没有邮箱的提供商

Better Auth 目前要求每条用户记录都必须有电子邮件地址。大多数提供商会在使用 email scope 时返回邮箱,但有些提供商在合法情况下可能不会返回。当这种情况发生时,OAuth 流程会失败并报 error=email_not_found(而对于 Generic OAuth 插件则是 error=email_is_missing)。

下表总结了每个受影响的提供商:何时可能缺少 email、在 mapProfileToUser 中可作为回退的稳定标识符,以及对提供商 email_verified 信号的可信度。

提供商何时可能缺少 email稳定回退 IDemail_verified 可信度
Discord仅手机号账户;未授予 email scopeprofile.id(雪花 ID)可靠(专用 verified 字段)
Apple第一次之后的每次登录(Apple 仅在首次同意时发出 emailprofile.sub(每个 Apple Team 稳定)可靠;中继地址也会被标记为已验证
GitHub用户将邮箱设为私密;GitHub App 缺少 “Email addresses” 权限profile.id(数字)可靠
Facebook即使授予了 email 权限,文件中仍没有有效邮箱profile.id(应用作用域)未知:Graph API 不提供逐邮箱验证标志
LinkedIn成员没有已确认邮箱;未授予 email scopeprofile.sub(每个应用的 pairwise)出现时可靠
Microsoft Entra ID托管用户没有 mail 属性,除非将 email 配置为 可选声明profile.oidprofile.tid(或 profile.sub不可信:Microsoft 明确警告 切勿将其用于授权

使用 mapProfileToUser 合成占位邮箱

email 字段为 null 或不存在时,回退到提供商的稳定 ID:

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

export const auth = betterAuth({
  socialProviders: {
    discord: {
      clientId: process.env.DISCORD_CLIENT_ID!,
      clientSecret: process.env.DISCORD_CLIENT_SECRET!,
      mapProfileToUser: (profile) => ({
        email: profile.email ?? `${profile.id}@discord.placeholder.local`,
      }),
    },
    apple: {
      clientId: process.env.APPLE_CLIENT_ID!,
      clientSecret: process.env.APPLE_CLIENT_SECRET!,
      mapProfileToUser: (profile) => ({
        email: profile.email ?? `${profile.sub}@apple.placeholder.local`,
      }),
    },
    microsoft: {
      clientId: process.env.MICROSOFT_CLIENT_ID!,
      clientSecret: process.env.MICROSOFT_CLIENT_SECRET!,
      mapProfileToUser: (profile) => ({
        email: profile.email ?? `${profile.oid}@entra.placeholder.local`,
      }),
    },
  },
});

合成邮箱只是占位符,不是真实联系地址。发送邮件的插件(密码重置、魔法链接、邮箱验证、组织邀请)无法投递到这些地址。请使用你控制的域名,或使用保留后缀如 .invalid.local,以免误发到真实邮箱。

提供商特定说明

  • Apple:第一次看到邮箱时请将其持久化保存。Apple 不提供用户信息端点,因此如果你在首次登录时不保存它,之后就无法再获取。email_verifiedis_private_email 都会被序列化为 字符串"true" / "false"),而不是布尔值。
  • GitHub:默认会请求 user:email scope。私密邮箱在 /user 上仍会返回 null;主要已验证地址可在 /user/emails 获取。
  • Microsoft Entra ID:由于 email 可随租户变化且从不验证,请使用 profile.oid(不可变、在租户内稳定)作为身份锚点;仅将 email 视为资料属性。Microsoft 的 声明验证指南 明确警告,切勿将 emailpreferred_usernameunique_name 用于授权决策。
  • Facebook:由于没有逐邮箱验证标志,除非你自行执行验证挑战,否则应将每个 Facebook 邮箱都视为未验证。

针对无邮箱用户的一等支持——使用稳定的 (providerId, accountId) 对作为身份键(与 OpenID Connect Core §5.7 一致)——已在 #9124 中跟踪。

提供商选项

clientId

提供商签发的 OAuth 2.0 Client ID。

对于通过受众(audience)验证 ID token 的提供商(Google、Apple、Microsoft Entra、Facebook、Cognito),你可以传入一个数组,以接受为任意已配置客户端签发的 token。Better Auth 驱动授权码流程时会使用数组中的第一个条目;验证 ID token 的 aud 声明时会接受所有条目。这使得你可以使用单一后端配置实现跨平台登录(Web、iOS、Android),其中每个平台的原生 SDK 都会使用各自的 Client ID 签发 token。

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

export const auth = betterAuth({
  // 其他配置...
  socialProviders: {
    google: {
      clientId: [
        process.env.GOOGLE_WEB_CLIENT_ID as string,
        process.env.GOOGLE_IOS_CLIENT_ID as string,
        process.env.GOOGLE_ANDROID_CLIENT_ID as string,
      ],
      clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
    },
  },
});

所有 Client ID 必须位于同一个提供商项目中,这样用户同意才能共享。对于不通过受众验证 ID token 的提供商,只接受单个字符串。

scope

访问请求的权限范围,例如 emailprofile

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

export const auth = betterAuth({
  // 其他配置...
  socialProviders: {
    google: {
      clientId: "YOUR_GOOGLE_CLIENT_ID",
      clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
      scope: ["email", "profile"],
    },
  },
});

redirectURI

提供商的自定义重定向 URI。默认使用 /api/auth/callback/${providerName}

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

export const auth = betterAuth({
  // 其他配置...
  socialProviders: {
    google: {
      clientId: "YOUR_GOOGLE_CLIENT_ID",
      clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
      redirectURI: "https://your-app.com/auth/callback",
    },
  },
});

disableSignUp

禁用新用户注册。

disableIdTokenSignIn

禁用使用 ID token 登录。默认对某些提供商(如 Google 和 Apple)启用。

verifyIdToken

验证 ID token 的自定义函数。

overrideUserInfoOnSignIn

一个布尔值,决定登录时是否覆盖数据库中的用户信息。默认是 false,即登录时不覆盖用户信息。如果希望每次登录都更新用户信息,设置为 true

mapProfileToUser

一个自定义函数,将提供商返回的用户信息映射到数据库中的用户对象。

当你希望从提供商的资料中填充用户对象的额外字段,或改变默认的映射方式时非常有用。

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

export const auth = betterAuth({
  // 其他配置...
  socialProviders: {
    google: {
      clientId: "YOUR_GOOGLE_CLIENT_ID",
      clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
      mapProfileToUser: (profile) => {
        return {
          firstName: profile.given_name,
          lastName: profile.family_name,
        };
      },
    },
  },
});

你可能想在 mapProfileToUser 中传入用户对象的额外字段,需先配置 user.additionalFields 选项。 (无状态认证设置同理)

refreshAccessToken

自定义刷新令牌的函数。此功能仅支持内置社交提供商(Google、Facebook、GitHub 等),暂不支持通过通用 OAuth 插件配置的自定义 OAuth 提供商。对于内置提供商,需按需提供刷新令牌的自定义函数。

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

export const auth = betterAuth({
  // 其他配置...
  socialProviders: {
    google: {
      clientId: "YOUR_GOOGLE_CLIENT_ID",
      clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
      refreshAccessToken: async (token) => {
        return {
          accessToken: "new-access-token",
          refreshToken: "new-refresh-token",
        };
      },
    },
  },
});

clientKey

你的应用客户端密钥。TikTok 社交提供商使用此项替代 clientId

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

export const auth = betterAuth({
  // 其他配置...
  socialProviders: {
    tiktok: {
      clientKey: "YOUR_TIKTOK_CLIENT_KEY",
      clientSecret: "YOUR_TIKTOK_CLIENT_SECRET",
    },
  },
});

getUserInfo

自定义函数,用于从提供商获取用户信息,覆盖默认的用户信息获取流程。

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

export const auth = betterAuth({
  // 其他配置...
  socialProviders: {
    google: {
      clientId: "YOUR_GOOGLE_CLIENT_ID",
      clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
      getUserInfo: async (token) => {
        // 自定义实现获取用户信息
        const response = await fetch("https://www.googleapis.com/oauth2/v2/userinfo", {
          headers: {
            Authorization: `Bearer ${token.accessToken}`,
          },
        });
        const profile = await response.json();
        return {
          user: {
            id: profile.id,
            name: profile.name,
            email: profile.email,
            image: profile.picture,
            emailVerified: profile.verified_email,
          },
          data: profile,
        };
      },
    },
  },
});

disableImplicitSignUp

禁用隐式注册新用户。启用后,登录时需要传入 requestSignUptrue 才能创建新用户。

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

export const auth = betterAuth({
  // 其他配置...
  socialProviders: {
    google: {
      clientId: "YOUR_GOOGLE_CLIENT_ID",
      clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
      disableImplicitSignUp: true,
    },
  },
});

prompt

授权码请求中使用的提示参数,控制认证流程行为。

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

export const auth = betterAuth({
  // 其他配置...
  socialProviders: {
    google: {
      clientId: "YOUR_GOOGLE_CLIENT_ID",
      clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
      prompt: "select_account", // 或 "consent", "login", "none", "select_account+consent"
    },
  },
});

responseMode

授权码请求的响应模式,决定授权响应的返回方式。

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

export const auth = betterAuth({
  // 其他配置...
  socialProviders: {
    google: {
      clientId: "YOUR_GOOGLE_CLIENT_ID",
      clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
      responseMode: "query", // 或 "form_post"
    },
  },
});

disableDefaultScope

移除提供商的默认权限范围。默认情况下,提供商会包含诸如 emailprofile 的权限。设置为 true 后,会移除默认权限,只使用你指定的权限。

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

export const auth = betterAuth({
  // 其他配置...
  socialProviders: {
    google: {
      clientId: "YOUR_GOOGLE_CLIENT_ID",
      clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
      disableDefaultScope: true,
      scope: ["https://www.googleapis.com/auth/userinfo.email"], // 只使用此权限
    },
  },
});

其他提供商配置

每个提供商可能还有额外选项,请查看具体提供商文档了解更多细节。