Authentication
Better Auth、公开端点保护、验证重置、独立 Cookie、Session 和管理员角色权限。
Authentication 默认开启但可关闭。Admin、User Assets、Developer API 和 Product AI 依赖它;Projects、Newsletter、Content、Analytics 和 Search 不依赖它。Web 的 features.authentication.emailAndPassword 默认 true 但可独立关闭;关闭时必须至少选择一个 Google/GitHub Provider。当前项目为 Google/GitHub-only,邮箱表单、重置路由、邮箱端点、Auth delivery 与 Resend 要求不进入 Web 产物;Session、Social Auth、RateLimit 和 Turnstile 保留。Admin 始终使用邮箱密码。
以下内容描述 Authentication 启用态。匿名隐私偏好由 Analytics 或 Newsletter 派生的 Consent 与隐私偏好 边界负责,不复用 Session 或认证 Cookie。
用户流程
- 用户通过 Manifest 启用的 Google/GitHub 登录;
emailAndPassword=true时也可注册邮箱、密码和显示名称。 - Better Auth 将
user、account和session写入 D1。 - 登录成功后,Worker 返回 HttpOnly 会话 Cookie。
- 后续受保护请求由同源 Worker 解析会话。
- 登出会撤销服务端 Session 并清除浏览器 Cookie;重放旧 Cookie 与过期 Session 都返回未登录结果。
- 仅在 Web 邮箱能力开启时,邮箱验证和密码重置请求通过注入的 delivery adapter 产生类型化 action URL;token 过期或消费一次后不能重放。
Web 的 Better Auth 路由位于 /api/auth/*。Admin 保留原 UI 需要的 /api/auth/login|register|logout|me 契约,并在 Worker 内映射到 Better Auth。
共享核心与应用边界
packages/auth 是 Web/Admin 唯一 Better Auth 配置源:固定 Drizzle/schema、可关闭的 Web 邮箱密码 8–128 字符、Session 7 天过期/每天刷新、应用级 Cookie prefix、认证消息契约和不转发原始参数的日志边界。apps/web/src/api/-internal/auth-runtime.ts 按 Manifest 注入 Social Provider,并只在邮箱能力开启时创建 delivery;apps/admin/src/server/auth.ts 固定传入 emailAndPassword=true。两者只注入 D1、应用身份、origin、secret、protection 和安全日志 sink,不再维护纯参数转发 adapter。
Web/Admin 都使用共享 D1 账号事实,但分别声明 base/trusted origins,并使用 tanplate-web.* / tanplate-admin.* Cookie;即使本地端口共享主机也不会复用 Session。
认证 handler 在进入 Better Auth 前校验请求 URL origin 与可选 Origin header;跨应用 origin 固定返回 403。应用仍分别拥有 runtime DB/secret、Worker route 和 Admin 兼容 envelope。
Web 公开写操作保护
Web 的保护清单包含 sign-in/social,并在邮箱能力开启时包含 sign-up/email、sign-in/email、send-verification-email、request-password-reset 与 reset-password。每次请求固定按“运行时 Schema → Rate Limiting → Turnstile → delivery/Better Auth”执行;任一步拒绝都不会创建用户、credential、Session、验证 token 或消息。Admin 的 /api/auth/login|register 兼容层不在该公开 protection 清单;授权后的 /api/admin/users mutation audit 与私有资源安全契约不把两类入口混为同一保护模型。
RateLimit adapter 只收到 auth:<operation>:<HMAC> 固定长度 key;HMAC 输入来自服务端 secret、固定操作与 Cloudflare CF-Connecting-IP,原始 IP、完整邮箱、密码、Cookie、Authorization 和 Turnstile token 不进入日志或响应。RateLimit/Turnstile denial 均返回 429 / AUTH_REQUEST_DENIED,adapter 故障返回 503 / AUTH_PROTECTION_UNAVAILABLE,输入畸形返回 400 / AUTH_REQUEST_INVALID;认证 UI 只显示本地化通用失败文案。
Local/Preview 显式注入确定性 adapter,不调用 Siteverify 或远程 limiter。Production 使用 Workers Rate Limiting binding 和服务端 Siteverify;Siteverify token 最长 2048 字符、五分钟有效且单次使用,服务端还校验 operation action 与 Web hostname,不自动重试。GET /api/auth/abuse-protection 仅向浏览器公开是否启用和 site key;登录、注册、重置 widget 每次提交后重建,避免 token replay。
邮箱验证与密码重置
emailAndPassword=true 时共享 Core 拦截验证/重置的公开 handler;关闭时相关邮箱路径统一返回 404,Social Provider capability 与登录继续可用。随机 token 只存在于 delivery message 的 actionURL;D1 verification 表只保存 purpose + SHA-256、用户 ID 和有效期。消费使用单条条件删除并返回用户 ID,因此 replay、过期或跨用途 token 都得到 400 / AUTH_TOKEN_INVALID_OR_EXPIRED。验证成功更新共享 user.email_verified;密码重置 action URL 打开 /reset-password 的本地化新密码表单,提交后使用 Better Auth credential 哈希并让旧密码立即失效。
Delivery adapter 同时声明 mode、单项 capability,并返回 sent、retryable-error 或 permanent-error。非成功结果会先撤销刚签发的 token,Web Worker 再写入只含 messageType 与 outcome 的 auth.delivery 结构化事件;公开请求仍返回与未知邮箱相同的 { status: true }。
账号隐私同时覆盖响应内容与响应时序。合法的验证邮件/密码重置请求统一经过 500ms 最低响应时长;用户查询后,token 签发与真实邮件投递由 Worker waitUntil 延续,不进入公开响应关键路径,因此慢 Provider 不会暴露账号是否存在。日志不包含 Provider、action URL 或完整收件人,也不自动重试。
Local Web 和 Wrangler preview environment 注入同一个 verification/reset React Email sink。GET /api/auth/capabilities 返回 deliveryMode: development 与两项 capability;GET /api/auth/development-messages 按 message.type 区分两类 HTML/text,均包含品牌、working action link、有效期与安全提示,且不发送真实邮件。Production 复用同一 renderer,经 Resend 最多发送一次;网络/限流/服务端错误映射为 retryable-error,其他 Provider 拒绝映射为 permanent-error,不自动重试。Production 不装配开发 sink,检查入口稳定返回 404。
Web 邮箱能力开启时必须声明 AUTH_DELIVERY_ENVIRONMENT。版本化 Wrangler/初始化器管理 top-level production 和 env.preview 的 preview,环境 CLI 对远程 target 校验对应值;Vite DEV/E2E 强制 local。所有 Production Authentication Profile 都要求 HTTPS BETTER_AUTH_URL、AUTH_RATE_LIMITER + account-unique namespace_id、公开 TURNSTILE_SITE_KEY 和 TURNSTILE_SECRET_KEY;只有邮箱能力或 Newsletter 开启时才要求 RESEND_API_KEY。Social-only profile 不读取或实例化 Resend adapter。
关闭 Authentication 时初始化器会移除这些 Production protection 引用。重新开启后,使用已有 Cloudflare 资源的公开标识显式恢复,不会自动创建远程资源:
pnpm init:template -- --auth-rate-limit-namespace-id <NAMESPACE_ID> --turnstile-site-key <SITE_KEY>不提供参数时 Local/Preview 的确定性 adapter 仍可工作,但 Production preflight 会因缺失 binding/site key 失败关闭。
pnpm test:e2e:web 使用独立 4184 Worker、随机密钥、专用 D1 和仅 DEV 注入的确定性 protection adapter,验证 Auth lifecycle、malformed/denied/unavailable、拒绝零邮件副作用及实际 reset action 改密。认证 UI 在 hydration、Session 和 protection capability 就绪前禁用表单。
管理员授权
进入页面和拥有普通会话并不等于管理员权限。Admin 业务 API 同时要求:
- 有效的 Admin origin Session;
- D1
user.email_verified = 1; - D1
user.banned不为真; - D1
user.role的逗号分隔角色集合包含admin。
ADMIN_EMAILS 只守护 bootstrap/recovery 的目标身份选择,不参与运行时授权。bootstrap 会幂等确保目标用户已验证、未封禁并持有 admin role。
所有 /api/admin/* route/method 必须登记到唯一合同,并由统一 actor 包装在任何 D1 读写前授权。无 Session 返回 401 / ADMIN_AUTHENTICATION_REQUIRED;未验证、已封禁或不含 admin role 返回 403 / ADMIN_AUTHORIZATION_REQUIRED。用户列表、创建、更新、删除、设置角色和封禁再由 Better Auth Admin plugin 检查各自 permission;客户端角色只用于导航体验。
Better Auth Admin plugin 的 HTTP endpoint 不作为产品 API 暴露。共享 Core 对公开 /api/auth/admin/* 固定返回 404,Admin 服务端通过专用 handler 调用插件,因此浏览器不能绕过当前管理员保护和 correlated audit。公开 health route 必须位于 /api/admin/* 之外。
Admin Playwright 使用随机测试 secret 和一次性 bootstrap 白名单,自动对合同内每个 method 验证 401/403,并证明普通角色和直接 Better Auth Admin endpoint 都被拒绝。测试不输出 Cookie、Session、密码或 token。
首个超级管理员
本地迁移后执行 pnpm admin:bootstrap:local,得到邮箱和密码均为 admin@6owen.com 的开发测试管理员。
远程 D1 使用 pnpm admin:bootstrap:remote,必须显式提供 ADMIN_BOOTSTRAP_EMAIL 和 ADMIN_BOOTSTRAP_PASSWORD,当前开发测试环境允许显式使用同一组凭据。邮箱还必须先由 init:template --admin-email 写入 Admin Wrangler bootstrap 白名单;命令会创建或提升已验证、未封禁、持有 admin role 的账号,并使用 Better Auth 的 credential 哈希格式。
用户管理
| 方法/动作 | 路径 | 作用 |
| ---------------------- | ------------------ | -------------------------------------------------------- | ------------------------------------------ |
| GET | /api/admin/users | 姓名/邮箱搜索、角色/状态筛选、五档页大小、分页与会话统计 |
| POST | /api/admin/users | 创建用户和 Better Auth credential |
| PATCH | /api/admin/users | 更新名称、邮箱和验证状态 |
| DELETE | /api/admin/users | 删除用户并级联 account/session |
| POST action=set-role | /api/admin/users | 设置 admin | user 角色 |
| POST action=ban | unban | /api/admin/users | 封禁撤销全部 Session,或清除封禁并恢复登录 |
管理页还提供角色/状态过滤、列显隐和 10/20/30/40/50 页大小。当前管理员不能删除、封禁或降权自己,也不能修改自己的邮箱或验证状态;这些保护同时存在于 UI 和服务端策略中。
Admin 安全审计
每个已授权用户 create/update/delete/set-role/ban/unban 尝试在业务结果确定后写入一条 correlated audit。记录只允许 actor ID、固定 action、业务层确认存在/成功返回且最多 128 字符的 target ID、typed result、RFC UUID correlation ID 和服务端时间;无效、缺失、not-found 或业务未确认的 target 固定为 null。schema、D1 CHECK 与查询 DTO 都不接受 request body、email、password、Cookie、token、secret、任意 error 文本或 Session。
审计写入是 best effort:失败会产生只含 correlation ID 的 admin.audit/write-failed observation,但不会重试业务 mutation,也不会把已完成的成功响应改成可重试错误。GET /api/admin/audit 复用同一 Admin route wrapper,最多返回 100 条 newest-first 记录;用户管理页的“审计记录” Dialog 默认查询最近 50 条。
Secret 要求
Admin 部署时 Web 与 Admin 必须配置相同的高强度 BETTER_AUTH_SECRET;只有 Web Auth 时仅 Web 需要它。Production Web 另需 TURNSTILE_SECRET_KEY;secret 只进入 .dev.vars 或 Cloudflare Worker secret,不写入 Git,site key 则是允许公开的 Wrangler variable。