OpenWork Local Managed MCP OAuth 深度解析:让 OpenCode 引擎安全消费需要 OAuth 的远程 MCP
【免费下载链接】openworkThe open-source alternative to Claude Cowork (powered by opencode)项目地址: https://gitcode.com/GitHub_Trending/ope/openwork
导读
本文围绕 OpenWork 的 Local Managed MCP OAuth 能力展开:当自定义远程 MCP Server 依赖 OAuth 2.0 授权、而其 OAuth 流程无法被 OpenCode 引擎的直连 MCP 客户端正确处理时,OpenWork Desktop 可以在本地接管完整的 OAuth 生命周期(发现、动态客户端注册、PKCE 授权、令牌交换与刷新),并通过一个绑定到工作区与连接的回环 MCP 网关把工具暴露给内置 OpenCode 引擎。读完本文你将掌握:该能力的设计动机与端到端用户流程、加密持久化与密钥管理的实现细节、回环网关与运行时注册机制、出站网络边界的安全约束,以及项目源码与测试中的可验证依据。
设计动机:为什么需要"托管式 OAuth"
OpenWork 的 Server 侧使用 enterprise-mcp-client 作为远程 MCP 消费的统一运行时。该包以"依赖注入"方式持有网络、持久化、租户/授权、诊断与时钟等契约,可完整处理 MCP 协议协商与 OAuth 生命周期;而 OpenCode 引擎的直接 MCP 客户端对部分提供商的 OAuth 流程支持不完整。
Local Managed MCP OAuth 提供了一条兼容路径:
- 对"通过 OpenWork 企业 MCP 客户端可以正常工作、但无法通过 OpenCode 直接 MCP 客户端完成授权"的提供商,OpenWork 在本地桌面工作区中为连接代为持有 OAuth;
- OpenCode 侧只看到一个指向本地回环网关的远程 MCP 条目(
oauth: false),它能看到提供商工具,却永远接触不到提供商访问令牌、刷新令牌或 OAuth 客户端密钥。
该路径是按连接显式开启(opt-in)的,且目前仅限桌面端(desktop-only)。既有的直接远程 MCP 与本地命令 MCP 保持原有路径不变(见 docs/features/local-managed-mcp-oauth/README.md)。
用户流程:四个步骤完成托管授权
原文档给出了标准用户流程,结合前端实现(add-mcp-modal.tsx 中managedOAuth: state.oauthExpanded、store.ts 中的managedOAuthAvailable)可还原为:
- 添加远程 MCP 并展开 OpenWork-managed OAuth:在本地桌面工作区的 MCP 连接界面添加一个远程 MCP Server,展开OpenWork-managed OAuth选项。
- 可选填写预注册信息:如果提供商支持预注册客户端,可输入已注册的
clientId、clientSecret与requestedScopes;若提供商支持动态客户端注册(DCR),这些字段可以留空,由 OpenWork 代为注册。 - OpenWork 完成 OAuth 握手:自动执行 OAuth discovery(含
authorizationServerIssuer绑定)、必要时 DCR、PKCE 授权、回环回调、令牌交换,以及一次带认证的tools/list校验(确认令牌确实可用)。 - OpenCode 接入工具:OpenCode 收到一个指向 OpenWork 回环网关的远程 MCP 条目,其
oauth: false。OpenCode 能看到提供商工具,但拿不到任何令牌与密钥。
在服务端,创建连接与启动授权的入口是POST /workspace/:id/mcp/managed(server.ts),其请求体结构为:
{ name: string; // 连接名,如 "mock-oauth" url: string; // 远程 MCP Server 的 URL,需为公网 HTTPS oauth: { applicationType?: "native" | "web"; // 默认 "native" requestedScopes?: string[]; // 去重后保存 authorizationServerIssuer?: string; // 精确绑定授权服务器 issuer clientId?: string; // 预注册客户端(可选) clientSecret?: string; // 预注册密钥(可选) }; }对应的连接状态查询为GET /workspace/:id/mcp/:name/managed,重新发起授权为POST /workspace/:id/mcp/:name/managed/connect(server.ts)。
核心实现:local-managed-mcp.ts 的组成
托管 MCP 的全部逻辑集中在 apps/server/src/local-managed-mcp.ts(约 1300 行),可划分为四个职责域:
| 职责域 | 关键导出/常量 | 说明 |
|---|---|---|
| 加密 Vault | writeVault/loadVaultLocked/recoverVaultLocked | AES-256-GCM 加密的本地持久化文件 |
| 状态与持久化 | createPersistence | 实现 enterprise-mcp-client 的三类持久化端口 |
| OAuth 编排 | startLocalManagedMcpAuthorization/completeLocalManagedMcpAuthorization | 发起与完成 PKCE 授权 |
| 回环网关 | handleLocalManagedMcpGateway/authorizeLocalManagedMcpGateway | 供 OpenCode 引擎调用的认证 MCP 网关 |
每条连接在 Vault 中保存的结构(StoredLocalManagedMcpConnection)包括:serverUrl、enabled、oauth(applicationType/requestedScopes/authorizationServerIssuer/clientId/clientSecret)、status(needs_auth/connecting/connected/reconnect_required)、lastError、clientRegistration(DCR 结果)、credential(令牌)、authorizations(进行中的 PKCE 事务)、discovery(OAuth 发现状态)。
通过企业 MCP 客户端完成握手
startLocalManagedMcpAuthorization的执行顺序(local-managed-mcp.ts):
- 将连接置为
connecting,并写入运行时 MCP 条目; - 创建带 HMAC 签名的授权 state(
createAuthorizationState,绑定 workspaceId/name/connectionId/redirectUri/过期时间与 nonce); - 调用
enterpriseClient().connect(...),由 enterprise-mcp-client 完成 OAuth discovery、DCR、PKCE 授权与令牌交换; - 若结果为
connected,调用verifyTools执行一次tools/list校验并将状态置为connected;若需要浏览器跳转,则返回authorizeUrl并将状态置为needs_auth。
回调端点GET /mcp/oauth/callback(server.ts)校验state与code后调用completeLocalManagedMcpAuthorization(local-managed-mcp.ts):验证签名 state →completeAuthorization完成令牌提交 →tools/list校验 → 重写运行时条目。state 验证使用timingSafeEqual比较 HMAC-SHA256 签名,并校验expiresAt(10 分钟窗口)与connectionId是否匹配。
持久化与生命周期:加密 Vault 与状态机
加密存储格式
Provider 凭据、OAuth 注册、发现状态与 PKCE 事务统一存放在运行时存储目录下的local-managed-mcp-vault.json中(vaultPath由 local-managed-mcp.ts 计算),并采用AES-256-GCM加密(VaultEnvelope,local-managed-mcp.ts):
{ "schemaVersion": 2, "index": { /* 明文、非机密字段投影(不含 clientSecret 与令牌) */ }, "vault": { "schemaVersion": 1, "algorithm": "aes-256-gcm", "iv": "<base64>", "tag": "<base64>", "data": "<base64 密文>" } }写入时(writeVault,local-managed-mcp.ts)使用 12 字节随机 IV、以openwork-local-managed-mcp-v1作为 AAD(关联数据),并将文件以0o600权限写入临时文件后原子rename,避免半写状态。
密钥从哪里来
resolveVaultKey(local-managed-mcp.ts)决定密钥来源:
- OpenWork Desktop:加密密钥由操作系统安全存储服务(如 macOS Keychain / Windows Credential Manager)持有,磁盘上只持久化受保护的密钥 Blob,与加密 Vault 分离存放;
- 独立 Server:必须设置环境变量
OPENWORK_ENCRYPTION_KEY,其值经 SHA-256 派生为 32 字节密钥;没有明文密钥文件的兜底。密钥缺失或长度非法时,接口返回503 managed_mcp_secure_storage_unavailable。
状态机与生命周期操作
| 操作 | 实现 | 效果 |
|---|---|---|
| 创建 | createLocalManagedMcpConnection(L766) | 校验 URL 后写入 Vault(状态needs_auth),并立即写入启用状态的运行时条目 |
| 授权 | start/completeLocalManagedMcpAuthorization | connecting→ 授权成功经tools/list校验后 →connected |
| 启停 | setLocalManagedMcpEnabled(L1198) | 同步 Vault 与运行时条目 |
| 断开 | disconnectLocalManagedMcp(L1215) | 删除已存凭据、清空 PKCE 事务、置enabled: false、状态回needs_auth,并停用网关条目 |
| 删除 | deleteLocalManagedMcp(L1231) | 从 Vault 移除连接并清理运行时条目 |
| 启动协调 | reconcileLocalManagedMcpRuntimeEntries(L758) | Server 每次启动时用当前回环端口与新的 Bearer重写托管运行时条目,同时保留加密的 Provider 凭据 |
安全存储变化后的恢复
当 OS 安全存储发生变化(如换机、重装、Keychain 重置)导致密钥无法再解密 Vault 时,recoverVaultLocked(local-managed-mcp.ts)会:
- 将无法解密的文件改名为
local-managed-mcp-vault.json.openwork-backup-<时间戳>隔离归档; - 从明文
index(非机密投影)重建连接骨架,状态统一置为reconnect_required,并写入用户可读错误:"Secure storage on this device changed, so saved sign-ins were cleared. Reconnect to restore this connection."; - 若为 v1 旧格式(无 index),同时清理失去 Vault 连接的孤儿网关运行时条目(
pruneOrphanedManagedRuntimeEntries)。
此外,listLocalManagedMcpConnectionsSafe(L844)在密钥不可用时仍可基于明文 index 提供只读连接列表(available: false),保证 UI 可渲染而非报错;inspectLocalManagedMcpVault(L881)为诊断提供不触密的状态检查(absent/ok/recovered/secure-storage-unavailable/unreadable)。所有 Vault 读写都经由 per-path 队列withVaultQueue串行化,防止并发写坏文件。
回环网关与运行时注册:OpenCode 只看到"本地远程 MCP"
授权完成后,OpenWork 会向 OpenCode 的运行时配置写入一条托管网关条目(runtimeConfig,local-managed-mcp.ts):
{ "type": "remote", "url": "http://127.0.0.1:<port>/mcp/managed/<workspaceId>/<name>", "enabled": true, "headers": { "Authorization": "Bearer <gateway-token>" }, "oauth": false }关键设计点:
- Bearer 的生成:网关密钥
gatewaySecret是进程内randomBytes(32),仅存于内存(gatewaySecretByConfigWeakMap),每次 Server 重启都会轮换;令牌本身是HMAC-SHA256(secret, "<workspaceId>\0<name>")的 base64url 输出,因此作用域被绑定到工作区 + 连接(L717-L727)。 - 运行时条目不含秘密:e2e 测试明确断言运行时配置
JSON.stringify结果既不包含 Provider 地址、也不包含访问令牌(local-managed-mcp.e2e.test.ts),且oauth为false。 - 网关端点:
POST/GET/DELETE /mcp/managed/:workspaceId/:name(server.ts)在handleLocalManagedMcpGateway(L1255)中先以timingSafeEqual校验 Bearer(未授权返回 401;连接被禁用返回 503),再使用WebStandardStreamableHTTPServerTransport启动一个仅暴露tools能力的 MCP Server,并开启enableDnsRebindingProtection、allowedHosts限定为127.0.0.1:<port>与localhost:<port>。 - 工具调用转发:
ListToolsRequestSchema与CallToolRequestSchema处理器内部调用 enterprise-mcp-client 的listTools/callTool,由后者携带已保存的令牌向真实 Provider 发起请求;凭据从未出现在回环网关的响应中。 - 凭据失效即提示重连:工具发现/执行失败时,若 Vault 中已无凭据(
markReconnectWhenCredentialIsGone,L1122),网关向 OpenCode 返回"需要重连"的明确错误;已有的reconnect_required原因不会被后续失败覆盖。
网络边界:防止回环网关变成内网代理
原文档强调:托管 Provider 在显式本地开发之外必须使用 HTTPS 且只能解析到公网地址。该约束由 apps/server/src/local-managed-mcp-url-guard.ts 实现,从四个层面落地:
1. URL 与协议校验(assertLocalManagedMcpUrl,L169)
- 仅允许
http:/https:,且非开发模式强制 HTTPS(managed MCP egress requires HTTPS); - 禁止 URL 内嵌用户名/密码;
- 创建连接时即校验,非法地址返回
400 managed_mcp_url_not_allowed(local-managed-mcp.ts)。
2. 私有/保留地址阻断
isLocalManagedMcpPrivateAddress覆盖完整的 IPv4 保留段(10/8、127/8、100.64/10、169.254/16、172.16/12、192.0.0.0/24、192.0.2.0/24、192.88.99/24、192.168/16、198.18/15、198.51.100/24、203.0.113/24、组播与0.0.0.0/8等)以及 IPv6 保留段(::、::1、64:ff9b::、fc00::/7、fe80::、fec0::/10、ff00::/8、2001:db8::/32、Teredo/6to4 映射等,见 L24-L98)。
3. DNS 解析与防重新绑定
createLocalManagedMcpPublicLookup(L145)把"验证后的解析结果"直接交给 socket 连接器:所有解析出的地址先经私有地址校验,再进入net.connect,后续 DNS 应答无法在验证与连接之间偷换地址(防止 DNS rebinding)。allowPrivateUrls仅在OPENWORK_DEV_MODE=1或OPENWORK_ALLOW_PRIVATE_MCP_URLS=1时放行(L117-L119)。
4. 重定向再验证
createLocalManagedMcpGuardedFetch(L235)以手动模式跟随重定向(上限 5 跳),每一跳都重新走 URL/协议/地址校验:
- 阻止 HTTPS → 非 HTTPS 降级;
- 跨源时禁止携带请求体重定向(
GET/HEAD之外的方法或带 body 的请求直接拒绝); - 跨源跳转时删除
authorization、cookie、proxy-authorization、mcp-session-id、last-event-id、x-api-key、x-auth-token等敏感头(redirectedRequestInit,L205-L226)。
这四层共同保证了:一个被配置的 MCP URL 或恶意重定向无法把桌面网关变成内网请求代理。
持久化契约:enterprise-mcp-client 的三类端口
托管实现通过createPersistence(local-managed-mcp.ts)向 @openwork/enterprise-mcp-client 注入三类窄端口,全部落盘到加密 Vault:
| 端口 | 职责 | 托管实现要点 |
|---|---|---|
clientRegistrations | 预注册或 DCR 客户端的存取 | 有clientId时直接以pre-registered:1版本返回;否则保存 DCR 结果,first-writer-wins(已有注册不覆盖);支持显式expiresAt与invalidate |
authorizations | 有状态、可过期、可被消耗的 PKCE 事务 | 用HMAC-SHA256(vaultKey, authorizationId)派生存储键;保存codeVerifier与expiresAt;load不消耗事务,仅在令牌提交时原子消费 |
credentials | 令牌的加载/保存/失效 | authorization-code提交会校验并消耗对应授权事务与客户端注册版本(防并发乱序覆盖);refresh提交通过expectedCredentialRevision做 compare-and-swap,过期令牌无刷新令牌时置为reconnect_required |
每次写入都携带绝对commitExpiresAt与 abort signal(ensurePersistenceContext,L522-L526):生命周期超时或中止时,适配器必须拒绝/回滚事务,避免"先报告超时、之后又静默写回凭据"的竞态——这正是 enterprise-mcp-client 的安全不变量之一(见其 README 的 "Security invariants" 章节)。
错误处理与诊断
- 安全错误收敛:当握手失败且诊断事件表明存在具体外部原因(HTTP 4xx/5xx、
OAuthError、RegistrationRejectedError、SdkHttpError、私有地址错误、明确的网络错误码如ECONNREFUSED/ECONNRESET/ETIMEDOUT等,见NETWORK_FAILURE_CODES,L1011-L1021)时,统一转换为502 managed_mcp_connection_failed与用户可读文案(externalHandshakeApiError,L1072-L1089),并将连接置为reconnect_required。 - 诊断脱敏:
updateConnectionStatus保存的lastError经sanitizeDiagnosticString处理,且会用[REDACTED]替换 URL 查询串与 JSON 中的access_token/refresh_token/client_secret/code/state,并截断到 500 字符(L984-L1003)。 - 输入校验不落盘:URL 非法、名称冲突(
409 managed_mcp_exists)等输入错误不会写入 Vault。
验证与测试覆盖
针对该功能的聚焦测试位于 apps/server/src/local-managed-mcp.e2e.test.ts,与文档声明的覆盖范围一一对应:
| 测试(行号) | 覆盖点 |
|---|---|
| L264 | 无托管 Vault 时,普通 MCP fallback 路径不受影响 |
| L284 | 初始 OAuth 握手失败时回滚新建连接 |
| L334 | DCR 与协议协商失败时的安全连接错误 |
| L499 | 可操作的输入错误不持久化连接 |
| L563 | 完整生命周期:拥有 OAuth、向 OpenCode 暴露工具、刷新令牌、重启存活、断开连接;并断言运行时条目不含 Provider 地址与令牌 |
| L713 | 安全存储密钥变化后隔离归档、重建 Vault、重新连接 |
| L873 | 安全存储不可用时明文 index 只读服务 |
| L932 | 无法解密的 legacy v1 Vault 隔离归档并清理孤儿网关条目 |
除聚焦 e2e 外,桌面端与 Server 端 TypeScript 项目以及编译后的内嵌 Server 构建也会纳入检查,确保桌面场景(依赖 OS 安全存储)与独立 Server 场景(依赖OPENWORK_ENCRYPTION_KEY)行为一致。
适用范围与边界
- 桌面优先:托管路径目前仅对本地桌面工作区开放(用户界面入口位于桌面端 MCP 连接界面);独立 Server 部署需要自行提供
OPENWORK_ENCRYPTION_KEY。 - 按连接开启:既有直接远程 MCP、本地命令 MCP 的连接方式不变,托管路径不会默认启用。
- 仅公网 HTTPS:除显式开发模式(
OPENWORK_DEV_MODE=1/OPENWORK_ALLOW_PRIVATE_MCP_URLS=1)外,托管 Provider 必须是公网 HTTPS 地址,且解析结果不得命中私有/保留网段。 - 重启语义:OpenWork Server 每次重启都会轮换网关 Bearer 并通过启动协调重写运行时条目,凭据在 Vault 中保持不变,因此重启后连接无需重新授权即可继续使用。
【免费下载链接】openworkThe open-source alternative to Claude Cowork (powered by opencode)项目地址: https://gitcode.com/GitHub_Trending/ope/openwork
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考