fix(oauth): warn instead of silently opening unreachable localhost redirect for LAN-IP Codex/xAI/Grok OAuth (#8046) (#8152)

This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-22 11:27:53 -03:00
committed by GitHub
parent 6302a78657
commit b954a3a60f
4 changed files with 115 additions and 13 deletions

View File

@@ -0,0 +1 @@
- fix(oauth): warn instead of silently opening an unreachable localhost:1455 redirect for Codex/xAI/Grok CLI OAuth when the dashboard is reached from a LAN IP (#8046)

View File

@@ -0,0 +1,40 @@
/**
* #8046: PKCE_CALLBACK_SERVER_PROVIDERS (codex/xai-oauth/grok-cli) register a FIXED
* loopback redirect_uri (e.g. http://localhost:1455/auth/callback for codex) with the
* upstream OAuth app. That redirect only resolves on the machine actually running the
* browser, not on whatever host serves the OmniRoute dashboard.
*
* OAuthModal's `isTrueLocalhost` check (hostname === "localhost" || "127.0.0.1") only
* covers one such case. A dashboard reached via a LAN IP (192.168.*, 10.*, 172.16-31.*)
* is `isLocalhost: true, isTrueLocalhost: false` — the callback-server branch was falling
* straight through to the standard authorize flow and window.open()ing an authUrl whose
* embedded redirect_uri can never resolve, with zero warning (reported as a silent
* Auth0 `invalid_state` failure with no server-side log line).
*
* Extracted out of OAuthModal.tsx (frozen file-size baseline) so the guard has an
* isolated, unit-testable home. Mirrors the messaging pattern of the sibling
* remote-origin hint added for #7523 (`remoteOAuthHint.ts::buildRemoteOAuthHint`).
*/
const PKCE_LOOPBACK_REDIRECT_HINT: Record<string, string> = {
codex: "http://localhost:1455/auth/callback",
"xai-oauth": "http://127.0.0.1:56121/callback",
"grok-cli": "http://127.0.0.1:56122/callback",
};
/**
* Build the warning shown instead of silently opening the provider's authorize URL
* when the dashboard is reached from a LAN IP (isLocalhost but not isTrueLocalhost)
* for a PKCE_CALLBACK_SERVER_PROVIDERS provider.
*/
export function buildPkceLoopbackMismatchWarning(provider: string): string {
const redirect = PKCE_LOOPBACK_REDIRECT_HINT[provider] ?? "a fixed localhost callback URL";
return (
`OmniRoute is being accessed from a LAN IP, not true localhost. ${provider}'s OAuth app ` +
`is registered with a fixed loopback redirect (${redirect}) that only resolves on the ` +
"machine running this browser tab, not on the OmniRoute server — the login will silently " +
"fail on the provider's side. Open the OmniRoute dashboard from true localhost instead " +
"(SSH port-forward: ssh -L <port>:127.0.0.1:<port> <user>@<omniroute-host>, then browse to " +
"http://localhost:<port>), or use the token-import flow for this provider if available."
);
}

View File

@@ -15,6 +15,7 @@ import {
} from "@/lib/oauth/utils/codexSessionImport";
import GheConfigStep from "@/shared/components/oauthModal/GheConfigStep";
import { parseGrokCliPasteToken } from "@/lib/oauth/utils/grokCliAuthJson";
import { buildPkceLoopbackMismatchWarning } from "@/lib/oauth/utils/pkceLoopbackWarning";
export { formatDeviceCodeRemaining } from "./OAuthModalPanels";
@@ -451,10 +452,8 @@ export default function OAuthModal({
forceManual = true;
}
// PKCE callback server providers (Codex, Windsurf, Devin CLI):
// On localhost, spin up a local callback server and poll for the result.
// Codex uses a fixed port 1455; Windsurf/Devin CLI use a random OS-assigned port.
// On remote the server is unreachable — fall through to standard manual flow.
// PKCE callback server providers (Codex, Windsurf, Devin CLI): true localhost spins
// up a callback server + polls; a LAN IP warns (#8046); remote falls through below.
if (PKCE_CALLBACK_SERVER_PROVIDERS.has(provider)) {
if (isTrueLocalhost) {
try {
@@ -508,21 +507,22 @@ export default function OAuthModal({
setPolling(false);
forceManual = true;
}
} else if (isLocalhost) {
setError(buildPkceLoopbackMismatchWarning(provider));
setStep("error");
return;
}
// Remote: fall through to standard auth code flow below
// Remote (non-LAN): fall through to standard auth code flow below
}
// Authorization code flow
// Redirect URI strategy:
// - Codex/OpenAI: always port 1455 (registered in OAuth app)
// - Windsurf/Devin CLI (remote fallback): use localhost with OmniRoute port + /auth/callback
// (on true localhost the callback server handles it; this is only reached on remote)
// - Google OAuth providers (antigravity/agy): default to loopback so the
// bundled native/desktop credentials keep working. Prefer 127.0.0.1 over
// localhost for the Google native-app handoff; Google documents that localhost
// can run into local firewall/name-resolution edge cases. The authorize route
// upgrades this to the public callback when custom Google web credentials plus
// NEXT_PUBLIC_BASE_URL or OMNIROUTE_PUBLIC_BASE_URL are configured.
// - Windsurf/Devin CLI (remote fallback; true localhost handled above): localhost:port
// - Google OAuth providers (antigravity/agy): default to loopback (127.0.0.1 preferred —
// Google docs flag localhost firewall/name-resolution edge cases) so bundled
// native/desktop credentials keep working; the authorize route upgrades this to the
// public callback when custom Google web credentials + a public base URL are configured.
// - Other providers on remote: use actual origin (supports PUBLIC_URL env var)
// - Localhost: use localhost:port
let redirectUri: string;

View File

@@ -0,0 +1,61 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
import { buildPkceLoopbackMismatchWarning } from "../../src/lib/oauth/utils/pkceLoopbackWarning";
// #8046: Codex (and the other PKCE_CALLBACK_SERVER_PROVIDERS: xai-oauth, grok-cli)
// register a FIXED loopback redirect_uri with the upstream OAuth app. On a LAN-IP
// origin (isLocalhost true, isTrueLocalhost false — e.g. 192.168.*/10.*/172.16-31.*),
// OAuthModal used to fall straight through to the standard authorize flow and
// window.open() an authUrl whose embedded redirect_uri can never resolve back to the
// dashboard, with zero warning — surfacing as a silent Auth0 `invalid_state` failure.
//
// Source-level guard (like the sibling oauth-modal-grok-cli-browser-login-7013.test.ts):
// OAuthModal is a "use client" component with heavy runtime deps (next-intl,
// popup/fetch orchestration); pinning the fix by source inspection + a direct unit
// test of the extracted warning-message helper is the reliable check here.
const here = dirname(fileURLToPath(import.meta.url));
const modal = readFileSync(resolve(here, "../../src/shared/components/OAuthModal.tsx"), "utf8");
function extractSet(constName: string): string[] {
const match = modal.match(new RegExp(`const ${constName} = new Set\\(\\[([^\\]]*)\\]\\)`));
assert.ok(match, `expected to find ${constName} in OAuthModal.tsx`);
return match![1]
.split(",")
.map((s) => s.trim().replace(/^"|"$/g, ""))
.filter(Boolean);
}
test("PKCE_CALLBACK_SERVER_PROVIDERS still includes codex (anchor)", () => {
assert.ok(extractSet("PKCE_CALLBACK_SERVER_PROVIDERS").includes("codex"));
});
test("codex/openai redirectUri is still hardcoded to localhost:1455 (anchor, unchanged)", () => {
assert.match(modal, /redirectUri = "http:\/\/localhost:1455\/auth\/callback"/);
});
test("PKCE callback-server providers now warn (not window.open) on a LAN-IP origin (#8046 fix)", () => {
// The callback-server branch must gain an isLocalhost-but-not-isTrueLocalhost arm
// that surfaces the loopback-mismatch warning instead of falling through silently.
assert.match(
modal,
/else if \(isLocalhost\) \{[\s\S]{0,200}buildPkceLoopbackMismatchWarning/,
"OAuthModal.tsx should warn via buildPkceLoopbackMismatchWarning() for isLocalhost && !isTrueLocalhost " +
"inside the PKCE_CALLBACK_SERVER_PROVIDERS branch, instead of silently falling through to " +
"window.open() an authUrl with an unreachable hardcoded loopback redirect_uri."
);
});
test("buildPkceLoopbackMismatchWarning mentions the fixed redirect and a way forward", () => {
const msg = buildPkceLoopbackMismatchWarning("codex");
assert.match(msg, /localhost:1455/);
assert.match(msg, /LAN IP/i);
assert.match(msg, /localhost/i);
});
test("buildPkceLoopbackMismatchWarning has a generic fallback for unknown providers", () => {
const msg = buildPkceLoopbackMismatchWarning("some-future-pkce-provider");
assert.match(msg, /fixed localhost callback URL/);
});