Skip to content

feat: add browser-based OAuth login (vargai login)#212

Open
SecurityQQ wants to merge 1 commit into
mainfrom
feature/browser-login
Open

feat: add browser-based OAuth login (vargai login)#212
SecurityQQ wants to merge 1 commit into
mainfrom
feature/browser-login

Conversation

@SecurityQQ

Copy link
Copy Markdown
Contributor

Summary

  • Adds browser-based OAuth as the default login method for vargai login, reusing the MCP OAuth 2.1 server infrastructure at mcp.varg.ai
  • Users can now sign in via Google or email through the app consent page, instead of typing OTP codes in the terminal
  • Email OTP and API key paste remain as options 2 and 3 for headless/CI environments

Flow

  1. CLI starts a temporary HTTP server on http://127.0.0.1:<random-port>
  2. CLI registers as a dynamic client via POST mcp.varg.ai/oauth/register
  3. Browser opens to mcp.varg.ai/oauth/authorize with PKCE challenge
  4. User authenticates on app.varg.ai (Google OAuth or email OTP)
  5. User approves on the consent page ("varg CLI wants to access your account")
  6. Browser redirects to localhost callback with auth code
  7. CLI exchanges code for API key via PKCE token exchange
  8. API key saved to ~/.varg/credentials

New files

File Purpose
src/cli/oauth/pkce.ts PKCE code_verifier/challenge generation + state parameter
src/cli/oauth/oauth-callback-server.ts Temporary localhost HTTP server for OAuth callback, with success/error HTML pages

Modified files

File Change
src/cli/commands/login.tsx Added loginWithBrowser(), updated mode selector (browser=1, email=2, API key=3)

Companion PR

  • app: vargHQ/app — feature/generic-consent-page — generalizes consent page text for non-MCP clients

Adds a third login mode that opens the browser for authentication,
reusing the MCP OAuth 2.1 server infrastructure. Users can now sign
in via Google or email through the app consent page instead of
typing OTP codes in the terminal.

Flow: CLI starts localhost callback server -> opens browser to
mcp.varg.ai/oauth/authorize -> user authenticates on app.varg.ai ->
callback redirects to localhost with auth code -> CLI exchanges
code for API key via PKCE token exchange.

Browser login is now the default (option 1). Email OTP and API key
paste remain as options 2 and 3 for headless environments.
@coderabbitai

coderabbitai Bot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

walkthrough

adds oauth browser-based login with pkce to cli. introduces callback server for handling authorization code flow, pkce utilities for secure token exchange, and updates login command to support 3-option selection with browser oauth as default.

changes

cohort / file(s) summary
browser oauth login integration
src/cli/commands/login.tsx
swaps login menu from 2 to 3 options (browser/email/api-key), sets browser oauth as default flow, integrates callback server and pkce utilities for token exchange and validation.
oauth infrastructure
src/cli/oauth/oauth-callback-server.ts, src/cli/oauth/pkce.ts
adds callback server for handling authorization codes with state validation and error handling; adds pkce helpers for generating verifiers, challenges, and state values.

sequence diagram(s)

sequenceDiagram
    participant User
    participant CLI
    participant Localhost as Localhost<br/>Callback Server
    participant Browser
    participant OAuth as MCP OAuth<br/>Endpoints
    participant Gateway

    User->>CLI: run login (browser option)
    CLI->>CLI: generate pkce state & verifier
    CLI->>Localhost: start callback server
    Localhost-->>CLI: return port
    
    CLI->>Browser: open auth url<br/>(code_challenge, state, redirect_uri)
    Browser->>OAuth: authorization request
    OAuth-->>Browser: redirect to localhost callback
    Browser->>Localhost: GET /callback?code=X&state=Y
    Localhost->>Localhost: validate state
    Localhost-->>Browser: success page
    Localhost-->>CLI: resolve with code & state
    
    CLI->>OAuth: exchange code for token<br/>(code, verifier, client_id)
    OAuth-->>CLI: access_token
    
    CLI->>Gateway: validate token (check balance)
    Gateway-->>CLI: success
    CLI-->>User: logged in
Loading

estimated code review effort

🎯 4 (complex) | ⏱️ ~45 minutes

possibly related prs

poem

