远程服务器上怎么完成 OAuth 登录:SSH 隧道、粘贴回调与设备码三条路


你把 Hermes 装在一台云服务器上,或者在跳板机后面的开发机上,然后想把 Spotify 接进来,或者挂一个需要登录的 MCP 服务器(Linear、Sentry、Atlassian 这类)。命令敲下去,Hermes 打印一个授权链接,你复制到本地浏览器里点“同意”——然后浏览器跳到一个 127.0.0.1 的地址,页面上只有一行“无法访问此网站”。你回到服务器终端,那边还在安静地等一个永远回不来的回调。

这不是配置错误,而是 OAuth 流程的物理位置冲突:回调地址里的 127.0.0.1 指的是“浏览器所在的那台机器”,而监听器开在远端机器上。本文把三条出路讲清楚,顺便点名几个不需要隧道就能过的提供方。

为什么远程机器上登不上

先说清原理,一分钟就够。Hermes 里有两类 OAuth 登录:

  • 回环重定向(loopback redirect):Hermes 在本机起一个极小的 HTTP 服务,监听 127.0.0.1 的某个端口;授权完成后,授权服务器让你的浏览器跳到 http://127.0.0.1:端口/callback,这个小服务把授权码收下来。Spotify 和大多数需要 OAuth 的远程 MCP 服务器都是这种。
  • 设备码 / 粘贴码(device code / paste-the-code):不需要回调地址。授权服务器给你一个短码和验证网址,你在任意浏览器里完成授权,Hermes 轮询直到拿到令牌。

两种流程在同机场景下都顺畅。问题出在第一种:你的浏览器在笔记本上,它访问的 127.0.0.1 是笔记本自己的 127.0.0.1,而监听器绑定在服务器的 127.0.0.1 上,两者根本不是同一台机器。

那为什么不干脆让监听器绑 0.0.0.0、或者改成服务器的公网地址?因为授权方会把回调地址和预先登记的规则做严格比对。Spotify 和多数 MCP 授权服务器都要求回环形式的 http://127.0.0.1:端口/callback:绑定到 0.0.0.0 或换个端口,授权服务器会以“回调地址不匹配”直接拒绝。所以正确的做法不是改绑定,而是把那个回环端口原样“搬”到你的笔记本上——这就是 SSH 本地转发。

路线一:SSH 本地端口转发(用 Spotify 举例)

这条路线要在你本地机器上再开一个终端,专门拿着隧道:

# 本地终端:把笔记本的 43827 端口转发到远端服务器的 43827
ssh -N -L 43827:127.0.0.1:43827 user@remote-host

-N 的意思是“不要给我开远端 shell,只保持这条转发”。保持这个终端开着,直到登录成功。

然后在另一个 SSH 会话里跑授权命令:

ssh user@remote-host
hermes auth spotify --no-browser

--no-browser 会跳过“自动打开浏览器”这一步(远端也没有浏览器可用)。Hermes 会打印授权链接,以及一行关键信息——它实际绑定到的端口:

Waiting for callback on http://127.0.0.1:43827/callback

把授权链接复制到笔记本的浏览器里打开并同意。授权服务器把浏览器跳到 127.0.0.1:43827/callback;这一次这个地址指向你的笔记本,命中你的 SSH 隧道,请求被转发到远端监听器,终端里随即出现登录成功的提示,隧道随后可以关掉。

端口从哪来? 上面那行 Waiting for callback on ... 里写了。Spotify 默认 43827,但如果被占用,Hermes 可能自动换一个——请以那行输出为准,别照抄本文的数字。

经过跳板机的话,用 SSH 自带的 -J(ProxyJump),这样回环端口不会被暴露在跳板机上:

ssh -N -L 43827:127.0.0.1:43827 -J jump-user@jump-host user@final-host

老版本 OpenSSH 不支持 -J 时,等价的长写法是:

ssh -N \
    -o "ProxyCommand=ssh -W %h:%p jump-user@jump-host" \
    -L 43827:127.0.0.1:43827 \
    user@final-host

mosh / tmux / ControlMaster 这三个坑值得单独提:

  • 你在 mosh 会话里跑 Hermes?mosh 的漫游特性不会带上 -L 转发。请单独开一条普通 SSH 会话专门做隧道,让那条连接在授权期间保持存活;mosh/tmux 里照常使用 Hermes 不受影响。
  • 用了 ssh -o ControlMaster=auto 的话,端口转发依附于 master 连接的生命周期。隧道没起来时,先干掉 master 再重连:ssh -O exit user@remote-host,然后重新执行转发命令。

路线二:把回调 URL 粘回来(MCP 服务器)

远程 MCP 服务器走的是同一种回环流程,但 Hermes 在交互式终端里给了你一个更省事的选项:不用隧道,直接在提示符里把回调 URL 粘回去。

浏览器里点击同意后,页面会跳到 127.0.0.1 的地址并显示“无法访问”——这是正常的。你只需要把浏览器地址栏里的完整 URL 复制下来,粘到 Hermes 的提示符里:

  MCP OAuth: authorization required.
  Open this URL in your browser:

    https://mcp.linear.app/authorize?response_type=code&...

  Or paste the redirect URL here (or the ?code=...&state=... portion) and press Enter:
