From 849558093896122abeb56126cf6549d5a9496ded Mon Sep 17 00:00:00 2001 From: Minxi Hou Date: Tue, 28 Jul 2026 05:50:47 -0400 Subject: [PATCH 1/2] feat(oauth): support web client type for remote Google OAuth Google Desktop app OAuth clients require loopback redirect URIs per policy, which breaks remote deployments where the browser cannot reach 127.0.0.1 on the server. Add ANTIGRAVITY_OAUTH_CLIENT_TYPE env var: when set to 'web' and OMNIROUTE_PUBLIC_BASE_URL is configured, the loopback redirect URI is upgraded to the public base URL. Default behavior (desktop) is unchanged. Enables remote deployments without SSH tunneling by registering a Web application OAuth client in Google Cloud Console. Signed-off-by: Minxi Hou --- src/lib/oauth/providers.ts | 33 +++++- .../unit/oauth-redirect-uri-mismatch.test.ts | 109 ++++++++++++++++++ 2 files changed, 137 insertions(+), 5 deletions(-) diff --git a/src/lib/oauth/providers.ts b/src/lib/oauth/providers.ts index 3a0bb4a62745..a369ec154f39 100644 --- a/src/lib/oauth/providers.ts +++ b/src/lib/oauth/providers.ts @@ -47,6 +47,20 @@ function isLoopbackHostname(hostname: string): boolean { return /^(localhost|127\.0\.0\.1|\[::1\]|::1)$/i.test(hostname); } +function upgradeLoopbackToPublic(redirectUri: string, publicBaseUrl: string): string { + try { + const requested = new URL(redirectUri); + if (!isLoopbackHostname(requested.hostname)) { + return redirectUri; + } + const callbackPath = + requested.pathname && requested.pathname !== "/" ? requested.pathname : "/callback"; + return `${publicBaseUrl}${callbackPath}${requested.search}`; + } catch { + return redirectUri; + } +} + /** * Google providers default to loopback redirects so the embedded public * credentials keep working on out-of-the-box local installs. When operators @@ -68,21 +82,30 @@ export function resolveBrowserOAuthRedirectUri( } const publicBaseUrl = - normalizeBaseUrl(env.NEXT_PUBLIC_BASE_URL) || normalizeBaseUrl(env.OMNIROUTE_PUBLIC_BASE_URL); + normalizeBaseUrl(env?.NEXT_PUBLIC_BASE_URL) || normalizeBaseUrl(env?.OMNIROUTE_PUBLIC_BASE_URL); if (!publicBaseUrl) { return redirectUri; } + // Web application OAuth client type allows non-loopback redirect URIs. + // When the operator sets ANTIGRAVITY_OAUTH_CLIENT_TYPE=web with custom + // credentials, upgrade the loopback redirect so remote deployments work + // without SSH tunneling. Non-web client types fall through to the + // existing custom-credentials upgrade path below. + if (GOOGLE_BROWSER_PROVIDERS.has(providerName)) { + const clientType = (env?.ANTIGRAVITY_OAUTH_CLIENT_TYPE || "").toLowerCase().trim(); + if (clientType === "web") { + return upgradeLoopbackToPublic(redirectUri, publicBaseUrl); + } + } + try { const requested = new URL(redirectUri); if (!isLoopbackHostname(requested.hostname)) { return redirectUri; } - - const callbackPath = - requested.pathname && requested.pathname !== "/" ? requested.pathname : "/callback"; - return `${publicBaseUrl}${callbackPath}${requested.search}`; + return upgradeLoopbackToPublic(redirectUri, publicBaseUrl); } catch { return redirectUri; } diff --git a/tests/unit/oauth-redirect-uri-mismatch.test.ts b/tests/unit/oauth-redirect-uri-mismatch.test.ts index daa540860c77..27f034265a83 100644 --- a/tests/unit/oauth-redirect-uri-mismatch.test.ts +++ b/tests/unit/oauth-redirect-uri-mismatch.test.ts @@ -305,3 +305,112 @@ test("OMNIROUTE_PUBLIC_BASE_URL is used as fallback when NEXT_PUBLIC_BASE_URL is assert.equal(redirectUri, "https://fallback.example.com/callback"); }); + +// --------------------------------------------------------------------------- +// Web client type (ANTIGRAVITY_OAUTH_CLIENT_TYPE=web) - public redirect +// --------------------------------------------------------------------------- + +test("antigravity with client type 'web' and custom credentials switches loopback to public URL", () => { + const redirectUri = resolveBrowserOAuthRedirectUri( + "antigravity", + "http://127.0.0.1:20128/callback", + { + OMNIROUTE_PUBLIC_BASE_URL: "https://192.168.100.10:20128", + ANTIGRAVITY_OAUTH_CLIENT_TYPE: "web", + ANTIGRAVITY_OAUTH_CLIENT_ID: "custom-id.apps.googleusercontent.com", + ANTIGRAVITY_OAUTH_CLIENT_SECRET: "custom-secret", + } + ); + + assert.equal( + redirectUri, + "https://192.168.100.10:20128/callback", + "web client type must use public base URL" + ); +}); + +test("agy with client type 'web' and custom credentials switches loopback to public URL", () => { + const redirectUri = resolveBrowserOAuthRedirectUri("agy", "http://127.0.0.1:20128/callback", { + OMNIROUTE_PUBLIC_BASE_URL: "https://omniroute.example.com", + ANTIGRAVITY_OAUTH_CLIENT_TYPE: "web", + ANTIGRAVITY_OAUTH_CLIENT_ID: "custom-id.apps.googleusercontent.com", + ANTIGRAVITY_OAUTH_CLIENT_SECRET: "custom-secret", + }); + + assert.equal( + redirectUri, + "https://omniroute.example.com/callback", + "agy must inherit web client type from antigravity" + ); +}); + +test("client type 'web' without custom credentials keeps loopback", () => { + const redirectUri = resolveBrowserOAuthRedirectUri( + "antigravity", + "http://127.0.0.1:20128/callback", + { + OMNIROUTE_PUBLIC_BASE_URL: "https://omniroute.example.com", + ANTIGRAVITY_OAUTH_CLIENT_TYPE: "web", + ANTIGRAVITY_OAUTH_CLIENT_ID: DEFAULT_ANTIGRAVITY_CLIENT_ID, + ANTIGRAVITY_OAUTH_CLIENT_SECRET: "GOCSPX-SomeDefaultSecret", + } + ); + + assert.equal( + redirectUri, + "http://127.0.0.1:20128/callback", + "web client type with default credentials must keep loopback" + ); +}); + +test("client type 'desktop' (default) keeps loopback even with public base URL", () => { + const redirectUri = resolveBrowserOAuthRedirectUri( + "antigravity", + "http://127.0.0.1:20128/callback", + { + OMNIROUTE_PUBLIC_BASE_URL: "https://omniroute.example.com", + ANTIGRAVITY_OAUTH_CLIENT_TYPE: "desktop", + } + ); + + assert.equal( + redirectUri, + "http://127.0.0.1:20128/callback", + "desktop client type must keep loopback" + ); +}); + +test("no client type set defaults to desktop (keeps loopback)", () => { + const redirectUri = resolveBrowserOAuthRedirectUri( + "antigravity", + "http://127.0.0.1:20128/callback", + { + OMNIROUTE_PUBLIC_BASE_URL: "https://omniroute.example.com", + } + ); + + assert.equal( + redirectUri, + "http://127.0.0.1:20128/callback", + "missing client type must default to desktop" + ); +}); + +test("client type 'web' preserves custom callback path", () => { + const redirectUri = resolveBrowserOAuthRedirectUri( + "antigravity", + "http://127.0.0.1:20128/custom-callback", + { + OMNIROUTE_PUBLIC_BASE_URL: "https://omniroute.example.com", + ANTIGRAVITY_OAUTH_CLIENT_TYPE: "web", + ANTIGRAVITY_OAUTH_CLIENT_ID: "custom-id.apps.googleusercontent.com", + ANTIGRAVITY_OAUTH_CLIENT_SECRET: "custom-secret", + } + ); + + assert.equal( + redirectUri, + "https://omniroute.example.com/custom-callback", + "custom callback path must be preserved" + ); +}); From 3cdfcfa331d3ea08502e7fcb3c91f1d00323adda Mon Sep 17 00:00:00 2001 From: Minxi Hou Date: Tue, 28 Jul 2026 08:58:27 -0300 Subject: [PATCH 2/2] docs(changelog): add fragment for #8845 Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --- changelog.d/fixes/8845-oauth-web-client-type.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/fixes/8845-oauth-web-client-type.md diff --git a/changelog.d/fixes/8845-oauth-web-client-type.md b/changelog.d/fixes/8845-oauth-web-client-type.md new file mode 100644 index 000000000000..a7e6f77536a5 --- /dev/null +++ b/changelog.d/fixes/8845-oauth-web-client-type.md @@ -0,0 +1 @@ +- **OAuth**: `ANTIGRAVITY_OAUTH_CLIENT_TYPE=web` lets a remote deployment use a Google Web application OAuth client, upgrading the loopback redirect to `OMNIROUTE_PUBLIC_BASE_URL` instead of leaving the operator to hand-edit the callback URL. Opt-in and requires custom credentials — unset or `desktop` keeps the existing loopback behaviour untouched