🔐 browser opens wide, localhost listens for the code,
pkce keeps it safe in a cryptographic abode,
state dances with verifier in oauth's ballet so fine,
login flows securely now—passwords bye-bye ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed the title clearly describes the main feature: adding browser-based oauth login for the vargai login command.
Description check ✅ Passed the description is directly related to the changeset, detailing the oauth flow, new files, and modifications involved.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/browser-login

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 and usage tips.

@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.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/cli/commands/login.tsx (1)

316-329: potential resource leak - server not closed on success path

on success (lines 310-315), the callback server isn't explicitly closed. the server does auto-shutdown after responding, but if the token exchange fails after receiving the callback, closeServer() isn't called in all paths.

actually wait - looking closer, the callback server shuts itself down after handling the request (via setTimeout(shutdown, 500) in oauth-callback-server.ts), so this should be fine. but you could still add a finally block for cleanliness.

🔧 optional: wrap in try-finally for clarity
   } catch (err) {
     process.stdout.write("\r\x1b[K");
-    closeServer();
 
     const message = err instanceof Error ? err.message : String(err);
     if (message.includes("timed out")) {
       log.error("Authentication timed out. Please try again.");
     } else if (message.includes("State mismatch")) {
       log.error("Security check failed. Please try again.");
     } else {
       log.error(`Authentication failed: ${message}`);
     }
     return null;
+  } finally {
+    closeServer();
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/commands/login.tsx` around lines 316 - 329, The callback server may
not be explicitly closed on all success/error paths; wrap the existing try/catch
in a try/finally (or add a finally block) so closeServer() is always called
regardless of outcome—ensure this change touches the same scope where
closeServer() is invoked (the login flow handling the OAuth callback, around the
code that currently catches errors and calls closeServer()), leaving existing
error handling (message checks for "timed out" and "State mismatch") intact;
this mirrors the auto-shutdown in oauth-callback-server.ts but guarantees
cleanup if token exchange fails after callback.
src/cli/oauth/oauth-callback-server.ts (1)

11-11: consider using bun.serve() per coding guidelines

coding guidelines suggest Bun.serve() over node:http. node:http works fine in bun, but bun.serve is more idiomatic if this is a bun-first project.

totally optional since node:http is compatible - just noting for consistency. meow 🐱

as per coding guidelines: "Use Bun.serve() with support for WebSockets, HTTPS, and routes instead of express"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/oauth/oauth-callback-server.ts` at line 11, Replace the node:http
createServer usage with Bun.serve: remove the import of createServer/Server from
"node:http", call Bun.serve(...) where createServer(...) is currently used
(e.g., in the function that starts the OAuth callback server), convert
request/response handling to the Bun Request/Response handler signature, and
update any type references from Server to appropriate Bun types or narrow to
unknown/void if no Bun types are available; ensure the same port, path handling,
and shutdown logic are preserved when migrating the existing createServer-based
handlers to Bun.serve.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/cli/oauth/oauth-callback-server.ts`:
- Around line 50-80: The ERROR_HTML template function inserts the unescaped msg
parameter directly into the response, allowing XSS via attacker-controlled
error_description; fix by HTML-escaping msg before interpolation (e.g., add an
escapeHtml(html: string) helper that replaces &, <, >, ", ' and ` with entities)
and use ERROR_HTML(escapeHtml(msg)) (or call escapeHtml inside ERROR_HTML) so
any oauth redirect error_description cannot inject raw HTML/script.
- Around line 160-164: The code extracts the port with server.address() and
silently falls back to 0, producing an invalid redirect URI; update the oauth
callback logic (where server.listen, server.address and the port variable are
used) to validate the address/port after listen and throw a clear error if port
is missing or 0 (e.g., if typeof address !== "object" || !address ||
!address.port) so the function fails fast instead of constructing an invalid
redirect URI.

---

Nitpick comments:
In `@src/cli/commands/login.tsx`:
- Around line 316-329: The callback server may not be explicitly closed on all
success/error paths; wrap the existing try/catch in a try/finally (or add a
finally block) so closeServer() is always called regardless of outcome—ensure
this change touches the same scope where closeServer() is invoked (the login
flow handling the OAuth callback, around the code that currently catches errors
and calls closeServer()), leaving existing error handling (message checks for
"timed out" and "State mismatch") intact; this mirrors the auto-shutdown in
oauth-callback-server.ts but guarantees cleanup if token exchange fails after
callback.

In `@src/cli/oauth/oauth-callback-server.ts`:
- Line 11: Replace the node:http createServer usage with Bun.serve: remove the
import of createServer/Server from "node:http", call Bun.serve(...) where
createServer(...) is currently used (e.g., in the function that starts the OAuth
callback server), convert request/response handling to the Bun Request/Response
handler signature, and update any type references from Server to appropriate Bun
types or narrow to unknown/void if no Bun types are available; ensure the same
port, path handling, and shutdown logic are preserved when migrating the
existing createServer-based handlers to Bun.serve.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b4f387bb-191a-4df3-a5d9-6d0bf434e8b1

📥 Commits

Reviewing files that changed from the base of the PR and between a147085 and ee5d245.

📒 Files selected for processing (3)
  • src/cli/commands/login.tsx
  • src/cli/oauth/oauth-callback-server.ts
  • src/cli/oauth/pkce.ts

Comment on lines +50 to +80
const ERROR_HTML = (msg: string) => `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>varg.ai — Authentication Failed</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #0a0a0a; color: #fafafa;
display: flex; align-items: center; justify-content: center;
min-height: 100vh;
}
.card {
text-align: center; padding: 3rem 2rem;
border: 1px solid #262626; border-radius: 12px;
background: #141414; max-width: 400px;
}
.icon { font-size: 48px; margin-bottom: 1rem; }
h1 { font-size: 1.25rem; margin-bottom: 0.5rem; }
p { color: #a3a3a3; font-size: 0.875rem; }
</style>
</head>
<body>
<div class="card">
<div class="icon">&#10007;</div>
<h1>Authentication Failed</h1>
<p>${msg}</p>
</div>
</body>
</html>`;

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.

⚠️ Potential issue | 🟠 Major

xss vulnerability - error message not escaped

the msg parameter is injected directly into html without escaping. since error_description comes from the oauth redirect url (attacker-controlled), a malicious auth server or mitm could inject script tags.

🛡️ fix: escape html entities
+function escapeHtml(str: string): string {
+  return str
+    .replace(/&/g, "&amp;")
+    .replace(/</g, "&lt;")
+    .replace(/>/g, "&gt;")
+    .replace(/"/g, "&quot;")
+    .replace(/'/g, "&#039;");
+}
+
 const ERROR_HTML = (msg: string) => `<!DOCTYPE html>
 <html>
 ...
     <h1>Authentication Failed</h1>
-    <p>${msg}</p>
+    <p>${escapeHtml(msg)}</p>
   </div>
 </body>
 </html>`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/oauth/oauth-callback-server.ts` around lines 50 - 80, The ERROR_HTML
template function inserts the unescaped msg parameter directly into the
response, allowing XSS via attacker-controlled error_description; fix by
HTML-escaping msg before interpolation (e.g., add an escapeHtml(html: string)
helper that replaces &, <, >, ", ' and ` with entities) and use
ERROR_HTML(escapeHtml(msg)) (or call escapeHtml inside ERROR_HTML) so any oauth
redirect error_description cannot inject raw HTML/script.

Comment on lines +160 to +164
// Listen on a random available port on loopback
server.listen(0, "127.0.0.1");

const address = server.address();
const port = typeof address === "object" && address ? address.port : 0;

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.

⚠️ Potential issue | 🟡 Minor

edge case: port extraction could fail silently

if server.address() returns null (shouldn't happen after successful listen, but edge case), port becomes 0 and the redirect uri would be invalid. might want to throw early if port is 0.

🔧 suggested fix
   const address = server.address();
   const port = typeof address === "object" && address ? address.port : 0;
+  if (port === 0) {
+    throw new Error("Failed to start callback server: could not get port");
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/oauth/oauth-callback-server.ts` around lines 160 - 164, The code
extracts the port with server.address() and silently falls back to 0, producing
an invalid redirect URI; update the oauth callback logic (where server.listen,
server.address and the port variable are used) to validate the address/port
after listen and throw a clear error if port is missing or 0 (e.g., if typeof
address !== "object" || !address || !address.port) so the function fails fast
instead of constructing an invalid redirect URI.

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