> https://mcp.linear.app/callback?code=abc123&state=xyz
  Got authorization code from paste — completing flow.

只贴 ?code=...&state=... 这一段也可以。这条路线不需要改任何 SSH 配置,任何 auth: oauth 的 MCP 服务器都能用。

需要无人值守地重授权(比如脚本里重新登录、没法交互粘贴)时,就还是回到路线一的隧道,因为它能自动收下回调。

顺带说一条桌面端的差异:桌面 App 的 Skills → MCP 面板里,回调由原生应用在你的电脑上接收,再转发给对应的连接与 profile,因此不需要隧道,也不依赖 dashboard.public_url;令牌留在所属的后端 profile 上。离开 MCP 标签页或切换范围会取消待处理的登录。

路线三:设备码与粘贴码(根本不需要隧道)

如果你要登录的提供方本来就用设备码流程,那么除了在浏览器里完成一次授权,不需要任何网络改造。下面这张表是“要不要隧道”的速查:

提供方 回环端口 需要隧道吗
Spotify 默认 43827 需要(Hermes 在远端时)
MCP 服务器(auth 为 oauth) 每台服务器自动选端口 需要,或者贴回回调 URL
xai-oauth(Grok SuperGrok) 不适用 不需要,设备码流程
anthropic(Claude Pro/Max) 不适用 不需要,粘贴验证码
openai-codex(ChatGPT Plus/Pro) 不适用 不需要,设备码流程
minimax、nous-portal 不适用 不需要,设备码流程
openrouter 仅本机 不需要:在 SSH 会话里 Hermes 自动切到无头流程,让你把浏览器里显示的验证码粘回来

OpenRouter 是最近新加的一条路:hermes auth add openrouter --type oauth 走浏览器 PKCE 流程,登录成功后会直接生成一个 API key 并放进凭据池(source 标记为 manual:openrouter_pkce),你不需要自己去网页上复制粘贴 key。它在本地用的是“先绑端口再拼回调地址”的回环监听器(端口由系统分配,避免了“探测空闲端口—关闭—再绑定”的竞态),并且针对远程会话特别处理:检测到 SSH 会话时改用 OpenRouter 的无头模式,不传 callback_url,把验证码显示出来让你粘贴。如果省略 --type,它仍然按 API key 的方式工作,所以老的 hermes auth add openrouter --api-key ... 用法没有变化。

四个实际会踩的坑

  1. 改配置后在正在运行的会话里登录,30 秒就超时。 你在会话里编辑 config.yaml 新增一个 OAuth MCP 服务器时,CLI 会用 30 秒的超时自动重载 MCP 连接——这个时间根本不够走完一次交互式授权,重载会放弃。请在新终端里用 hermes mcp login <server>,它没有这个 30 秒上限,会等你整整 5 分钟。
  2. 令牌写进了错误的 ~/.hermes。 令牌是写给“执行这条命令的那个用户”的。如果你的 gateway 或 systemd 服务以别的用户(比如 root 或专门的 hermes 用户)运行,就要以那个用户身份去登录,否则令牌落在你自己的家目录里,服务找不到。用 sudo -u hermes -i 之类的方式切换。
  3. bind [127.0.0.1]:43827: Address already in use。 你笔记本上这个端口被占了:可能是上次的隧道没退干净,也可能本地也跑着一份 Hermes。查一下并结束它:lsof -iTCP:43827 -sTCP:LISTEN,然后 kill 对应的 PID,再重试转发命令。
  4. “authorization timed out waiting for the local callback”。 说明回调没回到远端监听器。确认隧道那条终端还活着(ssh -N 本身不输出任何东西,所以要看它所在的终端),确认你用的端口来自最新那行 Waiting for callback on ...,必要时重启隧道再跑一次授权命令。

安全上多说一句:OAuth 令牌本质上是长期凭据,落盘位置和权限同样值得管理。凭据池与加密的相关做法见 凭据池与浏览器自动填充 与 Keychain 密钥加密;如果你要接的是第三方 MCP 服务器,建议先过一遍 MCP 目录安全指南 和 官方远程 MCP 目录。

小结

判断走哪条路,只需问自己一个问题:这个提供方是靠回调,还是靠设备码?

  • 靠回调(Spotify、OAuth MCP 服务器):交互式终端里优先选“粘回调 URL”;要自动化,就用 ssh -N -L 把回环端口搬到本地,记得端口以 Hermes 打印的那行为准,并注意 mosh/tmux/ControlMaster 的特例。
  • 靠设备码 / 粘贴码(xai-oauth、openai-codex、anthropic、minimax、nous-portal、OpenRouter 在 SSH 下):什么都不用改,打开链接、填码、回车。

一个常被忽略的收尾动作:登录完成后,别忘了把长期跑着的 gateway 或服务重启一次,让它在自己的进程里重新读一遍凭据。想从零搭一遍环境的话,安装指南 里有完整步骤。