Skip to content

fix(cli): surface auth URL so headless logins can complete - #3950

Closed
johrld wants to merge 2 commits into
ParadevAI:mainfrom
johrld:fix/cli-show-auth-url
Closed

johrld wants to merge 2 commits into
ParadevAI:mainfrom
johrld:fix/cli-show-auth-url

Conversation

@johrld

@johrld johrld commented May 1, 2026 •

Copy link
Copy Markdown

Summary

Closes #3949.

superset auth login hangs on headless Linux hosts because openBrowser() invokes xdg-open via fire-and-forget exec() — when xdg-open is missing the failure is silent and no URL is ever surfaced.

Fix

  • Add an optional onAuthUrl(url) callback to a new LoginOptions interface in lib/auth.ts.
  • Refactor login(signal) → login(options). Single internal caller updated.
  • In the auth command, the callback prints the URL via p.log.info(...). On TTY clack interleaves it with the running spinner; on non-TTY it's a clean info line.

Browser auto-launch is unchanged for GUI users — openBrowser() is still invoked after the URL is surfaced.

Test plan

  • On macOS (TTY): browser still opens; URL also appears as a p.log.info line, interleaves cleanly with the spinner.
  • On Linux ARM64 headless (Hetzner Neoverse-N1, no xdg-open): URL is now printed. Combined with ssh -L 51789:localhost:51789 <host> the OAuth loopback callback completes through the tunnel and CLI logs in successfully.
  • bun run lint:fix && bun run typecheck — couldn't run locally; relying on CI.

Scope

  • packages/cli/src/lib/auth.ts — new LoginOptions; login() signature refactor; URL hoisted to a const; onAuthUrl invoked before openBrowser.
  • packages/cli/src/commands/auth/login/command.ts — pass options including the callback.

Out of scope (potential follow-ups)

  • openBrowser() still doesn't actually await exec() completion. Surfacing the URL eagerly sidesteps that for users; making openBrowser truly synchronous is a separate change.
  • A --no-browser flag for purely scripted flows.

Summary by cubic

Print the OAuth authorization URL during superset auth login so headless Linux hosts can complete login when no default browser is available. Auto-launch still works; this adds a visible URL fallback.

  • Bug Fixes
    • Added LoginOptions with optional onAuthUrl(url) and refactored login(signal) to login({ signal, ... }).
    • The auth command logs the URL before launching the browser, allowing copy/paste on headless hosts.
    • If an onAuthUrl callback throws, close the loopback server to avoid port leaks and “All loopback ports in use” errors.

Written for commit e798930. Summary will update on new commits.

Summary by CodeRabbit

  • Improvements
    • Enhanced authentication flow: the app now exposes a handler to surface the authorization URL and will print a fallback message with the URL during login if needed, improving visibility and reliability when completing sign-in from the browser.

`superset auth login` opens the authorization URL via xdg-open (Linux),
open (macOS), or start (Windows). On headless Linux hosts — the typical
target of the CLI release — xdg-open is often missing, and exec()
silently swallows the failure. The user sees only "Waiting for browser
authorization..." with no URL anywhere; the login hangs forever.

Make the URL discoverable via a new `onAuthUrl` callback on `login()`.
The callback fires just before the browser launch, letting the command
layer render through clack — printing a fresh `p.log.info` with the URL
which works on TTY (interleaved with the spinner) and headless alike.
Browser auto-launch behaviour is unchanged for GUI users.

Verified: an SSH user can now `ssh -L 51789:localhost:51789 <host>`,
read the printed URL, paste it into a local browser, and complete the
OAuth loopback callback through the SSH tunnel.
@coderabbitai

coderabbitai Bot commented May 1, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

The login API now accepts a LoginOptions object (with signal and optional onAuthUrl) instead of a bare AbortSignal. login() calls onAuthUrl(url) immediately before opening the browser; the CLI command forwards the signal and logs the URL via onAuthUrl for headless visibility.

Changes

Auth Login Flow

