资讯详情

资讯详情

OpenMausBot 远程 MCP 登录验证实战:用伪造 OAuth 提供方跑通另一台电脑上的 MCP 授权

人工智能AI AgentAI 应用大模型交互助手工具调用Agent 编排桌面应用【免费下载链接】OpenMausBotOpen-source Grok Bot alternative with a virtual machine that bots can use项目地址https://gitcode.com/GitHub_Trending/op/OpenMausBot点击查看免费下载本文围绕 OpenMausBot 的 MCPModel Context ProtocolOAuth 登录能力展开重点讲解如何在不接触用户正在运行的应用、不读取真实 OAuth 凭据、不登录任何真实服务的前提下验证从另一台电脑完成 MCP 登录这条链路。读完后你将掌握如何按文档给出的命令分层运行单元测试与 API 测试、如何用verify-mcp-sign-in.ts夹具驱动真实设置面板、以及这条链路背后 PKCE 交换、回调校验、粘贴回填、取消与登出的实现原理。为什么需要从另一台电脑登录MCP 服务器通常以 OAuth 授权码模式接入。OpenMausBot 的默认做法是最标准的本地回环服务端在127.0.0.1上起一个临时监听浏览器授权后被重定向回http://127.0.0.1:port/mcp-oauth/callback回调直接落到进程内。但当管理界面是从另一台电脑或者经过反向代理的 Cloud / 托管工作区打开的这个回环地址对那台电脑毫无意义——浏览器会把用户带到一个打不开的127.0.0.1页面。OpenMausBot 的处理方式有两条能证明来源时为当前 https 地址动态注册客户端回调直接指向https://你的地址/mcp-oauth/callback由服务端公开回调接口接管完全不在本机开监听无法证明来源时仍然走回环监听但把状态标记为pasteBack界面上直接给出粘贴回填表单让用户把浏览器地址栏里那串http://127.0.0.1:.../mcp-oauth/callback?...整段复制回来。docs/verification/mcp-sign-in.md描述的正是这条链路的验证方法也是本文的主干。第一层聚焦单元测试与 API 测试文档给出的第一条命令把 6 个测试文件一次性跑完pnpm exec vitest run server/mcp-oauth.test.ts server/mcp-oauth-discovery.test.ts server/mcp-oauth-store.test.ts server/mcp-probe.test.ts src/lib/mcp-sign-in.test.ts src/components/McpServersPanel.test.ts第二条命令只挑server/index.test.ts里与 MCP OAuth 相关的用例避免整套服务端测试的时间成本pnpm exec vitest run server/index.test.ts -t MCP OAuth|OAuth URL MCP两组命令覆盖的职责边界很清晰按下表理解即可测试文件关注点server/mcp-oauth.test.ts授权码流程、PKCE、状态校验、过期、取消、token 刷新与登出、远程/粘贴两种完成方式server/mcp-oauth-discovery.test.tsRFC 8414 / RFC 9728 元数据发现、resource_metadata解析、issuer 一致性server/mcp-oauth-store.test.ts独立凭据文件的读写、schema 校验、按 URL 绑定server/mcp-probe.test.ts探测 MCP 服务器auth: required的产生与工具列表返回src/lib/mcp-sign-in.test.ts前端登录驱动仅打开 https 链接、轮询、取消、粘贴提交src/components/McpServersPanel.test.ts设置面板行为按钮状态、错误显示、登录后自动测试并启用测试到底验证了哪些攻击面server/mcp-oauth.test.ts里的 MCP sign-in from another computer 场景是重头戏。它用startFakeOAuthstartFakeHttpMcp起一套本地假服务然后逐条断言所有者隔离alice启动的流程bob去start会得到 409status返回undefinedcancelFlow抛错completeCallback得到 404且oauth.counts.token保持为 0回放防护同一个回调地址第二次提交得到 409并且 token 端点只被调用一次畸形回调not a URL、长度 16385 的超长输入、被替换成attacker.example的主机、http:改https:、混入user:pass、换路径、追加#fragment、重复code、code与error并存、错误的iss、错误的state、错误的端口——全部被拒且流程不被消耗、不会对用户提供的地址发起请求过期与取消无人完成的登录到点变成expired并关闭监听注册阶段或授权码交换阶段被取消时token 不会被写入。const variants [ not a URL, x.repeat(16_385), callback.replace(127.0.0.1, attacker.example), callback.replace(http:, https:), callback.replace(127.0.0.1, user:pass127.0.0.1), callback.replace(/mcp-oauth/callback, /other), callback #fragment, callback codeduplicate, callback erroraccess_denied, callback isshttps%3A%2F%2Fwrong.example, ];这些变体对应的实现是 server/mcp-oauth.ts 中的acceptCallback它先比对origin与pathname再拒绝username/password/hash要求所有查询参数都是单值params.getAll(key).length 1随后用恒定时间比较statesameSecret见 server/mcp-oauth.ts校验可选iss最后要求code与error互斥且code不超过 4096 字节。任何一条不满足就直接 400且不会把flow.consumed置真——这正是无效 URL 不消耗待完成流程的底层原因。第二层真实面板 伪造引擎工作区的端到端夹具单元测试证明的是函数级契约文档要求再跑一次真实界面node --experimental-strip-types scripts/verify-mcp-sign-in.ts脚本开头三行就交代了隔离边界scripts/verify-mcp-sign-in.ts// Real MCP panel server fake OAuth provider, only in a disposable workspace. const fixture await launchVerificationServer(); let oauth: AwaitedReturnTypetypeof startFakeOAuth | undefined; let mcp: AwaitedReturnTypetypeof startFakeHttpMcp | undefined;launchVerificationServer来自 scripts/control-omb.ts它会用mkdtempSync建一个临时数据目录并把HOME、USERPROFILE、XDG_*、TMPDIR、OMB_DATA_DIR全部指向它选一对空闲端口OMB_PORT与OMB_WEBHOOK_PORT写入config.json只装仓库自带的 fake engineserver/testing/fake-claude-cli.ts设置OMB_TEST_SEALED_PATH1避免扫描到开发者本机真实引擎 CLI轮询/api/health直到返回app: openmausbot并断言只播种了一个 starter bot然后重命名为Fixture Starter。夹具随后做的事与文档描述完全一致oauth await startFakeOAuth({ beforeToken: () tokenPending }); mcp await startFakeHttpMcp({ acceptBearer: oauth.isValid, wwwAuthenticate: oauth.challenge }); const api fixtureApi(fixture.info.url); await runControlOmb([doctor, --url, fixture.info.url]); await api(POST, /api/mcp/servers, { name: documents, url: mcp.url }); await api(POST, /api/mcp/servers/documents/test); const pairing await api(POST, /api/auth/pairing, { scopes: [admin, client] });也就是说doctor打到这台临时服务器、新建一个名为documents的合成 MCP 服务器、配对一个具备admin与client双 scope 的浏览器会话。整个过程不会碰用户正在跑的 OpenMausBot也不会读他们的 OAuth 凭据。pasteBack是如何被逼出来的夹具最巧妙的一处是它同时要满足两个互相拉扯的约束真实的伪造 OAuth 提供方说的是 loopback HTTP而渲染层只允许打开 https 链接mcpSignInLink只认protocol https:且无用户名密码见 src/lib/mcp-sign-in.ts。解决办法是一条仅存在于夹具中的代理路由它把真实开始响应里的authorizationUrl换成https://oauth.example.test/authorize但不改其它字段。extraRoutes: [{ path: /api/mcp/servers/documents/sign-in, method: POST, handler: async (req, res) { const response await fetch(${fixture.info.url}/api/mcp/servers/documents/sign-in, { method: POST, headers: { cookie: req.headers.cookie ?? , x-forwarded-for: 192.0.2.10 }, }); const body await response.json(); if (response.ok) { authorizationUrl body.auth.authorizationUrl; body.auth.authorizationUrl https://oauth.example.test/authorize; } res.writeHead(response.status, { content-type: application/json }).end(JSON.stringify(body)); }, }]注意请求头x-forwarded-for会命中服务端这是远程浏览器的判断server/routes/mcp-servers.ts 中remote auth.kind session (isProxied(req) || !isLoopbackHost(req.headers.host))因此服务端不会给这个流程发pasteBack又因为来自代理的请求带不出可证明的 https originmcpCallbackOrigin返回null于是它退回回环监听模式。夹具随后故意执行window.open () null模拟弹窗被拦截——这正好触发面板里从另一台电脑登录的details展开区与粘贴表单。夹具验证的 8 条行为文档列出的清单逐条都能在脚本里找到对应断言文档中的验证点脚本中的实现代理管理会话能启动流程上面的extraRoutes转发POST .../sign-in弹窗被拦截仍保留重开按钮与粘贴说明window.open () null后等待After you approve, your browser shows a page that can与#mcp-callback-documents出现无效 URL 报错且不消耗流程填入http://127.0.0.1:1/wrong?codewrongstatewrong等待[rolealert]并断言oauth.counts.token 0延迟的 token 交换保持按钮禁用且旋转、保留已粘贴 URL、提供方返回后展示结果beforeToken: () tokenPending挂起 token 端点断言button[aria-busytrue]的disabled为true、输入框value仍是原 URL交换期间取消后立即可重新登录且粘贴表单在新请求落定前可用点Cancel→ 点Sign in→ 重新填表 → 再点Complete sign-in粘贴伪造提供方的重定向地址能完成真实 PKCE 交换用fetch(authorizationUrl, { redirect: manual })取出location填入最终oauth.counts.token 2已登录的 MCP 服务器在 Test 时返回工具等待Connected: 1 tool登出取消下一个待完成流程并关闭其监听登出后重新开始/api/auth/logout后对location的fetch必须 reject几处细节值得展开为什么不跟随重定向脚本用redirect: manual拿到伪造提供方 302 的location再把这串 URL 粘贴进真实 UI。这样既没有让浏览器真的访问回环监听也证明了粘贴回填链路本身可用——这正是文档强调的证明的是无头完成流程而不是与某个真实 OAuth 提供方的兼容性。const approved await fetch(authorizationUrl, { redirect: manual }); const callbackUrl approved.headers.get(location)!; await command(fill, #mcp-callback-documents, callbackUrl); await clickButton(Complete sign-in);延迟交换的两个断言aria-busytrue且带.animate-spin、disabled true、输入框值不变三者同时成立才算通过。对应的 UI 在 src/components/McpServersPanel.tsx输入框disabled{completingSignIn}按钮aria-busy{completingSignIn}取消按钮只在signingIn时出现。取消与旧请求的竞态signInAbort.current controller这一守卫src/components/McpServersPanel.tsx保证旧请求落定时不会污染新表单的忙碌状态服务端侧则用held的tokenPending复现旧请求尚未落定的窗口。对应单元测试是does not let an old cancellation cancel a new attempt。登出必须真的让回调失效脚本断言登出后对先前location的fetch会 reject。这依赖signOut先cancel(name)关闭监听再删记录server/mcp-oauth.ts以及回调响应头里的Connection: closeserver/mcp-oauth.ts——否则一条 keep-alive 连接会把下一次登录的回调送到已经停止接收的监听上。环境变量与浏览器准备文档给出的复用方式与默认值# 复用已安装的 agent-browser 与 Chrome export OMB_AGENT_BROWSER_PATH/path/to/agent-browser export AGENT_BROWSER_EXECUTABLE_PATH/path/to/chrome # 自定义截图证据目录默认 .omb-scratch/mcp-oauth-evidence export OMB_UI_EVIDENCE_DIR/tmp/mcp-evidenceensureUiBrowser的解析顺序是OMB_AGENT_BROWSER_PATH→ 工具目录 →PATH最后才下载固定版本Chrome 同理走AGENT_BROWSER_EXECUTABLE_PATH两者都落在同一个UI_TOOLS_DIR下。若显式设置了AGENT_BROWSER_EXECUTABLE_PATH但文件不存在会直接报错并提示 unset it to use the Chrome agent-browser installsscripts/testing/control-omb-ui.ts。夹具自身还给浏览器进程注入了严格环境scripts/verify-mcp-sign-in.tsHOME/USERPROFILE指向临时数据目录、TMPDIR指到其下的browser-tmp、AGENT_BROWSER_SESSION带上 PID 避免串会话、AGENT_BROWSER_HEADLESS1、AGENT_BROWSER_NO_WEBMCP1。这些是绝不接触用户正在运行的应用的具体落地方式。脚本成功后输出一行 JSON把验证过的项目显式列出便于 CI 断言{ok:true,evidence:/abs/path/.omb-scratch/mcp-oauth-evidence, tested:[remote admin start,blocked popup,invalid URL retry,pending spinner and preserved input, cancel and retry during pending completion,paste completion,MCP tools,logout cancellation]}服务端实现一次登录的完整生命周期阶段一发现授权元数据discoverMcpAuth先向 MCP 服务器发一次initialize若返回 401 就取WWW-Authenticate里的resource_metadatachallenge()见 server/mcp-oauth.ts再按顺序尝试/.well-known/oauth-protected-resourcepath与/.well-known/oauth-protected-resource拿到authorization_servers[0]作为 issuer 后再试/.well-known/oauth-authorization-server、/.well-known/openid-configuration。server/mcp-oauth-discovery.ts 里有几条硬性约束isAllowedAuthUrl只放行 https 与 loopback http所有抓取都带redirect: error不跟随重定向、5 秒超时、64 KiB 上限必须声明支持S256元数据里的issuer必须与抓取时使用的 issuer 一致RFC 8414 §3.3。最终把 MCP URL 本身作为 RFC 8707 的resource返回使 token 具备受众绑定。阶段二注册客户端或复用预注册应用如果用户在服务器条目上配置了oauthclientId可选clientSecret、scopes就走预注册应用路径否则尝试动态注册。注册时请求体固定为公共客户端client_name: OpenMausBot, redirect_uris: [redirectUri], grant_types: [authorization_code, refresh_token], response_types: [code], token_endpoint_auth_method: none,若元数据里没有registration_endpoint抛McpOAuthError(no-registration, ...)并在提示里直接教用户编辑服务器在 Sign-in app 下填该应用的 client ID——这是常见的一类失败。回环端口不是随机的而是对 URL 做 FNV-1a 哈希后映射到20000 hash % 20000server/mcp-oauth.ts。这样做是为了让预注册应用每次回到同一个redirect_uri。因此当端口被占用且存在预注册客户端时服务端不会退让到随机端口而是报port-busy并提示关闭占用该端口的程序对应测试explains a busy redirect port instead of moving to another one只有动态注册场景才会listen(server, 0)。阶段三拼装授权 URLauthorize.searchParams.set(response_type, code); authorize.searchParams.set(client_id, client.id); authorize.searchParams.set(redirect_uri, redirectUri); authorize.searchParams.set(state, state); // randomBytes(32) base64url authorize.searchParams.set(code_challenge, createHash(sha256).update(verifier).digest(base64url)); authorize.searchParams.set(code_challenge_method, S256); authorize.searchParams.set(resource, meta.resource); if (scopes?.length) authorize.searchParams.set(scope, scopes.join( ));state与verifier各 32 字节flowId为randomUUID()默认有效期 5 分钟DEFAULT_LIFETIME_MS到期定时器unref()以免拖住进程退出。此处还决定pasteBack...(server starting.browser.remote ? { pasteBack: true } : {}),即存在本地监听 浏览器在远程这一组合才需要粘贴回填。阶段四两种回收路径回环监听回收server.on(request, ...)落到callback()先校验Host必须等于redirectUri的 host、URL 必须以/开头然后交给answer()响应固定Cache-Control: no-store、Referrer-Policy: no-referrer、Connection: close正文只使用固定话术Signed in. You can close this tab and return to OpenMausBot. / Invalid or expired sign-in...绝不回显code或提供方自己的错误文本。公开回调回收publicCallback(host, search)遍历所有流程要求该流程没有本地监听、host 匹配、且state恒定时间相等然后调用同一个acceptCallback。所以远程 https 回调与本地监听共享全部校验逻辑包括iss比对RFC 9207允许 Google 那种 issuer 尾部斜杠差异。粘贴回收completeCallback先在本地解析 URL长度上限 16384再走同一套校验绝不向用户提供的地址发起请求。阶段五兑换、落盘与善后授权码兑换固定走表单 POST形参包含grant_typeauthorization_code、code、redirect_uri、code_verifier、resource10 秒超时。若流程在兑换期间被取消或条目被移除服务端不会复活登录而是把刚拿到的 token 送去revocationEndpoint并返回内部标记DISCARDED最终对调用方表现为 409if (this.flows.get(flow.name) ! flow || flow.status.phase ! waiting) { if (flow.meta.revocationEndpoint) { void postForm(flow.meta.revocationEndpoint, { token: tokens.refresh ?? tokens.access }, flow.client).catch(() undefined); } return DISCARDED; }成功时写入的记录包含issuer、clientId、可选的tokenAuth、redirectUri、tokenEndpoint、revocationEndpoint与tokens。tokenAuth依据 RFC 8414 §2 推导没有 secret 即none有 secret 且元数据未声明或声明支持client_secret_basic默认用 Basic并按 RFC 6749 §2.3.1 先做表单编码仅当服务器只接受client_secret_post时才把 secret 放进表单。凭据存放为什么 token 不在 config.json 里server/mcp-oauth-store.ts 的第一行注释就把设计意图写死了与config.json分开存放这样粘贴或导出配置时不会带上 token并且 token 永远不会跟着条目迁移到别的 URL。get(name, url)会校验记录里的url与请求一致因此把一个服务器的登录记录改指向另一个地址时旧 token 自动失效对应测试never hands a token to a different URL。落盘用writeFileAtomic(..., { mode: 0o600 })读取则是每次都重读因为另一个进程可能刚改过这个文件。访问 token 的取用由accessToken统一把关距过期不足 2 分钟REFRESH_MARGIN_MS就刷新并发调用共享同一个Promise刷新返回invalid_grant或invalid_client或 400/401才判定凭据已死并标记needs-sign-in其它失败只要旧 token 仍在有效期内就继续使用。最后一环是withMcpSignInserver/mcp-oauth.ts需要登录的服务器根本不会进入这一轮的服务器集合已登录的会先剔除原有Authorization头再注入Authorization: Bearer token。这样引擎侧只看到请求头永远拿不到 token 本体。路由与界面契约服务端路由集中在 server/routes/mcp-servers.ts共四个动作方法与路径作用关键约束POST /api/mcp/servers/name/sign-in启动流程返回{ auth: McpSignInStatus }会话在启动后失效则撤销并 401GET /api/mcp/servers/name/sign-in/flowId查询状态非本浏览器持有者返回 404DELETE /api/mcp/servers/name/sign-in[/flowId]取消所有者不符抛 404POST /api/mcp/servers/name/sign-in/flowId提交粘贴的回调必须application/json正文上限 20480 字节POST /api/mcp/servers/name/sign-out登出先撤销 token 再删记录非 URL 服务器直接 400Only servers reached by URL can be signed in to.运行在别的电脑上的服务器 409runsElsewhere。所有者来自auth.kind session ? auth.session.id : loopback——这就是另一个浏览器的登录进行中会得到 409 的来源。前端驱动在 src/lib/mcp-sign-in.ts先POST启动拿到的链接必须过mcpSignInLink的 https 检查否则立即取消并返回failed随后每 1500ms 轮询一次状态同时用onStarted把complete回调交给面板使得用户粘贴后立刻完成不必等下一轮轮询。取消走DELETE且被记忆化cancellation ?? ...保证多次取消只发一次请求abort事件与completed、sleep三者用Promise.race竞争所以取消不必等待弹窗或状态请求落定。常见失败与对应含义现象 / 错误文本来源与含义This server does not offer an OAuth sign-in. Add its token as a header instead.发现阶段拿不到可用的元数据not-oauthThis server only signs in apps registered with it in advance. Edit the server and add that apps client ID under Sign-in app.无registration_endpointno-registrationPort n on this computer is in use...预注册客户端 固定回环端口被占用port-busyA sign-in is already in progress in another browser.同一服务器被另一个所有者占用409This redirect URL does not belong to the current sign-in.回调 origin/path/state/iss/参数重复等校验失败400This sign-in has ended or is already being completed.重复提交、已取消或已过期409This sign-in expired. Start again.超过 5 分钟有效期监听已关闭Your session ended. Start again.管理会话失效revokeOwner已撤销流程运行验证的实践建议先在纯测试层定位问题mcp-oauth.test.ts里的远程场景不依赖浏览器出错信息直接指到具体校验分支比跑端到端快得多。端到端只在隔离夹具中跑直接执行node --experimental-strip-types scripts/verify-mcp-sign-in.ts不要把它接到正在使用的 OpenMausBot 上脚本会自己launchVerificationServer()并在finally中依次关闭浏览器、预览、夹具服务、假 MCP 与假 OAuth。保留证据OMB_UI_EVIDENCE_DIR下的before.png/waiting.png/authenticating.png/signed-in.png是四张关键帧分别对应初始状态已弹出粘贴表单交换中已登录并连通。CI 里复用已装浏览器设置OMB_AGENT_BROWSER_PATH与AGENT_BROWSER_EXECUTABLE_PATH可以跳过固定版本下载若未设置夹具会按大小与 SHA-256 校验后安装。看日志而不是猜临时数据目录与server-timestamp-pid.log会随失败信息一起打印确认服务端行为前先读它。这条验证链路的价值不在于跑绿了一次而在于它把 MCP 远程登录的每一条安全边界——一次性状态、单值参数、恒定时间比较、所有者隔离、取消即作废、登出即关监听、token 不进配置文件——都变成了可重复执行、可截图留证、可在 CI 中回归的断言。赞分享人工智能AI AgentAI 应用大模型交互助手工具调用Agent 编排桌面应用【免费下载链接】OpenMausBotOpen-source Grok Bot alternative with a virtual machine that bots can use项目地址https://gitcode.com/GitHub_Trending/op/OpenMausBot点击查看免费下载相关推荐RAWmakase 远程推理实战用 ONNX Runtime 把 AI 蒙版跑在另一台电脑上RAWmakase 远程推理实战用 ONNX Runtime 把 AI 蒙版跑在另一台电脑上 RAWmakase 是一款免费、快速、兼容 Lightroompi-mcp-adapter认证实战OAuth 2.1PKCE如何安全登录远程MCP服务器pi mcp adapter认证实战OAuth 2.1PKCE如何安全登录远程MCP服务器 pi mcp adapter 是一款为 Pi coding agMCP TypeScript SDK 客户端 OAuth 授权码登录实战从 OAuthClientProvider 到 finishAuth 的完整接入指南MCP TypeScript SDK 客户端 OAuth 授权码登录实战从 OAuthClientProvider 到 finishAuth 的完整接入指南人工智能MCP 服务MCP Clients上一篇PowerJob分布式调度框架终极指南企业级任务调度与分布式计算架构解析下一篇amlogic-s9xxx-armbian 机顶盒 Armbian 网卡无法识别排查指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →