fix: honor public callbacks for remote Google OAuth

This commit is contained in:
akarray
2026-05-27 13:30:08 +02:00
parent a88d7d55a8
commit 6cfdf78900
4 changed files with 108 additions and 5 deletions

View File

@@ -6,6 +6,7 @@ import {
exchangeTokens,
requestDeviceCode,
pollForToken,
resolveBrowserOAuthRedirectUri,
} from "@/lib/oauth/providers";
import {
createProviderConnection,
@@ -80,7 +81,9 @@ export async function GET(
const { searchParams } = new URL(request.url);
if (action === "authorize") {
const redirectUri = searchParams.get("redirect_uri") || "http://localhost:8080/callback";
const requestedRedirectUri =
searchParams.get("redirect_uri") || "http://localhost:8080/callback";
const redirectUri = resolveBrowserOAuthRedirectUri(provider, requestedRedirectUri);
const authData = generateAuthData(provider, redirectUri);
if (provider === "qoder" && !authData.authUrl) {
return NextResponse.json({

View File

@@ -10,6 +10,66 @@
import { generatePKCE, generateState } from "./utils/pkce";
import { PROVIDERS } from "./providers/index";
const GOOGLE_BROWSER_PROVIDERS = new Set(["antigravity", "gemini-cli"]);
function normalizeBaseUrl(value) {
const trimmed = typeof value === "string" ? value.trim() : "";
if (!trimmed) return "";
return trimmed.replace(/\/+$/, "");
}
function hasCustomGoogleOAuthCredentials(providerName, env = process.env) {
if (providerName === "antigravity") {
return !!env.ANTIGRAVITY_OAUTH_CLIENT_ID?.trim();
}
if (providerName === "gemini-cli") {
return !!env.GEMINI_CLI_OAUTH_CLIENT_ID?.trim() || !!env.GEMINI_OAUTH_CLIENT_ID?.trim();
}
return false;
}
/**
* Google providers default to localhost redirects so the embedded public
* credentials keep working on out-of-the-box local installs. When operators
* provide their own Google OAuth client IDs for a remote deployment, prefer the
* public callback URL documented in .env.example / docs/README so the popup can
* navigate back to OmniRoute instead of stalling on localhost.
*/
export function resolveBrowserOAuthRedirectUri(
providerName,
redirectUri,
env = process.env
) {
if (!GOOGLE_BROWSER_PROVIDERS.has(providerName)) {
return redirectUri;
}
if (!hasCustomGoogleOAuthCredentials(providerName, env)) {
return redirectUri;
}
const publicBaseUrl =
normalizeBaseUrl(env.NEXT_PUBLIC_BASE_URL) || normalizeBaseUrl(env.OMNIROUTE_PUBLIC_BASE_URL);
if (!publicBaseUrl) {
return redirectUri;
}
try {
const requested = new URL(redirectUri);
const isLocalhostRedirect = /^(localhost|127\.0\.0\.1)$/i.test(requested.hostname);
if (!isLocalhostRedirect) {
return redirectUri;
}
} catch {
return redirectUri;
}
return `${publicBaseUrl}/callback`;
}
/**
* Get provider handler
*/

View File

@@ -381,9 +381,9 @@ export default function OAuthModal({
// - 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, gemini-cli): always localhost, regardless of
// where OmniRoute is hosted — Google only accepts pre-registered localhost URIs with
// the built-in credentials. Remote users must configure their own credentials.
// - Google OAuth providers (antigravity, gemini-cli): default to localhost so the
// bundled credentials keep working. The authorize route upgrades this to the public
// callback when custom Google credentials + NEXT_PUBLIC_BASE_URL are configured.
// - Other providers on remote: use actual origin (supports PUBLIC_URL env var)
// - Localhost: use localhost:port
let redirectUri: string;
@@ -433,7 +433,7 @@ export default function OAuthModal({
);
}
setAuthData({ ...data, redirectUri });
setAuthData({ ...data, redirectUri: data.redirectUri || redirectUri });
// For non-true-localhost (LAN IPs, remote) or manual fallback: use manual input mode (user pastes callback URL)
if (!isTrueLocalhost || forceManual) {

View File

@@ -18,8 +18,10 @@ const providersModule = await import("../../src/lib/oauth/providers/index.ts");
const oauthModule = await import("../../src/lib/oauth/constants/oauth.ts");
const registryModule = await import("../../open-sse/config/providerRegistry.ts");
const antigravityHeadersModule = await import("../../open-sse/services/antigravityHeaders.ts");
const oauthHelpersModule = await import("../../src/lib/oauth/providers.ts");
const PROVIDERS = providersModule.default;
const { resolveBrowserOAuthRedirectUri } = oauthHelpersModule;
const {
ANTIGRAVITY_CONFIG,
CLAUDE_CONFIG,
@@ -316,6 +318,44 @@ test("browser-based providers expose buildAuthUrl and return provider-specific a
assert.equal(clineUrl.origin, "https://api.cline.bot");
});
test("custom Google OAuth credentials switch Antigravity remote callbacks to NEXT_PUBLIC_BASE_URL", () => {
const redirectUri = resolveBrowserOAuthRedirectUri(
"antigravity",
"http://localhost:20128/callback",
{
NEXT_PUBLIC_BASE_URL: "https://omniroute.example.com/",
ANTIGRAVITY_OAUTH_CLIENT_ID: "custom-antigravity.apps.googleusercontent.com",
}
);
assert.equal(redirectUri, "https://omniroute.example.com/callback");
});
test("custom Google OAuth credentials switch Gemini remote callbacks to OMNIROUTE_PUBLIC_BASE_URL", () => {
const redirectUri = resolveBrowserOAuthRedirectUri(
"gemini-cli",
"http://127.0.0.1:20128/callback",
{
OMNIROUTE_PUBLIC_BASE_URL: "https://omniroute.example.com",
GEMINI_CLI_OAUTH_CLIENT_ID: "custom-gemini.apps.googleusercontent.com",
}
);
assert.equal(redirectUri, "https://omniroute.example.com/callback");
});
test("Google OAuth callbacks stay on localhost when no custom credentials are configured", () => {
const redirectUri = resolveBrowserOAuthRedirectUri(
"antigravity",
"http://localhost:20128/callback",
{
NEXT_PUBLIC_BASE_URL: "https://omniroute.example.com",
}
);
assert.equal(redirectUri, "http://localhost:20128/callback");
});
test("device and import-token providers expose the flow-specific fields expected by their configs", () => {
const deviceProviders = ["qwen", "kimi-coding", "github", "kiro", "amazon-q", "kilocode"];