Layer / File(s) Summary
Data Shape / Types
packages/cli/src/lib/auth.ts
Added exported LoginOptions interface with signal: AbortSignal and optional onAuthUrl(url: string).
Core Implementation
packages/cli/src/lib/auth.ts
Changed login signature to login(options: LoginOptions). Built auth URL into a local url var, invoke options.onAuthUrl?.(url) wrapped in try/catch (closing loopback server and rethrowing on callback error), and pass options.signal into callback-waiting logic.
Command Wiring
packages/cli/src/commands/auth/login/command.ts
Call login({ signal: opts.signal, onAuthUrl: url => p.log.info(fallback message with url) }) instead of passing opts.signal directly.
Behavioral Change (IO)
packages/cli/src/lib/auth.ts, packages/cli/src/commands/auth/login/command.ts
Browser launch unchanged; new onAuthUrl provides a surfaced URL for headless environments prior to opening the browser.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 I hopped through code to find the clue,
A URL printed where none once flew.
Now headless shells can see the link,
No more silent spinners to sink,
Hooray — auth blossoms, thanks to you! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: introducing a callback to surface the auth URL for headless logins, which directly addresses the core problem in the changeset.
Linked Issues check ✅ Passed All coding objectives from #3949 are met: the PR introduces LoginOptions with onAuthUrl callback, refactors login() signature, invokes the callback before openBrowser(), and enables headless users to access the authorization URL.
Out of Scope Changes check ✅ Passed All changes directly support the stated objective of surfacing auth URLs via callback; no unrelated modifications to unrelated systems or features are present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

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

@greptile-apps

greptile-apps Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR surfaces the OAuth authorization URL via a new optional onAuthUrl callback on the refactored LoginOptions interface, so headless Linux hosts without xdg-open can still complete the login flow by pasting the URL into a browser. The loopback server is bound before the callback fires, so the server is ready to receive the OAuth callback as soon as the user navigates to the URL. The change is minimal, focused, and backwards-compatible for any future callers that omit onAuthUrl.

Confidence Score: 5/5

Safe to merge — the change is a small, well-scoped addition with no logic regressions.

All findings are P2 or lower. The implementation is correct: the loopback server is bound before onAuthUrl fires, the callback is optional so no existing callers break, and the known openBrowser fire-and-forget issue is pre-existing and out of scope.

No files require special attention.

Important Files Changed

Filename Overview
packages/cli/src/lib/auth.ts Introduces LoginOptions interface, refactors login() to accept options object, and hoists the auth URL to a const so onAuthUrl can be called before openBrowser(). Logic is correct: the loopback server is already bound before the callback fires.
packages/cli/src/commands/auth/login/command.ts Updates the single internal login() call to use the new LoginOptions shape and adds an onAuthUrl callback that surfaces the URL via p.log.info for both TTY and non-TTY environments.

Sequence Diagram

sequenceDiagram
    participant CMD as command.ts
    participant AUTH as auth.ts (login)
    participant SERVER as Loopback Server
    participant BROWSER as openBrowser()
    participant USER as User/Terminal

    CMD->>AUTH: login({ signal, onAuthUrl })
    AUTH->>SERVER: bindLoopbackServer()
    SERVER-->>AUTH: { server, port }
    AUTH->>AUTH: build authorizeUrl
    AUTH->>CMD: onAuthUrl(url) callback
    CMD->>USER: p.log.info("If browser doesn't open…\n{url}")
    AUTH->>BROWSER: openBrowser(url)
    Note over BROWSER: fire-and-forget exec (xdg-open / open / start)
    AUTH->>SERVER: waitForCallback(...)
    USER-->>SERVER: paste URL → OAuth callback
    SERVER-->>AUTH: code
    AUTH->>AUTH: POST /api/cli/exchange
    AUTH-->>CMD: LoginResult
Loading

Reviews (1): Last reviewed commit: "fix(cli): surface auth URL so headless l..." | Re-trigger Greptile

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 2 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/cli/src/lib/auth.ts">

<violation number="1" location="packages/cli/src/lib/auth.ts:158">
P2: User-provided `onAuthUrl` callback can throw and leak the bound loopback server</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread packages/cli/src/lib/auth.ts Resolved
If a caller-supplied `onAuthUrl` callback throws, the loopback HTTP
server bound earlier by `bindLoopbackServer` would never be closed —
leaking the port and starving subsequent login attempts (loopback
candidates are 51789 + 51790; both being held would surface as
"All loopback ports in use").

