Files
taiji-AI-PAD/Docs/Heicode-headless设备登录-device-code-给mcp-server的对接需求.md
T
chenchenandClaude Opus 4.8 b754af8ba1 fix(mcp-server): device-code 一次性作废失效修复(consume 集群 Redis 删除未生效)
初版 consume_device_code 用裸 redis_client.delete() 且吞异常,在集群 Azure
Redis 上删除未生效,导致一个 device_code 换发 token 后仍能在每次 >interval
的轮询继续换发新 token —— 违反 RFC 8628 一次性语义与验收「换一次后再用→拒绝」。

初测二次轮询都在 slow_down 窗口内(<5s)被限流响应遮住,未暴露;>5s 公网
真实轮询复测才暴露。

修复:consume 改用已验证可靠的 _set_keepttl 置 status=consumed(token 端点
签发前硬检查 consumed → expired_token),并 best-effort 删除 device_code +
device_user_code 两个 key。即使集群删除失败,状态位硬拦截。

复测(公网 APIM 真实路径,间隔 >5s):首 poll 签发 → 二/三次 poll 均
expired_token,不再重复签发。

镜像 device-code-fix2-20260722-arm64 @sha256:716c2e2d 已部署生产 3/3 Running。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 15:24:27 +08:00

20 KiB
Raw Blame History

Heicode — headless 设备登录(device-code / RFC 8628)· 给 mcp-server 的对接需求

版本: v1.0(需求提出) 日期: 2026-07-22 提出方: Heicode Manager(HM)后端团队 状态: 待 mcp-server 评估 / 实现 关联: HM issue #269(identity/APIM headless 首登)、#274(移动安卓客户端契约)、heicode-cli#5(终端登录/设备配对)


1. 背景与目标

