Unified Platform Host

原生认证

面向原生客户端的 Bearer Session、Email OTP 与 Apple/Google ID Token 登录。

原生客户端与 Web 应用认证于同一个 Better Auth 实例——同一套 /api/auth/* 端点、同一张 user 表、同一批 Session。Web 的 Cookie Session 保持不变;原生 客户端叠加了 Bearer token 传输、Email OTP 流程,以及 Apple/Google 的 ID Token 登录。全部配置在 src/lib/auth.ts 中。

提供的能力

  • 面向原生客户端的 Bearer Sessionbearer({ requireSignature: true })), 与 Web Cookie 叠加共存。
  • 用于原生邮箱验证、无密码登录和密码重置的 Email OTPemailOTP 插件)——Web 保留链接式邮件。
  • 基于原生 ID Token 的 Apple 登录,按 iOS Bundle ID 校验——无需 Services ID 或 Apple OAuth client secret。
  • 基于 ID Token 的 Google 登录,带 Web/iOS/Android audience 白名单
  • App scheme 的受信任 Origin,以及敏感认证端点的按端点限流

重要文件

文件作用
src/lib/auth.tsBetter Auth 配置:插件、Provider、限流
src/lib/auth-config/trusted-origins.tsbuildTrustedOrigins() + 原生请求识别
src/lib/auth-config/google-id-token.tsGoogle ID Token 校验器(签名、签发者、过期、audience)
src/server/app/shared/session.tsrequireAppSession()——把「无 Session」映射为 401 UNAUTHENTICATED
.env.exampleAPP_SCHEMEGOOGLE_IOS_CLIENT_IDGOOGLE_ANDROID_CLIENT_IDAPPLE_APP_BUNDLE_IDENTIFIER

Bearer Session

bearer 插件在登录/注册的响应中通过 set-auth-token header 下发 Session token。原生客户端存储它(iOS 存 Keychain),并在每个请求携带 Authorization: Bearer <token>

POST /api/auth/sign-in/email        ← { email, password }
  200,header set-auth-token: <token.signature>

GET /api/app/v1/todos
  Authorization: Bearer <token.signature>

关键行为:

  • requireSignature: true——只接受签名 token。
  • Session 存储在数据库中且可撤销sign-outrevoke-other-sessions 之后,旧 token 立即失效。
  • 任何响应携带新的 set-auth-token 时,客户端必须覆盖已存储的 token。
  • Web Cookie Session 不受影响——插件是叠加式的。

Better Auth 的 get-session 对失效或已撤销的 token 返回 200 + null body,而不是 401。App API 的 requireAppSession 守卫把「无 Session」 统一转换为 401 UNAUTHENTICATED,业务端点因此行为正确。不要依赖 /api/auth/get-session 的状态码做鉴权判断。

受信任 Origin

Better Auth 会拒绝没有受信任 Origin 的请求(403 MISSING_OR_NULL_ORIGIN)。 原生客户端把自己的 App scheme 作为 Origin 发送,因此必须注册:

变量用途
APP_SCHEME生产 App scheme(如 soar://)——始终受信任
DEV_TRUSTED_ORIGINS逗号分隔的额外 Origin,仅在开发环境生效

buildTrustedOrigins() 在生产环境只信任 APP_SCHEME;wildcard 和 localhost 条目从构造上就只属于开发环境。

Email OTP

原生 App 无法点击邮件链接,因此 emailOTP 插件提供验证码流程(6 位数字、 5 分钟过期、最多 3 次尝试):

流程端点
验证邮箱email-otp/send-verification-otptype: "email-verification")→ email-otp/verify-email
无密码登录email-otp/send-verification-otptype: "sign-in")→ sign-in/email-otp
密码重置forget-password/email-otpemail-otp/reset-password

Web 保留链接式验证与重置邮件:当请求来自 App scheme 时 (isNativeAppRequest),sendVerificationEmail 会跳过链接邮件,原生用户只 收到 OTP;autoSignInAfterVerification 让刚完成验证的原生用户立即获得 Session。

Apple 登录(仅原生)

只有设置了 APPLE_APP_BUNDLE_IDENTIFIER 才会注册 Apple 登录,且只接受原生 ID Token:

POST /api/auth/sign-in/social
{ "provider": "apple", "idToken": { "token": "<identityToken>", "nonce": "<sha256(rawNonce)>" } }
  • Token 的 audience 必须等于 iOS Bundle Identifier——这就是全部客户端配置; 不需要 Apple Services ID 或 OAuth client secret。
  • Better Auth 直接比较请求 nonce 与 token 的 nonce claim,而 Apple 在 claim 中 存储的是 SHA-256(rawNonce)——因此客户端必须发送哈希后的 nonce。
  • Web 界面不提供 Apple 登录。

Google 登录(多客户端)

Web 登录继续使用 GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET 的标准 OAuth 重定向流程。原生客户端改为发送 ID Token:

POST /api/auth/sign-in/social
{ "provider": "google", "idToken": { "token": "<googleIdToken>" } }

原生 Google ID Token 的 aud平台客户端 ID,因此 src/lib/auth-config/google-id-token.ts 注入自定义 verifyIdToken,校验签名 (Google JWKS)、签发者、过期时间,以及由下列变量构成的 audience 白名单

变量Audience
GOOGLE_CLIENT_IDWeb 客户端(重定向流程也用它)
GOOGLE_IOS_CLIENT_IDiOS App
GOOGLE_ANDROID_CLIENT_IDAndroid App

请在同一个 Google Cloud 项目中创建所有客户端,让它们共享同一个 OAuth 同意屏幕。account.accountLinking.enabled 会把同邮箱的 Apple、Google 和密码 账户映射到同一个 Better Auth 用户

限流

rateLimit.customRules 收紧敏感端点(生产环境启用):登录、注册、密码重置、 修改密码/邮箱以及所有 OTP 端点都有较小的每分钟配额;客户端遇到 429不得自动重试登录或 OTP 请求。

客户端规则(原生实现必须遵守)

  • Token 只存安全存储(Keychain——绝不进 UserDefaults 或日志)。
  • 任何响应出现 set-auth-token,都视为「替换已存储的 token」。
  • 收到 401:清除 token 与本地状态并跳转登录。不做重试循环,也不静默 回退到另一套后端。
  • 发送 Content-Type: application/json;即使 POST body 为空也要发 {}
  • 账户删除走 DELETE /api/app/v1/account(级联清理),而不是 Better Auth 端点——见账户删除

相关页面

本页目录