Wrap the callback in try/catch and close the server before re-throwing.
The error still propagates so the caller knows their callback failed.

Reported by @cubic-dev-ai (P2) on ParadevAI#3950.
@johrld

johrld commented May 2, 2026

Copy link
Copy Markdown
Author

Thanks @cubic-dev-ai — fixed in e798930. The onAuthUrl callback is now wrapped in try/catch; on a thrown error the loopback server is closed before the error is re-thrown, so the loopback ports aren't leaked.

@cubic-dev-ai

cubic-dev-ai Bot commented May 2, 2026

Copy link
Copy Markdown
Contributor

Thanks @cubic-dev-ai — fixed in e798930. The onAuthUrl callback is now wrapped in try/catch; on a thrown error the loopback server is closed before the error is re-thrown, so the loopback ports aren't leaked.

@johrld The implementation in packages/cli/src/lib/auth.ts looks solid. Wrapping the callback ensures that even if the UI or logging layer fails, the CLI doesn't leave the loopback port dangling. Re-throwing the error is also the right approach here — it ensures the failure propagates so the user gets an error message instead of the CLI hanging or proceeding into a broken state.

This aligns well with the team's preference for explicit error handling and resource cleanup.

learnings | docs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/cli/src/lib/auth.ts (1)

134-142: ⚡ Quick win

onAuthUrl typed as void doesn't prevent async callers — the try/catch protection is incomplete

(url: string) => void looks safe, but TypeScript allows Promise<void> to satisfy void in a callback position without a type error. A caller can pass an async function without any compiler complaint. Because options.onAuthUrl?.(url) is not await-ed, an async rejection:

  1. Silently escapes the try/catch as an unhandled promise rejection.
  2. Bypasses server.close(), leaking the bound loopback port — the very scenario the commit message says this block is designed to prevent.

The fix is a one-liner: widen the return type and await the call.

🛡️ Proposed fix — support async callbacks and close the server on rejection
-	onAuthUrl?: (url: string) => void;
+	onAuthUrl?: (url: string) => void | Promise<void>;
-	try {
-		options.onAuthUrl?.(url);
-	} catch (err) {
-		// Don't leak the bound loopback server if the callback throws.
-		server.close();
-		throw err;
-	}
+	try {
+		await options.onAuthUrl?.(url);
+	} catch (err) {
+		// Don't leak the bound loopback server if the callback throws.
+		server.close();
+		throw err;
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/lib/auth.ts` around lines 134 - 142, The onAuthUrl callback
in LoginOptions can be async but is typed as (url: string) => void and is
invoked without await, so rejections can escape the try/catch and prevent
server.close(); update LoginOptions.onAuthUrl to accept a possibly async
callback (e.g. (url: string) => void | Promise<void>) and change the invocation
sites (where options.onAuthUrl?.(url) is called) to await the call inside the
existing try/catch so any rejection is caught and server.close() always runs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/cli/src/lib/auth.ts`:
- Around line 134-142: The onAuthUrl callback in LoginOptions can be async but
is typed as (url: string) => void and is invoked without await, so rejections
can escape the try/catch and prevent server.close(); update
LoginOptions.onAuthUrl to accept a possibly async callback (e.g. (url: string)
=> void | Promise<void>) and change the invocation sites (where
options.onAuthUrl?.(url) is called) to await the call inside the existing
try/catch so any rejection is caught and server.close() always runs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: eac460cc-6332-469f-b167-fef54010c4ea

📥 Commits

Reviewing files that changed from the base of the PR and between 81f1926 and e798930.

📒 Files selected for processing (1)
  • packages/cli/src/lib/auth.ts

@johrld

johrld commented May 2, 2026

Copy link
Copy Markdown
Author

Superseded by #3965 (cross-device login via OAuth code + PKCE, merged 2026-05-01 23:20 UTC). The new flow ships URL surfacing via the onAuthorizationUrl(url, mode) callback plus a paste-back fallback for SSH/CI/sandboxed environments — a strict superset of what this PR does. Closing.

@johrld johrld closed this May 2, 2026
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.

bug: superset auth login hangs silently on headless servers when xdg-open is missing

1 participant