原生认证
面向原生客户端的 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 Session(
bearer({ requireSignature: true })), 与 Web Cookie 叠加共存。 - 用于原生邮箱验证、无密码登录和密码重置的 Email OTP(
emailOTP插件)——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.ts | Better Auth 配置:插件、Provider、限流 |
src/lib/auth-config/trusted-origins.ts | buildTrustedOrigins() + 原生请求识别 |
src/lib/auth-config/google-id-token.ts | Google ID Token 校验器(签名、签发者、过期、audience) |
src/server/app/shared/session.ts | requireAppSession()——把「无 Session」映射为 401 UNAUTHENTICATED |
.env.example | APP_SCHEME、GOOGLE_IOS_CLIENT_ID、GOOGLE_ANDROID_CLIENT_ID、APPLE_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-out或revoke-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-otp(type: "email-verification")→ email-otp/verify-email |
| 无密码登录 | email-otp/send-verification-otp(type: "sign-in")→ sign-in/email-otp |
| 密码重置 | forget-password/email-otp → email-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_ID | Web 客户端(重定向流程也用它) |
GOOGLE_IOS_CLIENT_ID | iOS App |
GOOGLE_ANDROID_CLIENT_ID | Android 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 端点——见账户删除。
