Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/fixes/8845-oauth-web-client-type.md
Original file line number Diff line number Diff line change
@@ -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
33 changes: 28 additions & 5 deletions src/lib/oauth/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}
Expand Down
109 changes: 109 additions & 0 deletions tests/unit/oauth-redirect-uri-mismatch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
);
});
Loading