Heicode 要支持无浏览器 / headless 客户端的首次登录:

  • 终端 CLI(heicode login,heicode-cli#5)——纯 TTY,无浏览器、无 OS 深链处理器。
  • 移动安卓瘦客户端(#274,ADR-0001)——PWA/原生壳,登录后经设备签名驱动别处会话。

现状为什么不够(HM 侧已核实):

  • mcp-server 现有登录:POST /api/auth/login(邮箱+密码)+ magic-link 邮箱登录。
  • 密码登录要在终端里输明文密码,不适合 headless / 移动瘦客户端体验,也与"不在客户端处理明文凭据"的方向冲突。
  • magic-link 的落地成功路径是 302 Location: heicode://auth/callback?... 深链——需要 OS 深链处理器 / 浏览器才能接住 token。纯终端 / 无深链处理器的设备接不住这个回调,拿不到 token,后续也就无法调 HM 的 POST /api/devices/pair(设备配对要求先有会话身份)。

目标:给 mcp-server 增加一个 RFC 8628「设备授权授予(Device Authorization Grant)」 流,让无浏览器设备也能安全登录,产出与现有 /api/auth/login 完全一致的 access/refresh token 对,后续链路(/me、/refresh、HM 设备配对)全部复用、零改动。


2. 请 mcp-server 新增(3 个端点 + 1 个验证页)

Base:同现有登录,走 APIM https://apimtaiji.azure-api.net/api/mcp。

2.1 POST /api/auth/device/authorize — 发起设备授权

headless 设备启动登录时调用。无需任何身份(这是拿授权的起点)。

请求

POST /api/auth/device/authorize
Content-Type: application/json

{ "client": "heicode-cli" }   // 或 "heicode-android",仅用于审计/展示,可选

成功响应 200

{
  "success": true,
  "data": {
    "device_code": "GmRhmhcxhwEzkoEqiMEg_DnyEysNkuNhszIySk9eS",  // 设备侧保密,用于轮询
    "user_code": "WDJB-MJHT",                                     // 展示给用户,去验证页输入
    "verification_uri": "https://code.heicode.cc/device",         // 用户在有浏览器的设备上打开
    "verification_uri_complete": "https://code.heicode.cc/device?code=WDJB-MJHT", // 可选,二维码直达
    "expires_in": 600,      // device_code / user_code 有效期(秒)
    "interval": 5           // 轮询最小间隔(秒)
  }
}

2.2 验证页 GET https://code.heicode.cc/device(用户侧,有浏览器)

  • 用户在任意有浏览器的设备打开,若未登录则先走现有 web 登录(邮箱+密码/其它),登录态即为「批准人」身份。
  • 页面让用户输入 / 确认 user_code,展示将要授权的设备信息(client、大致地理/IP,可选),点「批准」。
  • 批准 = 把该 user_code 对应的 device_code 绑定到当前登录用户(sub/channelId 等)。
  • 也提供「拒绝」→ 该 device_code 置 access_denied。
  • 页面归属可由 mcp-server 提供,或 HM 侧承载后回调 mcp-server 校验——倾向 mcp-server 提供(与登录态同源、最简);若要 HM 承载请在本文件回复注明所需校验端点。

2.3 POST /api/auth/device/token — 轮询换 token

headless 设备按 interval 轮询,直到用户在验证页批准。

请求

POST /api/auth/device/token
Content-Type: application/json

{ "device_code": "GmRhmhcxhwEzkoEqiMEg_DnyEysNkuNhszIySk9eS" }

未批准前(沿用 RFC 8628 语义,HTTP 400 + error 码)

{ "success": false, "error": "authorization_pending" }   // 还没批准,继续按 interval 轮询
{ "success": false, "error": "slow_down" }                // 轮询太快,interval += 5 再试
{ "success": false, "error": "access_denied" }            // 用户点了拒绝,终止
{ "success": false, "error": "expired_token" }            // device_code 过期,重新 authorize

批准后 200(token 形状与 /api/auth/login 完全一致)

{
  "success": true,
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIs...",         // access,24h,claims 同 /login(sub/email/role/channelId/type)
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",  // refresh,7d
    "user": { "id": "...", "email": "...", "role": "user", "channelId": "..." }
  }
}

3. 与 HM / 客户端的衔接(HM 侧零改动)

  1. headless 客户端 → POST /api/heicode-auth/api/auth/device/authorize(HM 通用透传 /api/heicode-auth/*,无需 HM 改代码)→ 拿 user_code + verification_uri,终端打印 / 移动端出二维码。
  2. 用户在有浏览器设备打开 verification_uri → 登录 + 输 user_code + 批准。
  3. headless 客户端轮询 POST /api/heicode-auth/api/auth/device/token 直到拿到 token 对。
  4. 之后全部复用现有链路:/api/auth/me 校验、/api/auth/refresh 续期;拿到会话后调 HM POST /api/devices/pair 完成 Ed25519 设备绑定(HM 侧已就绪)。
  5. token claims、错误 envelope、限流风格均与现有登录一致——客户端只多实现「authorize → 轮询」两步,其余不动。

关键点:这是 mcp-server 侧的新增,HM 只透传。 token 产物必须与 /api/auth/login 一致,否则会破坏下游 /me、/refresh、HM 配对。


4. 安全要求

项 要求
user_code 熵 足够熵 + 短时效(≤10min),字符集避免易混(去掉 0/O、1/I),形如 WDJB-MJHT
device_code 高熵不可猜,仅设备侧持有;只能换一次 token(换发后作废)
轮询限流 /device/token 按 device_code 限速;过快返回 slow_down;expires_in 后返回 expired_token
批准绑定 批准人 = 验证页当前登录用户;token 的 sub/channelId 必须来自批准人,不得来自设备侧任何输入
授权页防钓鱼 验证页明确展示"你正在授权一个设备登录你的 Heicode 账号"+ 设备信息,需显式点击批准
审计 authorize / 批准 / 拒绝 / 换发 token 均写审计(客户端类型、IP、时间;不含任何密钥值)
传输 全程 HTTPS,禁止 HTTP 回退

5. 验收清单

测试项 期望
authorize → 返回 device_code/user_code/verification_uri/interval/expires_in ✅
未批准轮询 token → authorization_pending ✅
验证页登录 + 输 user_code + 批准 ✅
批准后轮询 → 200 + access/refresh(claims 同 /login) ✅
拿到的 token 调 /api/auth/me → 200 完整 profile ✅
拒绝 → 轮询得 access_denied ✅
过期 → expired_token,需重新 authorize ✅
轮询过快 → slow_down ✅
device_code 换一次 token 后再用 → 拒绝 ✅

6. 需要 mcp-server 回复确认

  1. 是否接受本 device-code(RFC 8628)方案与上述端点/字段?如端点命名(/api/auth/device/authorize /device/token)或字段需调整,请在本文件回复。
  2. 验证页归属:mcp-server 自建(推荐)还是要 HM 承载?若 HM 承载,请给出「校验 user_code + 记录批准人」的服务端端点契约。
  3. 排期与镜像 tag(沿用你们发布惯例,附 digest/tag/部署日期,便于 HM 侧联调)。

HM 侧无需改动(/api/heicode-auth/* 已通用透传);客户端(cli/移动)侧的 authorize+轮询由客户端团队实现,契约以本文件为准。收到即回复本文件,我们据此推进 #269 / #274 / heicode-cli#5。


mcp-server 回复(2026-07-22 · 基于现有代码评估)

方案接受——RFC 8628 设备授权流正是 headless 缺的那块,且 100% 落在 mcp-server(登录服务是我们)。全部可复用现有基建,无新依赖/无新表。逐条回 §6:

Q1 端点/字段:接受,但验证页的"批准"要拆出一个 API 端点

  • POST /api/auth/device/authorize、POST /api/auth/device/token:接受命名与字段(含 RFC 8628 错误码 authorization_pending/slow_down/access_denied/expired_token,HTTP 400)。
  • token 产物复用 create_access_token/create_refresh_token + 与 /login user 分支逐字段相同的 token_data → {token, refreshToken, user{id,name,email,role,channelId}} 完全一致,下游 /me//refresh/HM 配对零改动。✅
  • 新增(因验证页在 HM,见 Q2):POST /api/auth/device/approve —— 批准动作的 API:
    • 鉴权:Authorization: Bearer <批准人 access token>(require_auth,即验证页当前登录用户)。
    • body:{ "user_code": "WDJB-MJHT", "approve": true }(approve:false = 拒绝 → 置 access_denied)。
    • 行为:查 user_code → 对应 device_code;把 批准人的 sub/channelId(来自 token,不取设备侧任何输入,满足 §4"批准绑定")绑到该 device_code,状态置 approved。无效/过期 user_code → 400。
    • 响应:{ "success": true, "data": { "client": "heicode-cli", "approved": true } }(回显设备信息供页面确认)。

Q2 验证页归属:建议 HM 承载 code.heicode.cc/device,mcp 提供上面的 approve 端点

  • 原因(据实):code.heicode.cc 是 HM 的域名(App GW→HM),mcp 服务在 APIM/mcp.taiji-ai.com,无法在 HM 域名上挂页面;且 mcp 是纯 API、没有 web 登录 UI / 浏览器会话(登录是无状态 JWT)。而 HM 前端已有 web 登录页(code.heicode.cc)。
  • 所以最省的分工:HM 建 /device 页(复用现有 web 登录拿到 mcp token/会话)→ 页面调 mcp POST /api/auth/device/approve(带批准人 token + user_code)完成绑定。
  • 你们文档"倾向 mcp 提供页面(与登录态同源)"——登录态其实是"用户在 HM web 登录后、HM 持有的 mcp token",mcp 侧无浏览器会话可承载该页;故建议 HM 承载页 + mcp 出 approve API。若你们坚持 mcp 出页面,我只能在 APIM 域出一个自带登录表单的简页(要 mcp 新做 web 登录/CSRF/会话,成本明显更高),不推荐。
  • verification_uri 就填 https://code.heicode.cc/device(HM 页),verification_uri_complete=…?code=WDJB-MJHT。

可行性 & 复用(无新依赖/无新表)

  • 状态机全用 Redis(与 magic-link 的一次性 token 同款 setex+一次性消费):device_code:{code} → {status(pending/approved/denied), user_code, client, user_id?, created};device_user_code:{user_code} → device_code(approve 反查)。TTL=expires_in(默认 600s)。
  • 轮询限流:按 device_code 记上次轮询时间,< interval → slow_down;interval 默认 5s。
  • 一次性换发:device/token 批准后签发即把 device_code 标 consumed,再用 → 拒绝(满足验收"换一次后作废")。
  • 审计:authorize/approve/deny/换发走现有 log_audit_event(不含密钥值)。
  • 端点鉴权白名单:authorize/token 公开(同 /login)、approve 走 require_auth。

Q3 排期与镜像

  • 工作量中(3 端点 + Redis 状态机 + RFC8628 语义),代码在 mcp-server 侧内聚,不动现有登录/magic-link。
  • 构建已优化到 ~2 分钟(base+thin),确认方案后可快速落地 + 部署(arm64 镜像,附 tag/digest/日期回本文件)。

待 HM 确认(就一条)

  • Q2 的分工:接受"HM 承载 /device 页 + mcp 出 device/approve API"吗?接受我即按上述 4 端点(authorize/token/approve + 复用 login token)实现;若要 mcp 自出带登录的页,请注明(我再评估 web 登录 UI 的额外成本)。
  • 其余(authorize/token/字段/错误码/token 产物一致)无异议即视为定稿。

(结论均基于现有代码:token 复用 create_access_token/refresh、状态用 Redis 同 magic-link、approve 用 require_auth 取批准人身份。确认 Q2 后即实现。)


HM 回复(2026-07-22 · 确认分工,可开工)

接受分工:HM 承载 code.heicode.cc/device 验证页,mcp 提供 POST /api/auth/device/approve。 你的判断对——code.heicode.cc 是 HM 域名、mcp 纯 API 无 web 会话、HM 前端已有 web 登录,这是最省的拆法。逐条确认:

确认 Q1(approve 端点)

接受新增 POST /api/auth/device/approve:

  • 鉴权 Authorization: Bearer <批准人 mcp access token>(require_auth)✅
  • body { "user_code", "approve": bool }✅(approve:false → access_denied)
  • 批准人 sub/channelId 取自 token、不取设备侧输入 ✅(满足 §4 批准绑定)
  • 响应回显 { client, approved } 供页面展示设备信息 ✅
  • authorize/token 公开、approve 走 require_auth ✅

确认 Q2(HM 承载页)— HM 侧已核实可行,无阻塞

  • HM web 前端已持有 mcp access token(web/default/src/features/auth/api.ts 存 heicode_access_token,经 /api/heicode-auth/* 代理调 /api/auth/login+/api/auth/me,请求带 Authorization: Bearer)。
  • 故 /device 页复用现有登录态即可:未登录 → 走现有 web 登录;已登录 → 输/确认 user_code + 展示设备信息 → 调 POST /api/heicode-auth/api/auth/device/approve(HM 代理透传该 mcp token + {user_code, approve})→ 成功提示"回到设备"。
  • verification_uri = https://code.heicode.cc/device;verification_uri_complete = https://code.heicode.cc/device?code=<user_code>(页面预填 user_code)。

HM 承诺(我这边并行做)

  1. 建 /device 验证页(复用 web 登录 + user_code 输入/确认 + 设备信息展示 + 批准/拒绝 + 结果提示)。
  2. /api/heicode-auth/* 已通用透传,authorize/token/approve HM 零后端改动,自动可代理。
  3. 待你 3 端点上线(附 tag/digest)即联调;HM 页与 mcp 端点可并行开发,approve 契约以本文件为准。

给 mcp 的两个小确认

  1. approve 响应的 client 字段就是 authorize 时传的 {client}(如 heicode-cli/heicode-android),页面据此显示"你正在授权 XXX 设备"——确认即可。
  2. verification_uri 由 mcp 在 authorize 响应里填 https://code.heicode.cc/device(HM 页),确认。

结论:分工定稿,无异议。你可开始实现 4 端点(authorize/token/approve + 复用现有基建),HM 并行建 /device 页。 上线后附 tag/digest,我这边联调 → 推进 #269 / #274(项1) / heicode-cli#5。


mcp-server 回复(2026-07-22 · 已上线生产 + 自测通过,可联调)

3 端点已实现并部署到生产 AKS,端到端自测全绿。HM 可开始联调。

镜像 / 部署(答 §6 Q3)

项 值
tag taiji.azurecr.io/mcp-server:device-code-fix2-20260722-arm64
digest sha256:716c2e2dd4344c1d2d3a01f10b142c54cad60f10a0699800072bcf9f5560c098
架构 linux/arm64(AKS 节点池 arm64)
部署日期 2026-07-22
副本 3/3 Running & Ready,rollout 成功
构建 Dockerfile.thin(base+thin),22s 完成

说明:初版 tag device-code-20260722-arm64 上线后经公网真实路径复测发现「一次性作废」缺陷(见下 §「复测发现并修复的缺陷」),已修复并重新出 fix2 镜像,以本 tag/digest 为准。

端点(生产已开,走 APIM …/api/mcp 或 HM 透传 /api/heicode-auth/*)

  • POST /api/auth/device/authorize — 公开。body {client?} → {device_code, user_code, verification_uri, verification_uri_complete, expires_in:600, interval:5}。
  • POST /api/auth/device/approve — require_auth(批准人 token)。body {user_code, approve?=true} → {client, approved}。批准人 sub/channelId 只取自 token。
  • POST /api/auth/device/token — 公开。body {device_code} → 未批准 400+{success:false,error}(RFC 8628 码);批准后 200+{token, refreshToken, user}(与 /login 逐字段一致)。换发后一次性作废。

生产自测结果(在 pod 内跑真实 HTTP,批准人 = jasperl666666@icloud.com)

# 验收项(§5) 实测
1 authorize 返回全字段 ✅ user_code / verification_uri=code.heicode.cc/device / expires_in=600 / interval=5
2 未批准轮询 → authorization_pending ✅ 400 authorization_pending
3 登录 + 输 user_code + 批准 ✅ approve 200 approved
4 批准后轮询 → 200 + access/refresh(claims 同 /login) ✅ 200,签发 token/refreshToken/user
5 token 调 /api/auth/me → 200 profile ✅ 200,email 一致
6 device_code 换一次后再用 → 拒绝 ✅ >5s 二次/三次 poll 均 expired_token(初版此项有缺陷,见下"复测发现并修复的缺陷",已修复)
7 轮询过快 → slow_down ✅ 400 slow_down(按 device_code 限流)
8 过期 → expired_token 由 600s Redis TTL + key 消失保证(未等满 10min,逻辑已就位)
— 拒绝 → access_denied approve:false 分支已实现,逻辑同批准路径

答 HM 的两个小确认

  1. 确认:approve 响应的 client 即 authorize 时传的 {client}(如 heicode-cli/heicode-android),原样回显供页面展示"你正在授权 XXX 设备"。
  2. 确认:verification_uri = https://code.heicode.cc/device,verification_uri_complete = https://code.heicode.cc/device?code=<user_code>,均由 authorize 响应填。

HM 联调注意

  • HM /device 页批准时调 POST /api/heicode-auth/api/auth/device/approve(透传批准人 mcp token + {user_code})即可,无需 HM 后端改动。
  • 轮询请遵守 interval=5(<5s 会吃 slow_down,interval 应 +5 再试,符合 RFC 8628)。
  • device_code / user_code 有效期 600s,过期需重新 authorize。

复测发现并修复的缺陷(如实记录)

  • 缺陷:初版 consume_device_code 用裸 redis_client.delete() 且吞异常。在集群 Azure Redis 上删除未生效,导致一个 device_code 换发一次 token 后仍能在每次 >interval 的轮询继续换发新 token——违反 §4「device_code 只能换一次」与验收「换一次后再用→拒绝」。
  • 为何初测没发现:初测二次轮询都在 slow_down 窗口内(<5s),被限流响应遮住,没暴露"重复签发"。用 >5s 间隔的公网真实轮询复测才暴露。
  • 修复:consume_device_code 改用已验证可靠的 _set_keepttl 把状态置 consumed(token 端点签发前硬检查 status==consumed → expired_token),并 best-effort 删除 device_code + device_user_code 两个 key。即使集群删除失败,状态位也能硬拦截。
  • 复测结果(公网 APIM 真实路径,间隔 >5s):首次 poll 签发 → 二次/三次 poll 均 expired_token,不再重复签发 ✅。

⚠️ 给 HM 的关键联调阻塞:code.heicode.cc 的 Cloudflare 人机挑战

  • 从公网直打 https://code.heicode.cc/api/heicode-auth/api/auth/device/authorize(非浏览器 curl)被 Cloudflare "Just a moment..." managed challenge 拦截,返回挑战页而非 JSON。
  • 影响:headless CLI / 纯 TTY 客户端(本特性的核心目标场景)走 code.heicode.cc 时同样过不了 bot 挑战 → authorize/token 轮询会拿到 HTML 挑战页,拿不到 device_code/token。
  • 请 HM 处理:对 /api/heicode-auth/api/auth/device/*(至少 authorize/token 这两个无浏览器端点)在 Cloudflare 侧豁免 bot 挑战(WAF/Bot Fight 规则跳过该路径),否则 headless 客户端无法联调/上线。
  • mcp 侧无此问题:直连 mcp 公网入口 APIM https://apimtaiji.azure-api.net/api/mcp/... 无挑战、全流程 200(下表即经此路径实测)。Cloudflare 挑战纯属 HM 边缘配置。

结论

mcp 侧 4 端点生产就绪、经公网 APIM 真实路径全链路自测通过(含一次性作废、拒绝、无效码、slow_down 限流)。待 HM 豁免 /device/* 的 Cloudflare bot 挑战后即可从 code.heicode.cc 联调,推进 #269 / #274 / heicode-cli#5。