CLI Authentication, the Right Way
CLI Authentication, the Right Way
I SSH into a fresh dev VM and run claude to start a session in there. The CLI prints a login URL with http://localhost:54213/callback buried in the query string, tries to open a browser on the remote box, and starts waiting for a callback. There is no browser on this box. The CLI catches the failure, prints “Paste code here if prompted”, and hangs. I copy the URL into the browser on my laptop, log in, and the consent page hands me a one-time code instead of a redirect. I paste it back over SSH. It works. It is also 2009 wearing a 2026 t-shirt.
我通过 SSH 连接到一台全新的开发虚拟机,并运行 claude 来启动会话。CLI 在查询字符串中嵌入了 http://localhost:54213/callback 并打印出一个登录 URL,试图在远程主机上打开浏览器,并开始等待回调。但这台机器上并没有浏览器。CLI 捕获到失败,打印出“如果出现提示,请粘贴代码”,然后挂起。我将 URL 复制到笔记本电脑的浏览器中,登录后,授权页面给了我一个一次性代码,而不是重定向。我将其粘贴回 SSH 会话中。它成功了。但这感觉就像是穿着 2026 年 T 恤的 2009 年技术。
This is a solved problem. It has been solved since 2019. Most CLIs still have not caught up.
这是一个早已解决的问题。自 2019 年以来,它就已经有了解决方案。但大多数 CLI 工具至今仍未跟上步伐。
What most tools actually do
大多数工具的实际做法
The pattern is everywhere. gcloud auth login, wrangler login, the older vercel login, and a long tail of vendor CLIs all run the same dance:
这种模式随处可见。gcloud auth login、wrangler login、旧版的 vercel login 以及长尾的供应商 CLI 工具都在执行同样的流程:
-
The CLI binds an HTTP server on 127.0.0.1 at some port. Wrangler picks 8976. gcloud uses 8085. Claude Code grabs an ephemeral one each invocation.
-
It opens your system browser to the OAuth authorization endpoint with
redirect_uri=http://127.0.0.1:<port>/callback. -
You log in. The provider 302s back to the loopback URL with an authorization code.
-
The CLI’s tiny HTTP server picks up the request, reads the code, exchanges it at the token endpoint (usually with PKCE attached), and shuts down.
-
You see the “you can close this tab” page that every CLI ships.
-
CLI 在 127.0.0.1 的某个端口上绑定一个 HTTP 服务器。Wrangler 选择 8976,gcloud 使用 8085,Claude Code 则在每次调用时随机抓取一个临时端口。
-
它打开系统浏览器,跳转到 OAuth 授权端点,并附带
redirect_uri=http://127.0.0.1:<port>/callback。 -
你进行登录。提供商通过 302 重定向回到该回环 URL,并附带授权码。
-
CLI 的小型 HTTP 服务器接收请求,读取代码,在令牌端点进行交换(通常附带 PKCE),然后关闭。
-
你会看到每个 CLI 工具都会提供的“你可以关闭此标签页”页面。
On a laptop it wraps up in about five seconds. RFC 8252, the BCP for OAuth in native apps, endorses this pattern when the app has a browser available, and for a developer running everything on one machine, it is a good fit. What 8252 does not address is what to do when there isn’t a browser on the host. The rest of this post is about exactly that case.
在笔记本电脑上,整个过程大约五秒钟就能完成。RFC 8252(原生应用 OAuth 的最佳实践)在应用拥有可用浏览器时支持这种模式,对于在单机上运行所有内容的开发者来说,这非常合适。但 8252 没有解决的问题是:当主机上没有浏览器时该怎么办。本文接下来的内容正是针对这种情况。
Why you have probably never noticed
为什么你可能从未注意到这一点
The localhost step is invisible. The CLI prints a URL long enough that nobody reads it, but the redirect URI is sitting right there in the query string. You click through, log in on the provider’s real domain, and approve. The provider 302s your browser to the localhost callback. The CLI’s tiny HTTP server reads the code, then immediately bounces you to a polished “you’re signed in” page back on the provider’s actual website. The localhost URL flashes in your address bar for a hundred milliseconds before the final redirect lands you here.
本地回环步骤是不可见的。CLI 打印出的 URL 太长,没人会去读,但重定向 URI 就明晃晃地写在查询字符串里。你点击链接,在提供商的真实域名上登录并授权。提供商将你的浏览器 302 重定向到本地回环回调地址。CLI 的小型 HTTP 服务器读取代码,然后立即将你跳转到提供商官网上那个精致的“登录成功”页面。本地回环 URL 在你的地址栏中闪烁了不到一百毫秒,最终重定向将你带到目的地。
If you blink you miss it. Most users never realize their CLI bound a local HTTP server at all. The flow looks like “log in on a website, the CLI just knows”, and the illusion holds right up until the moment you try to use the CLI without a browser sitting next to it. The same design choice that builds the illusion is the one that breaks the flow.
如果你眨一下眼,就会错过它。大多数用户根本没意识到他们的 CLI 绑定了一个本地 HTTP 服务器。整个流程看起来就像是“在网站上登录,CLI 就自动感知到了”,这种幻觉一直持续到你尝试在没有浏览器的情况下使用 CLI 为止。正是这种构建幻觉的设计选择,导致了流程在特定场景下的失效。
Where it breaks
哪里出了问题
The whole thing rests on one assumption: the machine running the CLI is the machine running the browser. Once that stops being true, the dance falls apart.
整个机制基于一个假设:运行 CLI 的机器就是运行浏览器的机器。一旦这个假设不再成立,这套流程就会崩溃。
-
SSH sessions: No browser on the remote host.
xdg-openeither errors out or, with X forwarding on, opens a browser on the remote box that you cannot see. -
Containers: No browser inside, and most images don’t even ship
xdg-openoropen. -
WSL: The browser opens on Windows. The loopback server runs on Linux. WSL2’s port forwarding gets it right most of the time. “Most” is the keyword.
-
Shared boxes: Anything else on that machine can read
/proc/net/tcpto find the listening port, or race to bind a known one. -
SSH 会话: 远程主机上没有浏览器。
xdg-open要么报错,要么在开启 X 转发的情况下,在你看不到的远程主机上打开浏览器。 -
容器: 内部没有浏览器,大多数镜像甚至没有安装
xdg-open或open。 -
WSL: 浏览器在 Windows 上打开,而回环服务器在 Linux 上运行。WSL2 的端口转发大多数时候能正常工作,“大多数”是关键词。
-
共享主机: 机器上的其他进程可以读取
/proc/net/tcp来发现监听端口,或者抢先绑定已知端口。
Every CLI that ships this flow also ships a fallback for when it breaks. These are all manual device flows in disguise. They exist because the real flow does not work where the CLI is actually being used.
每个采用这种流程的 CLI 工具也都提供了故障时的备选方案。这些本质上都是伪装成手动设备流的方案。它们之所以存在,是因为真正的流程在 CLI 实际使用的环境中根本行不通。
The grant they should be using
他们应该使用的授权方式
The OAuth 2.0 Device Authorization Grant, RFC 8628, was published in 2019 for what the spec calls “input-constrained devices”. TVs, consoles, and yes, CLIs. The whole point is to decouple the device asking for the token from the device the user authenticates on.
OAuth 2.0 设备授权许可(RFC 8628)于 2019 年发布,专门用于规范所称的“输入受限设备”。包括电视、游戏机,当然还有 CLI。其核心目的在于将请求令牌的设备与用户进行身份验证的设备解耦。