Skip to content

fix(auth): log in when the browser is not on this machine - #1418

Draft
cristifalcas wants to merge 1 commit into
mainfrom
cf/auth-login-without-a-local-browser
Draft

fix(auth): log in when the browser is not on this machine#1418
cristifalcas wants to merge 1 commit into
mainfrom
cf/auth-login-without-a-local-browser

Conversation

@cristifalcas

Copy link
Copy Markdown
Contributor

Fixes aspect auth login on Linux and over SSH. Closes ENG-2098.

The two faults

1. The opener was macOS-only. _run_browser_login shelled out to open. On Linux that is at best Debian's xdg-utils symlink, so a login either recited www-browser: not found six times or died outright:

error: failed to spawn command open "https://auth.aspect.build/oauth/authorize?...":
       No such file or directory (os error 2)

2. The one that actually blocks people. The redirect is a loopback address. Paste the URL into a browser on another machine and the code is delivered to that machine's port:

This site can't be reached — 127.0.0.1 refused to connect. ERR_CONNECTION_REFUSED

…and the CLI waits forever. No amount of browser-opening fixes this. For a self-hosted deployment the edge's own callback page is explicit about it (jwt-validator/files/envoy.yaml:106):

location.replace("http://127.0.0.1:" + p + "/callback" + location.search);

The consolation: location.search is forwarded whole, so the address bar already holds the code. It is stranded, not lost.

What this does

Opening a browser moves into the runtime as ctx.aspect.auth.open_browser, carrying the platform table: $BROWSER first, then xdg-open / gio open / gnome-open / kde-open / x-www-browser / wslview on Linux, open on macOS, and rundll32 url.dll,FileProtocolHandler on Windows — that last because it takes the URL as one argument, where cmd /c start would hand an &-separated authorize URL back to cmd.exe to re-parse. Launcher output is discarded, so the six-line not found recital is replaced by one message that says what to do instead.

It doesn't try at all under SSH. A display counts only if it names a host (localhost:10.0, what ssh -X sets); a bare :0 is the remote machine's own screen — which is what a Cloud Workstation image sets, and where a browser opens in front of nobody.

AuthSession.redeem(pasted) is the login path for those sessions. It accepts the browser's full address-bar URL, a bare query string, or the bare code. wait() and redeem() now share one complete(), so both reach an identical token exchange.

--no-browser forces the paste flow on login and configure.

Open this URL in a browser to log in:

  https://silo-gcp.auth.aspect.build/oauth/authorize?...&state=YDBVcr...59173

Authorizing sends the browser on to http://127.0.0.1:59173/callback?code=...
— an address only this machine serves. If the browser is on another machine
it will report "refused to connect": that is expected, and the address bar
still holds the code. Copy the whole address and paste it below.

Paste the callback URL (or just the code):

Notes on the choices

  • CSRF guard kept. state is validated whenever the paste carries it; a stale paste from an earlier run is rejected. A bare code arrives without one, and there the paste is the user's intent, so require_state is false for that path only.
  • Off a TTY the listener is still waited on — there is nothing to paste with, and a forwarded port still delivers.
  • No port-forwarding hint in the prompt. It is only durable for the Aspect account (fixed 19556); a self-hosted deployment binds a fresh port each run, so "forward 59173 and re-run" hands the user a port that no longer exists. The caveat lives in --no-browser's help text instead.
  • No new dependency. The platform table is ~40 lines; adding the open/webbrowser crate would mean Cargo.lock and crate_universe churn for less.

Test plan

538 tests pass (cargo test -p axl-runtime -p aspect-cli), including new unit coverage for parse_pasted_callback, is_remote_session, is_forwarded_display and browser_launchers.

Driven by hand under a pty, since the branch that matters needs a TTY:

case result
SSH + DISPLAY=:0 (workstation shape) no launcher attempted, paste prompt
SSH + DISPLAY=localhost:10.0 browser opened, waits on listener
local, no SSH browser opened, waits on listener
--no-browser paste prompt
no TTY prints URL, waits on listener
loopback delivery (curl to the listener) success page served, exchange attempted
paste of a full 127.0.0.1 URL, --deployment silo-gcp state validated, OIDC token endpoint resolved, exchange posted
paste of a previous run's URL rejected: "callback state did not match"
paste with no code param "no `code` parameter in what was pasted…"
empty paste "No authorization code entered; not logged in."

Both flows fail identically on an invented code, which is as close as this gets to proving redeem and wait are equivalent without a real credential.

Also verified directly on gcloud workstations ssh to an aspect-internal-dev workstation: SSH_CONNECTION set, DISPLAY empty (detection fires), and the workstation reaches auth.aspect.build/oauth/token, so the exchange completes from there.

