feat(cli): complete a login by pasting the redirect when loopback is unreachable - #36
Merged
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
prk loginopens 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 to127.0.0.1reaches 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 -Ltunnel is built entirely on the client side, so a forwarded port and an unforwarded one are the samebindand the sameacceptfrom insideprk— no syscall or environment variable separates them. The tempting signals are wrong in both directions:SSH_CONNECTIONis unset insidetmuxand stripped bysudo, 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:
A login that completes in the browser needs nothing pasted. One that cannot — the browser fails to load a
127.0.0.1address — 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.Safety and scope
--no-input, off when stdin is not a terminal. A scripted login is byte-for-byte unchanged.stateis 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.REDIRECT_UNREADABLE(exit 11), distinct fromSTATE_MISMATCH(a redirect from a different login).spawn_blockingtasks: 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.--jsonreports which channel completed the login ("redirect": "loopback"|"pasted").Device flow — the
gh auth loginalternative that avoids a redirect entirely — is not available: Cloudflare Access advertises onlyauthorization_codeandrefresh_token, with no device authorization endpoint (verified against the live authorization-server metadata). A redirect is mandatory either way.Testing
mise run cigreen end to end:test:rust663 (+13),miri127, plustest:js,e2e, and the full lint/typecheck set.distprofile, installed it on the host, and completed an interactive sign-in end to end by pasting the redirect.🤖 Generated with Claude Code