从“登录成功但连不上”到完整授权:WorkBuddy 远程 MCP OAuth 排障实战
前言
给远程 MCP 增加企业用户登录时,最容易产生误判的场景不是“登录页打不开”,而是:
- WorkBuddy 能发现 MCP;
- 浏览器能打开登录页;
- 用户输入正确账号和密码;
- 页面显示“授权已提交,请返回 WorkBuddy”;
- 但 WorkBuddy 一直没有连接成功,工具列表也没有加载。
表面上看用户已经登录,实际上 OAuth 可能只完成了前半段。本文记录一套通用排障方法,重点覆盖 OAuth discovery、动态客户端注册、Authorization Code + PKCE、桌面回环地址、自定义 Deep Link 以及重复提交问题。
本文使用的服务端是 Python/FastMCP,但排障思路同样适用于其他语言。
配套代码、测试与上线清单:workbuddy-mcp-oauth-guide
一、先区分 MCP 请求和 OAuth 请求
远程 MCP 首次连接通常包含两条相互关联但职责不同的链路:
- MCP 资源服务器负责
/mcp,接收工具发现和工具调用。 - OAuth 授权服务器负责
/authorize、/token、/register和登录页。
首次连接时,客户端没有 Access Token,因此下面的请求是正常的:
POST /mcp -> 401 Unauthorized
GET /.well-known/oauth-protected-resource/mcp -> 200
GET /.well-known/oauth-authorization-server -> 200
这里的 401 不是最终故障,而是资源服务器告诉客户端“这个资源需要 OAuth,以及去哪里授权”。
同样,探活程序或客户端如果执行:
GET /mcp -> 405 Method Not Allowed
HEAD /mcp -> 405 Method Not Allowed
也不代表 Streamable HTTP MCP 已损坏。关键要看客户端是否随后继续 discovery、注册和授权流程。
二、正确的完整成功链路
一次完整的 OAuth 连接至少应该在服务端日志中留下以下阶段:
1. POST /mcp -> 401
2. GET /.well-known/oauth-protected-resource/mcp -> 200
3. GET /.well-known/oauth-authorization-server -> 200
4. POST /register -> 201
5. GET /authorize -> 302 登录页
6. GET /oauth/login -> 200
7. POST /oauth/login -> 302 回调 URI
8. POST /token -> 200
9. POST /mcp (Bearer Access Token) -> 200
如果日志停在第 7 步,说明账号密码校验成功,但授权结果没有回到桌面客户端,或者客户端没有继续换 Token。
所以页面上显示“登录成功”只能证明业务身份校验成功,不能证明 OAuth 完成。
三、问题一:动态客户端注册响应不兼容
桌面 MCP 客户端通常使用 OAuth Dynamic Client Registration。客户端把自己的回调 URI 发到 /register,服务端返回 client_id。
成功响应应使用 2xx 状态码,并包含客户端后续授权所需的字段。例如:
{
"client_id": "generated-client-id",
"redirect_uris": [
"http://127.0.0.1:18484/oauth/callback"
],
"token_endpoint_auth_method": "none",
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"]
}
常见错误包括:
- 把“客户端已注册”作为异常返回 400;
- 返回的
redirect_uris与客户端提交值不一致; - 公共桌面客户端却要求
client_secret; - 注册成功但服务重启后丢失客户端,旧
client_id直接变成unauthorized_client; - 客户端数量无上限,长期运行后内存持续增长。
修复时应做到:
- 新注册返回标准 201;
token_endpoint_auth_method对 PKCE 桌面客户端使用none;- 保存精确的回调 URI;
- 为动态客户端设置 TTL 和容量上限;
- 只回收没有活跃授权请求的旧客户端;
- 如需恢复客户端缓存,只恢复格式和回调路径都严格匹配的记录。
四、问题二:桌面回调没有真正回到 WorkBuddy
桌面客户端常见两种回调方式。
1. Loopback 回调
http://127.0.0.1:<随机端口>/oauth/callback
WorkBuddy 在本机启动临时 HTTP listener。登录结束后,浏览器访问该地址,客户端接收 code 和 state。
服务端必须原样返回注册时的主机、端口和路径,不能擅自固定端口,也不能把 127.0.0.1 改成 localhost。
2. 自定义 Deep Link
workbuddy://workbuddy/mcp/<connector-reference>/oauth/callback
操作系统把这个 URI 交给 WorkBuddy。服务端同样必须使用客户端注册的完整 URI,不能返回另一个“看起来差不多”的地址。
如果页面显示授权成功但 WorkBuddy 没反应,应检查:
/authorize中的redirect_uri;/register保存的回调;- 登录成功响应的
Location; - 三者是否逐字一致;
- 回调是否带上原始
state; - Deep Link 是否真的被操作系统交给 WorkBuddy;
- loopback 端口是否有客户端监听。
五、问题三:第一次点击没反应,第二次才提示提交
登录按钮的重复点击问题容易掩盖真实故障。用户第一次点击后请求可能仍在处理,第二次点击会并发提交同一个 request_id。
如果服务端实现为“读取 pending 请求 -> 调后端登录 -> 删除 pending 请求”,两个并发请求可能同时读到 pending 状态,导致:
- 生成两个授权码;
- 一个请求删除状态后,另一个请求报“授权已失效”;
- 页面展示成功,但客户端收到的是另一个请求的回调;
- 后端登录被调用两次。
修复目标不是简单禁用前端按钮,而是让服务端完成原子化和幂等处理:
- 同一
request_id同时只允许一个请求执行登录; - 第一个请求生成唯一授权码;
- 重复请求不能再次调用业务登录接口;
- 授权码只能消费一次;
- 页面不得在回调真正发出前显示“已完成”。
前端禁用按钮可以改善体验,但服务端幂等才是安全边界。
六、PKCE 和 Token 交换不能省略
桌面客户端是 public client,不适合保存 Client Secret,因此需要 PKCE。
客户端在 /authorize 提交:
code_challenge=<S256 challenge>
code_challenge_method=S256
换 Token 时提交:
grant_type=authorization_code
code=<one-time authorization code>
code_verifier=<original verifier>
client_id=<registered client>
redirect_uri=<exact registered redirect URI>
resource=<MCP resource URI>
服务端必须校验:
- 授权码未过期且未被消费;
client_id一致;redirect_uri完全一致;code_verifier能生成原code_challenge;- Token 的
resource/audience与 MCP 资源一致; - scope 不超过用户授权范围。
如果日志没有 POST /token,不要继续排查 MCP 工具。此时问题仍在浏览器回调或桌面客户端接收回调的阶段。
七、回调 URI 兼容不能以牺牲安全为代价
为了让 Deep Link 能工作,有些实现会允许所有自定义 scheme,这是危险的。攻击者可以注册恶意 URI,把授权码导向自己的应用。
推荐策略:
- 公网 Web 回调只允许 HTTPS;
- HTTP 只允许
127.0.0.1、localhost或::1的 loopback; - 自定义 scheme 使用精确 allowlist;
- 禁止 URI fragment;
- 禁止 username/password userinfo;
/authorize的回调必须与/register保存值精确匹配;state必须原样返回;- 授权码设置短 TTL 并单次消费。
本仓库的 examples/redirect_policy.py 展示了一个最小策略实现。
八、登录身份不能等同于业务权限
OAuth 解决的是“当前用户是谁”和“是否允许进入 MCP”。它不能自动保证用户只看到自己的业务数据。
正确做法是:
- 用户在 OAuth 页面完成企业身份校验;
- 服务端把 OAuth Token 绑定到可信的用户 ID 或后端 Session;
- 每次 MCP 工具调用都携带当前用户身份访问业务后端;
- 由业务后端继续执行数据范围过滤;
- MCP 不设置共享管理员身份,也不在用户身份丢失时回退到管理员 Token。
如果 HR A 的 Token 可以查询 HR B 的数据,那么即使 OAuth 协议完全正确,系统仍然存在越权漏洞。
至少要完成以下交叉测试:
- A 能查询 A 的数据;
- B 能查询 B 的数据;
- A 查询 B 的对象返回 404 或 403;
- B 查询 A 的对象返回 404 或 403;
- 管理员不能通过仅面向 HR 的 OAuth 入口登录;
- 非白名单用户在密码校验前就被拒绝;
- Token 过期、撤销或服务端 Session 失效后不能回退到共享身份。
九、如何读取日志
以下状态通常是正常的:
POST /mcp -> 401
GET /.well-known/oauth-protected-resource/mcp -> 200
GET /.well-known/oauth-authorization-server -> 200
POST /register -> 201
GET /authorize -> 302
GET /oauth/login -> 200
POST /token -> 200
以下组合值得重点检查:
/register -> 400
检查动态注册请求格式、重复注册处理、回调 URI 校验以及容量限制。
/authorize -> 302,登录页正常,但没有 /token
检查最终 Location、Deep Link、loopback listener、state 和客户端回调接收。
登录接口被请求两次
检查按钮重复提交、并发请求以及 request_id 幂等处理。
/token -> 400 invalid_grant
检查授权码是否重复消费、PKCE verifier、redirect_uri、client_id 和有效期。
带 Token 的 /mcp -> 401
检查 Token 是否过期、scope、resource/audience、issuer 和 Bearer Header。
十、本地最小化诊断方法
不要一开始就在生产环境同时调试业务登录、数据库、反向代理和 WorkBuddy。
建议先启动一个只监听本机的诊断 MCP:
- 地址使用
http://127.0.0.1:<port>/mcp; - 只创建一个本地虚拟用户;
- 只提供一个
whoami工具; - 不连接生产数据库、Redis 和外部业务系统;
- 日志记录 OAuth 阶段,但不记录密码、完整 Token 和授权码。
如果本地诊断服务能连接,问题通常位于生产 HTTPS、Nginx 或业务登录接口;如果本地也失败,应优先检查 WorkBuddy 注册和回调兼容。
十一、生产上线检查
上线前至少完成:
- OAuth 和 MCP 公网入口使用可信 HTTPS;
- discovery metadata 中的 issuer、resource 和端点地址一致;
/register、/authorize、登录和/token分别限流;- pending request、动态客户端、授权码和 Token 都有 TTL 与容量上限;
- 密码、Token、授权码、Cookie 和业务 Session 不写日志;
- Access Token 短期有效,Refresh Token 支持轮换与撤销;
- 多实例部署时把 OAuth 状态放入 Redis 或数据库;
- 功能有独立开关和明确回滚方式;
- 至少使用两个真实权限不同的账号做交叉越权测试。
总结
WorkBuddy 页面显示“登录成功”后仍然连接失败,通常不是密码问题,而是 OAuth 后半段没有完成。最有效的排查方式,是按顺序验证:
discovery
-> dynamic registration
-> authorize
-> login
-> registered callback
-> token exchange
-> authenticated MCP request
-> per-user business authorization
只要逐段确认请求和状态码,就能避免在“401 是否正常”“是不是账号密码错了”“为什么第二次点击才有效”之间反复猜测。
兼容桌面客户端时,可以增加针对回调格式和客户端缓存的适配,但不能放开任意回调、跳过 PKCE、共享管理员 Token,或者把账号密码写入客户端配置。协议链路完整与业务权限隔离缺一不可。
更多推荐


所有评论(0)