Skip to content

feat(mcp): OAuth 2.1 授权码流——发现/动态注册/PKCE/keychain 存储/401 刷新(P1-③) - #611

Open
coder-hhx wants to merge 4 commits into
mainfrom
feat/mcp-oauth
Open

feat(mcp): OAuth 2.1 授权码流——发现/动态注册/PKCE/keychain 存储/401 刷新(P1-③)#611
coder-hhx wants to merge 4 commits into
mainfrom
feat/mcp-oauth

Conversation

@coder-hhx

@coder-hhx coder-hhx commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Closes #610

概述

2026 H2 能力路线图 交付 P1-③ MCP OAuth 2.1:remote MCP server(http/sse)从「仅静态 headers」升级为完整 OAuth 授权码流——发现、动态注册、PKCE、keychain 存储、401 自动刷新。附设计文档与完整测试。

设计要点(设计文档

对 roadmap 假设做了四处现实修正(均已核对代码):仓库无 mcp.rs SDK(transport 为自研阻塞式 JSON-RPC,OAuth 直接挂 HttpTransport/SseTransport);不引 oauth2 crate(适配胶水超过手写协议本身,token 交换只是两个 form POST);auth 不设 "headers" 枚举值(静态 headers 已是独立字段,避免存量迁移);Linux 无 secret-service 时诚实降级 0600 文件(加密密钥无处安放,加密是安全剧场)。

核心取舍:授权流只由显式用户手势触发(卡片 Connect),transport 内 401 只做静默刷新 + 标记性错误,绝不弹浏览器——对话中途并发工具调用不会弹 N 个窗口。

实现

Rust(services/mcp_oauth/ 四模块 + transport 集成)

  • discovery:401 WWW-Authenticate → RFC 9728 PRM(挑战参数优先,退 well-known 路径插入推导)→ RFC 8414 AS 元数据(路径感知候选序列 + OIDC fallback)→ 全败退 2025-03-26 旧规范默认端点;强制校验 S256 支持
  • register:RFC 7591 动态注册(token_endpoint_auth_method: "none" 公共客户端);静态 clientId 跳过
  • flow:PKCE(S256) + state 恒等校验、127.0.0.1:0 loopback one-shot 回调(5 分钟超时,state 不符 400 拒绝但不终止等待)、RFC 8707 resource 受众绑定、refresh token 轮换即持久化
  • store:keyring v3(macOS Keychain / Windows Credential Manager / Linux secret-service)+ 文件降级 + 进程内缓存(避免每请求 Keychain IPC);TokenRecord 内嵌 server_url,与配置不符即视为无 token(防 audience 串用)
  • transport:每请求 ensure_bearer 注入(将过期主动刷新,单飞互斥);401 → Unauthorized 变体 → 被动刷新一次重试 → 不可行报带 MCP_OAUTH_AUTHORIZATION_REQUIRED 标记的引导错误;SSE GET 长连每次重连取最新 token(不固化 spawn 时刻值)
  • 命令:mcp_oauth_authorize/status/clearmcp_test_server 响应附 oauth 诊断(状态/过期/存储后端,永不含 token 本体
  • 出站纪律:全部 OAuth HTTP 走 system_proxy::blocking_client_builder(),代理异常 fail fast

TS/UI

  • McpServerConfig.auth: { type: "none"|"oauth", scope?, clientId? }——token 永不进 settings,Gateway 同步与 WebDAV 备份天然不含凭据,零脱敏改造
  • normalize:type:"oauth" 才落壳对象,"none"/未知/非对象一律不存(旧配置形态零变化)
  • 编辑弹窗(http/sse):鉴权方式 / Scope / Client ID 三字段 + 引导文案
  • 卡片:授权状态徽章(未授权/已授权/已过期,语义 variant)+ 连接/重新授权/断开授权;WebUI 仅状态展示(授权只能在桌面完成)
  • 删除 server 双路径(卡片确认 + McpManager delete action)同步清 keychain

端到端实测(mock OAuth AS + MCP server,全流程闭环)

mock server 端日志(401 挑战 → 发现 → DCR → PKCE 校验 → Bearer 访问):

05:19:29.985 MCP request without valid Bearer → 401 challenge (no token)
05:19:29.987 PRM fetched: /.well-known/oauth-protected-resource/mcp
05:19:29.991 AS metadata fetched
05:19:29.994 DCR: registered mock-client-XdMVUk7eXiI redirect_uris = [ 'http://127.0.0.1:54721/callback' ]
05:19:30.203 authorize: auto-approving mock-client-XdMVUk7eXiI resource = http://127.0.0.1:39170/mcp → 302 loopback
05:19:30.308 token: code exchanged OK (PKCE verified), resource = http://127.0.0.1:39170/mcp
05:23:33.494 MCP initialize OK (Bearer valid), protocol = 2025-11-25
05:23:33.495 MCP tools/list OK → 2 tools

授权后 mcp_test_server 诊断(token 入 keychain,实机验证 security find-generic-password 可见、断开后清除):

{ "ok": true, "toolsCount": 2, "oauth": { "state": "authorized", "refreshable": true,
  "storage": "keychain", "issuer": "http://127.0.0.1:39170", "scope": "mcp.read mcp.tools" } }

Change scope

  • Modules: agent-gui (src-tauri Rust + TS tools) / agent-ui (settings + mcp-hub UI + i18n)
  • Key paths: crates/agent-gui/src-tauri/src/services/mcp_oauth/crates/agent-gui/src-tauri/src/commands/integration/{mcp.rs,mcp_oauth.rs}crates/agent-ui/src/lib/mcp/oauthApi.tscrates/agent-ui/src/pages/mcp-hub/{McpServerCard,McpServerEditModal}.tsxcrates/agent-ui/src/lib/settings/{types,index}.tsdocs/design/mcp-oauth.md

Screenshots / preview

编辑弹窗:http transport 的鉴权配置区(OAuth 2.1 / Scope / Client ID + keychain 提示):

edit modal

授权前(未授权徽章 + 连接) 授权后(已授权 + 重新授权/断开授权)
unauthorized authorized

Verification

  • cargo test -p liveagent --lib:全绿,新增 18 用例(发现链解析/PRM 候选/PKCE 向量/loopback 真实 TCP 往返含 state 防 CSRF/store 文件降级 roundtrip+0600 权限/过期窗口/授权互斥/auth 配置判定与 serde 兼容)
  • cargo clippy -p liveagent --lib:零警告
  • agent-gui node --test2503/2503(新增 normalize auth 用例;mcp-hub-tabs 设计约束测试通过)
  • gateway web:631/631;三端 tsc 零错误;biome、check-ui-boundaries.mjs 门禁通过
  • 实机端到端:添加 oauth server → Connect → 系统浏览器授权 → 回调 → 卡片转「已授权」→ tools/list 带 Bearer 成功 → 断开授权 → keychain 条目清除(security CLI 验证)

已知边界(详见设计文档 §8)

  • 真实托管 MCP server(Linear/Notion 等)验收待手测(roadmap 验收项 1);mock 全流程已闭环
  • WebUI「authorization required」引导(gateway 事件)作为紧随的独立改动
  • device-code 流不做(roadmap 既定);SSE GET 流中失效依赖下一次 POST 触发刷新

Pre-submit checklist

按 MCP Authorization 规范(RFC 9728/8414/7591/8252/8707)实现完整授权码流:

- services/mcp_oauth/:discovery(401 挑战→PRM→AS 元数据候选序列+旧规范
  fallback)、register(RFC 7591 动态注册)、flow(PKCE S256+state、
  127.0.0.1 随机端口 loopback 回调、token 交换/刷新)、store(keyring v3
  三平台 + Linux 无 secret-service 降级 0600 文件 + 进程内缓存)
- transport 集成:http/sse 每请求经 ensure_bearer 注入 Bearer(将过期主动
  刷新单飞);401 → Unauthorized 变体 → 被动刷新一次重试,不可行则报带
  MCP_OAUTH_AUTHORIZATION_REQUIRED 标记的引导错误;SSE GET 流重连取最新 token
- 命令:mcp_oauth_authorize(系统浏览器,仅用户手势)/status/clear;
  test 响应附 oauth 诊断(状态/过期/存储后端,永不含 token 本体)
- 配置:McpServerConfig.auth { type, scope?, clientId? }——token 永不进
  settings/同步/备份;normalize 双端;卡片授权徽章+Connect/断开(WebUI 仅
  状态展示);编辑弹窗鉴权区;删除路径(卡片+McpManager)清 keychain

依赖新增 keyring 3.6 + getrandom 0.3。cargo test 18 项新用例、前端 2503+631
全绿,clippy/biome/三端 tsc/UI 边界门禁零告警。
@coder-hhx
coder-hhx marked this pull request as ready for review August 25, 2026 05:32
- McpManager create/update/validate 接受 auth {type, scope?, clientId?}:
  schema 与 McpAuthConfig 对齐,normalizeAuthInput 严格校验入参形状,
  type:"none"/缺省折叠为 undefined(patch 显式覆盖即可清除 oauth 配置),
  凭据永不经过本工具;list 行附 auth=oauth 标记
- authOauthHint(zh/en)改为如实描述:keychain 不可用时降级 0600 私有
  文件,凭据不参与同步/备份、不离开本机
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

P1 能力补齐:MCP OAuth 2.1——接通托管 MCP server(发现/动态注册/PKCE/keychain)

1 participant