DeepChat MCP OAuth 认证实战:外部浏览器、PKCE 与 Loopback 回调的完整落地
【免费下载链接】deepchat🐬DeepChat - A smart assistant that connects powerful AI to your personal world项目地址: https://gitcode.com/GitHub_Trending/dee/deepchat
DeepChat 为 OAuth 保护的 Streamable HTTP 类型 MCP 服务器提供了一整套开箱即用的认证链路:MCP 服务器卡片上的一键 Authenticate、系统浏览器中的授权流程、本地 loopback 回调服务,以及浏览器无法回连本地监听时的“粘贴完整回调 URL”兜底。读完本文,你将理解 DeepChat 如何复用 MCP TypeScript SDK v2 的 OAuth 流程完成发现(Discovery)、PKCE/state 校验、issuer 校验、令牌加密持久化与服务器自动重连,并掌握关键参数(端口、超时、回调路径)与状态机的源码级实现细节,来源规格文档为 mcp-oauth-authentication spec。
1. 问题背景:OAuth 保护的 MCP 服务器
一个 Streamable HTTP 类型的 MCP 服务器(例如linear,type: "http",baseUrl: "https://mcp.linear.app/mcp")在首次连接时可能返回 401 OAuth challenge。规格文档定义的完整用户路径是:
- 启动阶段无副作用:添加并启用这类服务器不会自动打开浏览器,只在检测到 OAuth 挑战后,让卡片进入“Authentication required”状态并展示 Authenticate 按钮。
- 点击认证才拉起 loopback 服务:仅在用户点击 Authenticate 时,DeepChat 才启动一个监听
127.0.0.1的本地 HTTP 回调服务器。 - 外部浏览器完成授权:通过
shell.openExternal在系统浏览器中打开授权 URL,完成 authorization code + PKCE 流程。 - 回调页面给出统一文案:认证成功后,回调页返回固定的英文提示:
Authentication complete. You can return to DeepChat. If DeepChat does not update, copy the full URL from your browser and paste it into DeepChat. - 令牌安全落盘并自动重连:访问令牌、动态客户端信息使用 Electron
safeStorage加密持久化,随后 MCP 服务器重启/重连,工具、提示、资源经既有 MCP presenter 路径加载。
OpenAI Codex 登录复用同样的“外部浏览器 + loopback 回调”模式,并保留独立的凭据域(credential domain)。
规格文档同时明确了非目标(Non-Goals):不为每个 provider 新建通用 OAuth 框架;不做 MCP OAuth 令牌的云同步;不支持 device-code 流程;不为已废弃的 SSE 授权新增行为(SSE 凭据仍属遗留兼容问题,新授权模式面向 Streamable HTTP);机器/企业级授权单独在 mcp-authorization-extensions 规格中定义;MCP 与 Codex 之间不共享令牌存储,只共享回调页/监听辅助函数。
2. 总体架构:三个核心类与一条数据流
从源码结构看,整条链路由三个核心模块协作完成,全部位于主进程:
| 模块 | 文件 | 职责 |
|---|---|---|
McpOAuthManager | mcpOAuthManager.ts | 交互式/机器式授权的总编排:状态机、发现校验、回调生命周期、绑定写回、凭据失效处理 |
DeepChatMcpOAuthProvider | mcpOAuthProvider.ts | 实现 SDK 的OAuthClientProvider接口:客户端元数据、PKCE verifier、跳转外部浏览器、按 issuer 上下文读写令牌 |
McpOAuthCredentialStore | oauthCredentialStore.ts | safeStorage 加密持久化、memory-only 降级、按“凭据绑定”键值隔离、容量上限校验 |
数据流为:连接失败(401)→handleConnectionError标记 required → 用户点击认证 →startAuth启动回调会话并打开浏览器 → loopback 回调校验state/iss→ SDK 换令牌 →saveTokens加密落盘 →onAuthenticated重启服务器 → 状态变为authenticated。
状态经类型化事件mcp.server.auth.changed发布(见 mcpOAuthManager.ts#L1142),渲染进程只消费“无密钥”的认证状态,令牌永远不进入 renderer state、日志、配置同步或 MCP 服务器配置。
3. 何时进入交互式 OAuth:isRemoteOAuthCapable判定树
并不是所有远程服务器都会走 OAuth 自动检测。mcpOAuthManager.ts#L93-L105 中的isRemoteOAuthCapable给出判定条件:
function isRemoteOAuthCapable(config?: Partial<MCPServerConfig> | null): boolean { if (!config?.baseUrl || hasAuthorizationHeader(config)) { return false } if (config.type !== 'http' && config.type !== 'sse') { return false } const mode = getAuthorizationMode(config) if (mode === 'none') { return false } return config.type === 'http' || mode === 'interactive' }要点:
- 静态
Authorization头优先:若配置里已有customHeaders.Authorization(大小写不敏感,见 getAuthorizationHeader),则直接跳过 OAuth 自动检测,既有 bearer-token MCP 配置继续工作。这与规格中“customHeaders.Authorizationremains higher priority than OAuth auto-detection”一致,并在 mcpClient.ts#L502-L514 落地:bearer前缀的头被包装为SimpleOAuthProvider,否则才调用createRuntimeProvider。 - 仅
http类型默认启用:SSE 只有在显式authorization.mode: 'interactive'时才允许,而新授权模式的目标是 Streamable HTTP。 authorization.mode缺省为'interactive',设为'none'可完全关闭 OAuth 检测。
4. 交互式认证流程:startAuth的完整拆解
startAuth(mcpOAuthManager.ts#L425-L560)是核心入口,interactive 模式下依次做四件事:
4.1 一次性 state 与随机回调路径
每次认证都生成全新的 state 与不可猜测的回调路径,使回调 URL 本身不可复用:
const state = createState() // randomBytes(16).toString('base64url') const callbackPath = `/mcp/oauth/callback/${randomBytes(12).toString('base64url')}`4.2 启动 loopback 回调会话
通过共享的 startOAuthLoopbackCallbackSession 启动监听:
- 默认监听
127.0.0.1(listenHost),回调页redirectHost为localhost,绝不使用0.0.0.0; - 首选端口为
MCP_OAUTH_REDIRECT_PORT,若EADDRINUSE则自动回退到系统分配端口; - 每个请求先校验method(仅 GET)→ path(必须完全匹配回调路径)→ state → issuer,任何一步失败都返回 400 失败页或 404;
- 会话带超时定时器,超时即拒绝并关闭服务器;成功/失败后
close()立即销毁监听,满足“短超时且总是关闭回调服务”的约束。
4.3 客户端元数据:原生应用 + PKCE
DeepChatMcpOAuthProvider.clientMetadata(mcpOAuthProvider.ts#L59-L69)把 DeepChat 标识为原生应用:
{ client_name: 'DeepChat', redirect_uris: [this.options.redirectUrl], grant_types: ['authorization_code', 'refresh_token'], response_types: ['code'], token_endpoint_auth_method: 'none', application_type: 'native', scope: this.options.scopes?.join(' ') || undefined }这对应规格中“Client ID Metadata Documents 优先,Dynamic Client Registration 仅作为授权服务器要求时的遗留回退”。跳转环节redirectToAuthorization还有一层协议白名单:授权 URL 必须是https:,或指向 loopback 主机的http:,否则直接抛错(mcpOAuthProvider.ts#L108-L120)。
4.4 SDKauth()与回调等待
管理器调用 SDK 的auth(provider, { serverUrl, scope })。若需要浏览器交互,provider 的redirectToAuthorization会执行shell.openExternal,随后startAuth挂起在callbackSession.waitForCallback()上,回调到达后以code和iss再次调用auth()完成令牌交换(finishAuthFlow),成功后清理 pending 流程、关闭回调服务器,并触发onAuthenticated重启服务器。
5. 回调校验:state 之后还有一道 issuer 矩阵
规格中最容易被忽略、但安全上最关键的部分是回调参数校验矩阵。回调路径中先做结构性校验(resolveOAuthLoopbackCallbackUrl):
- URL 的 protocol、hostname、port、pathname 必须与 redirect URI 逐项相等,且不允许携带 username/password/hash;
state与本次流程的期望值做精确字符串比较,不匹配即判失败(覆盖“缺失、过期、不匹配、已消费”的粘贴 URL 兜底场景);- 通过 state 校验后才执行
validateParameters钩子——MCP 场景下这里调用 SDK 的validateAuthorizationResponseIssuer(mcpOAuthManager.ts#L489-L499)。
issuer 校验遵循四象限矩阵(见规格 Acceptance Criteria):
元数据中authorization_response_iss_parameter_supported | 回调携带iss | 处理方式 |
|---|---|---|
true | 存在 | 要求与发现得到的授权服务器 issuer 做精确字符串相等 |
true | 缺失 | 拒绝 |
false/缺省 | 存在 | 同样要求精确字符串相等 |
false/缺省 | 缺失 | 继续 |
并且 issuer 比较不做URL 解析、归一化、尾斜杠改写、大小写折叠或百分号解码——刻意避免宽松比较带来的校验绕过。注意区分:回调侧的iss是“原始精确比较”,而复用已存凭据时的 issuer 比对(normalizeUrlIdentifier,mcpOAuthManager.ts#L175-L196)允许 URL 形式归一化,两者职责不同。
最终通过校验后,回调页按code/error/error_description/error_uri分别成功或失败,成功页携带规格指定的完整文案(OAUTH_CALLBACK_COMPLETE_TEXT),并附带no-store、严格 CSP、Referrer-Policy: no-referrer等响应头。
6. 凭据存储:绑定键、safeStorage 加密与 memory-only 降级
6.1 凭据键:绑定即隔离
令牌不是按“服务器名”存的,而是绑定到不可变的服务器身份。createMcpCredentialKey(mcpOAuthManager.ts#L149-L170)对以下字段拼接后取 SHA-256:
credentialClass(如 interactive_oauth) serverId configGeneration(配置代次) bindingHash(绑定哈希) endpoint(服务器端点) protectedResourceUrl authorizationServerIssuer clientIdrequireServerBinding会在serverId/configGeneration/bindingHash/baseUrl任一缺失时直接抛出 “MCP server identity is incomplete”。效果是:为某一个绑定或 issuer 发现的凭据,永远不会被提供给另一个服务器;服务器配置变更导致绑定失配时,旧凭据不可见。
6.2 加密落盘与容量护栏
McpOAuthCredentialStore 的默认文件路径为userData/mcp-oauth/credentials.json,持久化格式为 v2 信封:{ version: 2, storage: 'safeStorage', wrapped: <base64 密文>, updatedAt },写入采用“临时文件(0o600)+ 原子 rename”(persist),并兼容 v1 旧信封。容量护栏包括:文件 ≤ 16 MiB、明文载荷 ≤ 8 MiB、记录数 ≤ 512、key ≤ 512 字节、secret ≤ 256 KiB、私钥 ≤ 1 MiB。
降级策略精确对应规格:
safeStorage.isEncryptionAvailable()为 false,或 Linux 上报弱后端basic_text(isLinuxBasicTextBackend)时,存储状态变为memory:密钥只存在于当前进程内存,UI 会提示重启后需要重新登录,且磁盘上旧凭据文件会被删除;- 加载失败(文件损坏、超限)时记录
loadFailed,后续写入直接报错,而不是静默写入明文。
6.3 发现状态写回
认证成功后,finalizeInteractiveBinding(mcpOAuthManager.ts#L792-L866)把发现得到的authorizationServerIssuer、protectedResourceUrl、clientId写回宿主拥有的服务器配置,并校验配置代次/绑定哈希在发现期间未发生变化(“MCP server binding changed during OAuth discovery”)。此后凭据才可复用;运行时复用会再次执行 live discovery,issuer/resource 不再匹配的记录会被清除(isInteractiveCredentialCurrent,mcpOAuthManager.ts#L763-L790)。
7. 运行时接入:令牌刷新、401 识别与错误脱敏
7.1 连接时的 provider 选择
mcpClient.ts#L502-L514 在建立 v2 Streamable HTTP 传输前选择授权 provider:配置了Authorization头则用SimpleOAuthProvider(静态头优先),否则调用McpOAuthManager.createRuntimeProvider(mcpOAuthManager.ts#L334-L400)。interactive 模式下该方法会:加载既有凭据 → live discovery → 校验 issuer/resource 与配置一致 → 凭据过期即清除并返回 undefined(卡片回到 required 状态);存在 refresh token 的过期访问令牌走 SDK/provider 路径刷新。
7.2 401 → required 的自动识别
连接出错时,serverManager.ts#L402 调用handleConnectionError(mcpOAuthManager.ts#L402-L423)。isOAuthError的判定包括:SDK 的UnauthorizedError、状态码 401、以及消息模式匹配(401/unauthorized/auth required/authentication required/authorization required/invalid_token/no auth provider)。interactive 模式据此把卡片置为required(等待用户点击认证),机器模式置为error。
7.3 错误脱敏
任何进入状态或日志的错误信息都会先过sanitizeError(mcpOAuthManager.ts#L66-L76):access_token、refresh_token、client_secret、code/id_token、Bearer <token>及 JWT 形态(eyJ...)全部替换为[redacted],并截断到 2048 字符——这是“令牌绝不出现在日志”约束的直接实现。
8. 服务器卡片状态机与粘贴回调 URL 兜底
卡片只暴露无密钥状态:required(显示 Authenticate)、authenticating(等待回调)、authenticated(显示 Authenticated)、error(含脱敏错误)与none/unsupported,对应规格中的三幅 ASCII 卡片草图(Error + Authenticate → Running + Authenticated)。
粘贴兜底由completeAuthFromCallbackUrl(mcpOAuthManager.ts#L562-L600)实现,拒绝规则与规格一一对应:
- 服务器没有 pending flow → 置 error(“not pending”);
- 回调 URL 推导出的凭据键既不匹配初始绑定、也不匹配当前绑定 → 判定“认证期间服务器绑定已变化”并失败;
callbackSession.resolveCallbackUrl复用与 loopback 完全相同的校验链(host/path/method/state/issuer),因此缺失、过期、不匹配或已消费的 state 一律被拒绝;- 校验通过后等待同一
flowPromise,成功即按正常流程收尾。
应用内的粘贴 UI 仍走渲染进程常规 i18n 路径,只有回调 HTML 页固定为英文。
9. 与 OpenAI Codex 的共享策略
规格要求两者共享“有界 loopback 回调助手”但保持独立凭据域,实现上体现为:
- resolveOpenAICodexCallbackUrl 直接包装通用的
resolveOAuthLoopbackCallbackUrl,只替换错误文案,并复用startOAuthLoopbackCallbackSession管理监听与超时; - 成功/失败提示统一为 “Authentication complete. You can return to DeepChat.”;
- Codex 登录通过
shell.openExternal打开系统浏览器,不再在嵌入式BrowserWindow中加载 OAuth provider; - 凭据存储各自独立(Codex 使用自己的
OpenAICodexCredentialStore),不存在跨域令牌共享。
10. 可配置项、验收标准与边界
10.1 环境变量(oauthConstants.ts)
| 变量 | 默认值 | 说明 |
|---|---|---|
DEEPCHAT_MCP_OAUTH_REDIRECT_PORT | 1456 | 回调监听首选端口(1–65535),被占用时回退随机端口 |
DEEPCHAT_MCP_OAUTH_CALLBACK_TIMEOUT_MS | 600000(10 分钟) | 回调等待超时,超时即关闭会话 |
| (固定) | /mcp/oauth/callback | 默认回调前缀路径,每次认证追加随机 12 字节 base64url 段 |
10.2 关键验收标准(源自规格 Acceptance Criteria)
- 添加并启用
linear(type: "http",baseUrl: "https://mcp.linear.app/mcp")不会自动打开浏览器; - 启动时收到 OAuth 挑战时,卡片显示 authenticate 动作与明确的“需要认证”状态;
- 点击 authenticate 后,回调只接受预期的 loopback host、path、method 与 state;
- 回调页成功文案必须与规格字符串完全一致;
- 令牌与动态客户端信息经 Electron
safeStorage加密;不可用时 memory-only 且 UI 说明重启后需重新登录; - 令牌/授权码/client secret 不进入 renderer state、日志、配置同步或 MCP 服务器配置;
- 认证成功重启/重连后,工具、提示、资源经既有 MCP presenter 路径加载;
- 失效/过期凭据清除令牌状态,卡片回到 authenticate 状态;
- 凭据绑定不可变本地 server ID / config generation / binding hash、受保护资源、授权 issuer 与服务器端点;
- 既有 bearer-token 配置继续可用,
customHeaders.Authorization优先级高于 OAuth 自动检测。
10.3 边界与延伸阅读
- 新能力保持在类型化 route/event 与
src/renderer/api/*Client层,实现收敛在 MCP presenter 边界内(MCP 门面见 mcp/index.ts); - 机器/企业级授权(client credentials、private_key_jwt、cross-app access)在 mcp-authorization-extensions 中单独定义,本文的 interactive 链路不覆盖其细节;
- 实现与测试的对应关系可参考 mcpOAuthManager.test.ts、oauthCredentialStore.test.ts 与 mcpClient.test.ts;共享类型定义位于 shared/types/mcp。
- 规格状态为 “implemented and repository-validated”,外部浏览器的互操作验证仍标记为 pending,跨平台浏览器回连 loopback 的成功率可能受网络环境影响——这也是粘贴 URL 兜底存在的根本原因。
从这套实现可以看到一个清晰的工程取舍:发现、PKCE、令牌交换、刷新与 resource-indicator 行为全部交给 MCP TypeScript SDK v2,DeepChat 自己只负责“桌面端特有的三件事”——外部浏览器跳转与 loopback 监听、绑定键驱动的加密凭据隔离、以及面向用户的状态机与脱敏。理解这三块,就理解了 DeepChat MCP OAuth 认证的全部核心。
【免费下载链接】deepchat🐬DeepChat - A smart assistant that connects powerful AI to your personal world项目地址: https://gitcode.com/GitHub_Trending/dee/deepchat
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考