Skip to content

feat(cli): complete a login by pasting the redirect when loopback is unreachable - #36

Merged
yashau merged 1 commit into
mainfrom
worktree-headless-login
Aug 20, 2026
Merged

feat(cli): complete a login by pasting the redirect when loopback is unreachable#36
yashau merged 1 commit into
mainfrom
worktree-headless-login

Conversation

@yashau

@yashau yashau commented Aug 20, 2026

Copy link
Copy Markdown
Owner

The problem

prk login opens a browser and waits for the OAuth redirect on a loopback listener. On a remote shell or in a container the browser runs on a different machine, so its redirect to 127.0.0.1 reaches the wrong host and the login cannot complete. The documented SSH-forwarding workaround needs the port forwarded before the CLI has bound it, and the port is OS-assigned — so in practice it does not help.

Why not just detect it

Whether the browser can reach this machine's loopback is not knowable before it tries. An ssh -L tunnel is built entirely on the client side, so a forwarded port and an unforwarded one are the same bind and the same accept from inside prk — no syscall or environment variable separates them. The tempting signals are wrong in both directions: SSH_CONNECTION is unset inside tmux and stripped by sudo, and WSL looks remote while its loopback is shared with the browser's.

The approach — race, don't guess

Both channels are opened and the first redirect wins:

  • the loopback listener, exactly as before; and
  • a redirect address pasted on stdin.

A login that completes in the browser needs nothing pasted. One that cannot — the browser fails to load a 127.0.0.1 address — is finished by copying that address out of the URL bar. Correct in every topology (SSH with and without -L, containers, WSL) with no flag and no detection.

$ prk login https://prick.example.com
Signing in to https://prick.example.com
Open this URL to continue:
  https://…cloudflareaccess.com/cdn-cgi/access/oauth/authorization?…
If the browser cannot reach this machine, it will fail to load a 127.0.0.1 address.
That is expected. Paste that whole address here and press Enter:

Safety and scope

  • The paste channel is on only when there is a terminal to answer it — off under --no-input, off when stdin is not a terminal. A scripted login is byte-for-byte unchanged.
  • A pasted line must be the whole redirect, never a bare code. state is the only thing binding a redirect to the login that started it, so accepting a code without it would be accepting a redirect nothing can check. Enforced, with no flag to relax it.
  • A line carrying no authorization response is rejected as REDIRECT_UNREADABLE (exit 11), distinct from STATE_MISMATCH (a redirect from a different login).
  • End of input is not treated as an answer, so a stray Ctrl-D leaves the loopback its full deadline.
  • The two waits are detached OS threads, not spawn_blocking tasks: a blocked terminal read cannot be cancelled, and a tokio runtime waits for blocking tasks at shutdown, so the losing channel would otherwise hold the process open until its own deadline. A detached thread ends with the process.
  • --json reports which channel completed the login ("redirect": "loopback" | "pasted").

Device flow — the gh auth login alternative that avoids a redirect entirely — is not available: Cloudflare Access advertises only authorization_code and refresh_token, with no device authorization endpoint (verified against the live authorization-server metadata). A redirect is mandatory either way.

Testing

  • New unit tests cover redirect parsing (whole URL, bare query, trailing newline, appended fragment, error redirect), the refusal of a bare code, and the race from both sides — loopback wins, paste completes a login the loopback never receives, a paste that never comes leaves the loopback its full deadline, and a bad paste ends the login rather than being swallowed.
  • mise run ci green end to end: test:rust 663 (+13), miri 127, plus test:js, e2e, and the full lint/typecheck set.
  • Verified live against a real deployment from a real remote shell with no port forwarding (so the paste path): compiled the branch with the published dist profile, installed it on the host, and completed an interactive sign-in end to end by pasting the redirect.

🤖 Generated with Claude Code

…unreachable

`prk login` opens a browser and waits for the authorization redirect on a
loopback listener. On a remote shell or in a container the browser runs on a
different machine, so its redirect to 127.0.0.1 reaches the wrong host and the
login could not complete -- the documented SSH-forwarding workaround needs a
port known only after the CLI binds it.

Whether the browser can reach this machine's loopback is not knowable before it
tries: an `ssh -L` tunnel is built entirely on the client side, so a forwarded
port and an unforwarded one are the same bind and the same accept from here, and
the signals that look promising are wrong in both directions -- SSH_CONNECTION
is unset inside tmux and stripped by sudo, and WSL looks remote while its
loopback is shared with the browser's. So rather than detect the case, both
channels are opened and the first redirect wins: the loopback listener as
before, and a redirect address pasted on stdin. A login that completes in the
browser needs nothing pasted; one that cannot falls back without a flag or a
guess.

The paste is accepted only when there is a terminal to answer it -- off under
--no-input and off when stdin is not a terminal -- so a scripted login is
unchanged. A pasted line must be the whole redirect, not a bare code: `state` is
the only thing binding a redirect to the login that started it, so accepting a
code without it would be accepting a redirect nothing can check. A line that
carries no authorization response is rejected as REDIRECT_UNREADABLE (exit 11)
rather than swallowed, and end of input is not treated as an answer, so the
loopback keeps its full deadline. `--json` reports which channel completed it.

Device flow, the gh-style alternative, is not available: Cloudflare Access
advertises only authorization_code and refresh_token, with no device
authorization endpoint, so a redirect is mandatory either way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@yashau
yashau merged commit 0e0756b into main Aug 20, 2026
20 checks passed
@yashau
yashau deleted the worktree-headless-login branch August 20, 2026 22:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant