Files
wmailhub/docs/02_CLOUDFLARE_OAUTH.md
T
bigcode e7db6762fd fix(email): 开启 Email Routing 改用 PUT,并补上 zone-settings.write 权限
POST /zones/{id}/email/routing 不是有效路由,Cloudflare 返回
10405 Method not allowed for this authentication scheme。正确接口是
PUT /zones/{id}/email/routing(Apply Email Routing settings);
POST .../email/routing/enable 能用但官方已标 Deprecated。

该接口的 Accepted Permission 是 Zone Settings Write,而不是 Email Routing 权限族
—— 权限名和接口所属资源组对不上,Email Routing 四个权限全勾也不够。

- CF_OAUTH_SCOPES_DEFAULT / 探测候选集加 zone-settings.write;设置页文案同步
- cfapi 的 API 错误带上 Cloudflare 错误码(如 10405),便于区分路径错与权限错
- cf-mock 对非 PUT 的 /email/routing 返回 10405,旧写法会被打回
- 88/88 测试
2026-09-30 17:26:29 +08:00

7.5 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 选择器里的名称 用途
zone.read Zone Read 发现该账号下的域名,并顺带拿到账号 id 与账号名
workers-scripts.write Workers Scripts Edit 上传账号级 Email Worker 脚本
email-routing-rule.write Email Routing Rules Edit 配置 catch-all 规则指向 Worker
zone-settings.write Zone Settings Write 开启 Email Routing(见下)

「开启 Email Routing」不归 Email Routing 权限管(2026-09-30 实测):PUT /zones/{id}/email/routing 的 Accepted Permission 是 Zone Settings Write。所以 Email Routing 那一族权限全勾上也不够, 漏掉 zone-settings.write 会一直等到点「设置 Email Workers」才报出来。

顺带两个接口层面的坑:

  • 开启 Email Routing 用 PUT /zones/{id}/email/routing(Apply Email Routing settings)。 POST 到同一路径不是有效路由,返回 10405 Method not allowed for this authentication scheme; POST .../email/routing/enable 能用但官方已标 Deprecated。
  • catch-all 规则的请求体里,指向 Worker 的 action 是 { "type": "worker", "value": ["<脚本名>"] }, matchers 固定 [{"type":"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 实测)

GET /zones 的每个 zone 自带 account: {id, name},而该接口的 Accepted Permission 只有 Zone Read。 所以账号 id 和账号名都能从 zone 上直接读到:

  • 不需要 account-settings.read —— 它只覆盖 GET /accounts(列账号),本应用不再调这个接口;
  • 不需要 user-details.read —— 那是读「授权的那个人」(/user),跟 Cloudflare 账号无关。

实测该账号的 GET /zones 响应:

{ "id": "...", "name": "adx9.com",
  "account": { "id": "bf8cd983e6dfd171c1881b00b512ed5a", "name": "Phptits@msn.com's Account" } }

注意 account_id 这个旧字段在当前响应里已经是 null,读账号必须走 account.id。zone 上没有 account 时(非标响应)才回退:用 account_id + 第一个域名展示(如 example.com 等 3 个域名)。

授权配置自检(不用开浏览器)

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", "email-routing-rule.write"],
            "denied": ["zone-settings.read"], "probed": true, "note": null }

设置页把两列分别渲染成绿色/橙色标签,「待勾选」列就是需要在 OAuth App 选择器里补上的权限。