- 上一版默认 scope 名是猜的(account.read / email_routing.write),全部被拒。 正确词表是点号分隔的 <group>.<verb>,且: - 账号是 account-settings.read(选择器里叫 Account Settings Read),不是 account.read - Workers 脚本是 workers-scripts.write,不是 workers_scripts.write - Email Routing 在选择器里是四个独立权限,改 catch-all 对应 email-routing-rule.write - account:read 这种冒号写法是 wrangler 的第一方简写,第三方 client 一律被拒 - client 注册的 scope 是上限不是默认:没勾的权限即使账号有也不生效 - preflight 扩展 scope 探测:授权端点在同意页之前就校验并点名拒绝的 scope, 逐轮「请求剩余候选 -> 剔除被点名项」即可收敛出 App 已授权集合(未认证请求, 不登录/不产生授权),结果按 allowed/denied 返回并在设置页用绿/橙标签展示 - 缺少账号级读权限时不再失败:discover() 先试 /accounts,403 则改为列 token 可见的 /zones, 用 zone 自带的 account_id 反推账户(展示名用第一个域名) - 默认 scope 收敛为接入必需的三项(zone.read / workers-scripts.write / email-routing-rule.write),account-settings.read 改为可选:勾了才显示真实账号名 - 实测:线上 preflight 已 ok=true(Cloudflare 跳登录页),无需再改 App 即可「添加账号」
6.3 KiB
Cloudflare OAuth 与自动接入(docs/11 §14)
旧「Add Node」概念已删除,改为 Connect Cloudflare Account。
流程
Console
|
v
Cloudflare OAuth
|
v
用户授权 Account
|
v
Console
+-- 保存 Account
+-- Account / Zone 发现
+-- 配置 Domain(同步账号下全部域名,pending)
+-- 创建 catch-all Mailbox
+-- 部署账号级 Email Worker
+-- 签发 WorkerCredential(secret 注入 Worker 脚本)
用户不需要执行安装命令,也不手工创建 API Token。
安全
- Cloudflare credential(OAuth access token)AES-GCM 加密存储,不进入前端和日志。
- OAuth state 一次性(用后即删)、5 分钟过期、绑定账号(CSRF 防护)。
- 授权回调不校验会话:浏览器从
dash.cloudflare.com跳回 Console 属于跨站顶级导航,SameSite=Strict的会话 cookie 不会携带;该端点由一次性 state 承担 CSRF 防护,其余/api/oauth/*仍要求登录。 - Account ownership 以 token 实际可访问的 accounts 为准。
- 安装过程幂等,重复执行不产生重复资源。
- Worker credential 与 Cloudflare credential 分离;轮换 credential 后已配置域名自动重新部署。
前置条件:OAuth 客户端凭据
授权所需的 Client ID / Secret 有两个来源,settings 表优先、env 兜底:
| 来源 | 配置位置 | 说明 |
|---|---|---|
| settings(优先) | Console「设置 → Cloudflare 接入」 | Secret 以 AES-GCM 密文存 settings.cf_oauth_client_secret_enc,只回显末 4 位;保存后立即生效,无需重新部署。清除走 DELETE /api/settings/cf-oauth |
| env(兜底) | CF_OAUTH_CLIENT_ID / CF_OAUTH_CLIENT_SECRET |
部署时注入;settings 未配置或密文损坏时使用 |
两个来源都没有时,/api/oauth/cloudflare/start 与 /api/oauth/cloudflare/callback 一律返回 503
—— 系统不提供任何模拟/降级通道,不会出现"看起来接入成功"的假账号与假资源。
GET /api/settings 会返回 cf_oauth.source(settings / env / none)与 redirect_uri(需在
Cloudflare Dashboard 登记的回调地址),GET /api/oauth/mode 返回 source 供账号页判断。
Cloudflare OAuth App 配置清单(docs 依据:Cloudflare "Create OAuth Client" API)
在 https://dash.cloudflare.com/profile/oauth-apps 新建应用后,必须完成下面几项,否则授权端点
会直接拒绝(此时 Console 收不到 code,只有 error=invalid_scope / invalid_request):
| 字段 | 填什么 | 不对时的报错 |
|---|---|---|
| Redirect URL(s) | https://wmailhub.adx9.com/api/oauth/cloudflare/callback(本地联调另加 http://127.0.0.1:18787/api/oauth/cloudflare/callback) |
invalid_request: redirect_uri does not match |
| Client URI | Console 站点首页,如 https://wmailhub.adx9.com |
promotion to public requires a verified client_uri host |
| 域名校验 | 按 Cloudflare 提示在 client_uri 域下加 TXT 记录(形如 cloudflare_oauth_client_publisher=...) |
同上,一直卡在 not verified |
| Promote to public | 应用可见性从 private 提升为 public | 私有 App 不可被他人授权 |
| Scopes | 与 Console「设置 → Cloudflare 接入」里的 scope 字段逐字一致 | invalid_scope: Client is not allowed to request scope '...' |
scope 词表与选择器的对应关系
Cloudflare 第三方 OAuth 客户端的 scope 是 点号分隔 的 <group>.<verb>。account:read 这种冒号写法
是 wrangler 的第一方简写,第三方客户端请求会被一律拒绝(它只对第一方 client 生效)。权威列表是
GET /client/v4/oauth/scopes,选择器里是它的可勾选项。
本应用默认请求这四个(已在 Cloudflare 侧逐个验证可被接受):
| scope id | 选择器里的名称 | 用途 |
|---|---|---|
account-settings.read |
Account Settings Read | 列出账号(拿账号名) |
zone.read |
Zone Read | 发现该账号下的域名 |
workers-scripts.write |
Workers Scripts Edit | 上传账号级 Email Worker 脚本 |
email-routing-rule.write |
Email Routing Rules Edit | 开启 Email Routing + 配置 catch-all 规则 |
两个容易踩的点:
- Email Routing 在选择器里是四个独立权限(
Email Routing Account Rules/Addresses/Rules/Suppressions),不是笼统的"email routing 写"。改 catch-all 规则对应上面表里的email-routing-rule.write。 - 客户端注册的 scope 是上限,不是默认值。App 里没勾的权限,即使账号本身有,授权时也不会生效; 反过来请求里不写某个 scope,即使 App 勾了也不会被授予。
缺少账号级读权限时的降级(2026-09-30 实测后加入)
若 App 没有勾 account-settings.read,GET /accounts 会 403。Console 会自动降级:直接列 token 可见的
/zones,用 zone 自带的 account_id 反推目标账户,账号展示名用第一个域名代替(如 example.com 等 3 个域名)。
因此只勾 zone.read + workers-scripts.write + email-routing-rule.write 也能完成接入,只是账号名显示为域名。
授权配置自检(不用开浏览器)
GET /api/oauth/cloudflare/preflight 用当前配置请求一次 Cloudflare 授权端点但不跟随跳转,据跳转目标判定:
- 跳到登录/同意页 →
ok: true,配置可用; - 弹回我们的回调地址,或跳
dash.cloudflare.com/oauth/error→ok: false并带回error/error_description与一条处置建议。
设置页「Cloudflare 接入」tab 的「检测授权配置」按钮就是调它。纯只读探测,不会产生任何授权。
scope 探测:直接算出"哪些已授权、哪些还要勾"
授权端点在显示同意页之前就校验 scope,拒绝时通过 error_description 点名是哪一个
(... not allowed to request scope 'xxx')。于是可以逐轮「请求剩余候选 → 剔除被点名的那一个」,
收敛出该 App 实际持有的 scope 集合——每轮只是一个未认证请求,无需登录、无需同意、不产生任何授权。
preflight 在收到 invalid_scope 时会自动跑完这个探测,并在结果里返回:
"scopes": { "allowed": ["zone.read", "workers-scripts.write"],
"denied": ["account-settings.read"], "probed": true, "note": null }
设置页把两列分别渲染成绿色/橙色标签,「待勾选」列就是需要在 OAuth App 选择器里补上的权限。