Not verified: a real end-to-end login with a live code (needs a browser identity), and the Linux launcher table on Linux — though on a workstation it is never reached, since detection short-circuits first.

Aside, for the SaaS launch

auth.aspect.build advertises device_authorization_endpoint and the device_code grant — RFC 8628, the flow gh uses, and the properly correct answer to this whole class of problem. The client is currently refused with Invalid client_id, so it wants enabling on the Frontegg side; not something the CLI can land on its own.

🤖 Generated with Claude Code

`aspect auth login` opened the browser by shelling out to `open`, then waited
for the OAuth code on a loopback listener. Both halves fail away from a desktop:

  - `open` is macOS. On Linux it is at best Debian's xdg-utils symlink, so a
    login either recited `www-browser: not found` six times or died with
    "failed to spawn command open" and an AXL traceback (ENG-2098).
  - Worse, the redirect is a loopback address. Paste the URL into a browser on
    another machine and the code is delivered to *that* machine's port —
    ERR_CONNECTION_REFUSED for the user and a CLI that waits forever. No amount
    of browser-opening fixes it; over SSH the code has to come back by hand.

Opening a browser moves into the runtime as `ctx.aspect.auth.open_browser`,
which carries the platform table (`$BROWSER`, then xdg-open/gio/gnome-open/
kde-open/x-www-browser/wslview on Linux, `open` on macOS, rundll32 on Windows —
it takes the URL as one argument, where `cmd /c start` would hand an
`&`-separated authorize URL back to cmd.exe to re-parse). Launcher output is
discarded; the task prints one message that says what to do instead.

It reports "headless" without trying at all when this is an SSH session with no
forwarded display. A display counts only if it names a host (`localhost:10.0`,
what ssh -X sets) — a bare `:0` is the remote machine's own screen, which is
what a Cloud Workstation image sets and where a browser would open in front of
nobody.

`AuthSession.redeem(pasted)` is then the login path for those sessions: it takes
the browser's full address-bar URL, a bare query string, or the bare code.
`wait()` and `redeem()` now share one `complete()`, so both reach an identical
token exchange; `state` is still validated whenever it is present, and a stale
paste from an earlier run is rejected. Off a TTY there is nothing to paste with,
so the listener is still waited on — a forwarded port delivers.

`--no-browser` forces the paste flow on both `login` and `configure`. The port
is only worth forwarding for the Aspect account, which binds a fixed 19556; a
self-hosted deployment takes a fresh port each run, so that caveat lives in the
flag's help text rather than as a hint the user cannot act on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: f8955338-bfc4-4175-8dc1-03b814cd0ce7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cristifalcas
cristifalcas marked this pull request as draft September 1, 2026 14:01

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2ef89800e5

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +1628 to +1629
for entry in browser_env.unwrap_or_default().split(':') {
let argv: Vec<String> = entry.split_whitespace().map(str::to_string).collect();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve quoting when parsing BROWSER commands

When $BROWSER contains a quoted command line such as firefox --new-window "%s" or sh -c 'open-browser %s', split_whitespace() leaves the quote characters in the arguments and splits shell expressions incorrectly. spawn_launcher then substitutes the authorize URL into those malformed arguments; if the launcher still exits successfully, the CLI reports that the browser opened and waits indefinitely even though it received an invalid URL. Parse each colon-separated entry with shell-aware argument parsing rather than raw whitespace splitting.

Useful? React with 👍 / 👎.

@aspect-workflows

aspect-workflows Bot commented Sep 1, 2026

Copy link
Copy Markdown

Aspect Workflows Tasks

📅 Tue Sep 1 14:12:48 UTC 2026

Task Results

Fix

❌ buildifier (buildifier-gha-debug · buildifier-gha)

bazel buildifier --severity=info -- crates/aspect-cli/src/builtins/aspect/auth.axl

Install aspect: aspect.build/docs/cli/install

Reproduce

❌ buildifier (buildifier-gha-debug · buildifier-gha)

bazel buildifier --severity=fail --base-ref=origin/main

❌ delivery (delivery-uncacheable · delivery-gha-debug · delivery-gha)

# --mode=always --track-state=false for off-runner with no state backend.
aspect delivery \
  --commit-sha=2ef89800e5ec3aaebcdbe5b9d4eb9d303cdd3a48 \
  --mode=always \
  --track-state=false \
  --dry-run=true

Install aspect: aspect.build/docs/cli/install


⏱ Last updated Tue Sep 1 14:35:40 UTC 2026 · 📊 GitHub API quota 782/15,000 (5% used, resets in 36m)
🚀 Powered by Aspect CLI (v0.0.0-dev)  |  Aspect Build · X · LinkedIn · YouTube

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