mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
fix(oauth): warn instead of silently opening unreachable localhost redirect for LAN-IP Codex/xAI/Grok OAuth (#8046) (#8152)
This commit is contained in:
committed by
GitHub
parent
6302a78657
commit
b954a3a60f
1
changelog.d/fixes/8046-codex-oauth-lan-ip.md
Normal file
1
changelog.d/fixes/8046-codex-oauth-lan-ip.md
Normal 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)
|
||||
40
src/lib/oauth/utils/pkceLoopbackWarning.ts
Normal file
40
src/lib/oauth/utils/pkceLoopbackWarning.ts
Normal 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."
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
61
tests/unit/oauth-modal-codex-lan-ip-8046.test.ts
Normal file
61
tests/unit/oauth-modal-codex-lan-ip-8046.test.ts
Normal 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/);
|
||||
});
|
||||
Reference in New Issue
Block a user