RFC 8628 fixed CLI login in 2019. Most CLIs still ship the broken version

birdculture1 pts0 comments

CLI Authentication, the Right Way :: ABGEO's Personal websiteMenu ▾<br>Home<br>About<br>Projects<br>Blog<br>Book<br>Contact<br>Tags

CLI Authentication, the Right Way<br>18/06/2026 09:5710 min read<br>(1936 words)<br>#OAuth<br>#OpenID Connect<br>#CLI<br>#Security<br>#Authentication<br>I SSH into a fresh dev VM and run claude to start a session in there. The CLI prints a login URL<br>with http://localhost:54213/callback buried in the query string, tries to open a browser on the<br>remote box, and starts waiting for a callback. There is no browser on this box. The CLI catches the<br>failure, prints Paste code here if prompted, and hangs. I copy the URL into the browser on my<br>laptop, log in, and the consent page hands me a one-time code instead of a redirect. I paste it back<br>over SSH. It works. It is also 2009 wearing a 2026 t-shirt.<br>This is a solved problem. It has been solved since 2019. Most CLIs still have not caught up.<br>What most tools actually do#<br>The pattern is everywhere. gcloud auth login, wrangler login, the older vercel login, and a<br>long tail of vendor CLIs all run the same dance:<br>The CLI binds an HTTP server on 127.0.0.1 at some port. Wrangler picks 8976. gcloud uses 8085.<br>Claude Code grabs an ephemeral one each invocation.<br>It opens your system browser to the OAuth authorization endpoint with<br>redirect_uri=http://127.0.0.1:/callback.<br>You log in. The provider 302s back to the loopback URL with an authorization code.<br>The CLI&rsquo;s tiny HTTP server picks up the request, reads the code, exchanges it at the token<br>endpoint (usually with PKCE attached), and shuts<br>down.<br>You see the &ldquo;you can close this tab&rdquo; page that every CLI ships.<br>On a laptop it wraps up in about five seconds.<br>RFC 8252, the BCP for OAuth in native apps,<br>endorses this pattern when the app has a browser available, and for a developer running everything<br>on one machine, it is a good fit. What 8252 does not address is what to do when there isn&rsquo;t a<br>browser on the host. The rest of this post is about exactly that case.<br>Why you have probably never noticed#<br>The localhost step is invisible. The CLI prints a URL long enough that nobody reads it, but the<br>redirect URI is sitting right there in the query string:<br>gcloud auth login&rsquo;s authorization URL. Buried in the query string is redirect_uri=http%3A%2F%2Flocalhost%3A8085%2F, the loopback callback the CLI just bound.<br>You click through, log in on the provider&rsquo;s real domain, and approve. The provider 302s your browser<br>to the localhost callback. The CLI&rsquo;s tiny HTTP server reads the code, then immediately bounces you<br>to a polished &ldquo;you&rsquo;re signed in&rdquo; page back on the provider&rsquo;s actual website. The localhost URL<br>flashes in your address bar for a hundred milliseconds before the final redirect lands you here:<br>Caught mid-redirect. The browser just landed on localhost:8085, the authorization code is sitting in the query string, and the page is still loading. A blink later the CLI&rsquo;s local server replies with another redirect and the localhost URL is gone.<br>If you blink you miss it. Most users never realize their CLI bound a local HTTP server at all. The<br>flow looks like &ldquo;log in on a website, the CLI just knows&rdquo;, and the illusion holds right up until the<br>moment you try to use the CLI without a browser sitting next to it. The same design choice that<br>builds the illusion is the one that breaks the flow.<br>Where it breaks#<br>The whole thing rests on one assumption: the machine running the CLI is the machine running the<br>browser. Once that stops being true, the dance falls apart.<br>SSH sessions. No browser on the remote host. xdg-open either errors out or, with X<br>forwarding on, opens a browser on the remote box that you cannot see. You can tunnel the callback<br>port back to your laptop, but then the redirect URI registered with the provider has to allow<br>whatever port survives the tunnel. Most setups don&rsquo;t.<br>Containers. No browser inside, and most images don&rsquo;t even ship xdg-open or open. You can<br>punch the callback port through with -p, but only if you knew which port the CLI was going to<br>grab. Cloudflare&rsquo;s CLI has a long trail of<br>issues from people stuck on exactly this.<br>WSL. The browser opens on Windows. The loopback server runs on Linux. WSL2&rsquo;s port forwarding<br>gets it right most of the time. &ldquo;Most&rdquo; is the keyword.<br>Shared boxes. Anything else on that machine can read /proc/net/tcp to find the listening<br>port, or race to bind a known one. PKCE protects the code exchange. It does not protect the user&rsquo;s<br>authenticated session on the redirect itself.<br>Every CLI that ships this flow also ships a fallback for when it breaks. gcloud has<br>--no-launch-browser. Wrangler hangs, and the<br>accepted workaround is to curl the localhost<br>URL from a second terminal yourself. Anthropic&rsquo;s claude prints &ldquo;Paste code here if prompted&rdquo; and<br>waits. These are all manual device flows in disguise. They exist because the real flow does not work<br>where the CLI is actually being...

browser rsquo code http localhost callback

Related Articles