mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-22 06:42:19 +03:00
* 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 <houminxi@gmail.com> * docs(changelog): add fragment for #8845 Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Signed-off-by: Minxi Hou <houminxi@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
417 lines
14 KiB
TypeScript
417 lines
14 KiB
TypeScript
/**
|
|
* Tests for the Google OAuth redirect URI mismatch fix.
|
|
*
|
|
* Validates that:
|
|
* 1. Built-in (default) public credentials are correctly identified as non-custom
|
|
* so that loopback redirect URIs are preserved.
|
|
* 2. Truly custom credentials trigger the public base URL override.
|
|
* 3. resolvePublicCred() is used dynamically for default ID comparison.
|
|
* 4. The `agy` provider alias inherits antigravity credential detection.
|
|
*
|
|
* All tests run fully offline — no network calls.
|
|
*/
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
const { resolveBrowserOAuthRedirectUri } = await import("../../src/lib/oauth/providers.ts");
|
|
const { resolvePublicCred } = await import("../../open-sse/utils/publicCreds.ts");
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/** The default embedded antigravity client ID (decoded at runtime). */
|
|
const DEFAULT_ANTIGRAVITY_CLIENT_ID = resolvePublicCred("antigravity_id");
|
|
// ---------------------------------------------------------------------------
|
|
// resolvePublicCred sanity
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test("resolvePublicCred returns a valid Google client ID for antigravity_id", () => {
|
|
assert.ok(DEFAULT_ANTIGRAVITY_CLIENT_ID.length > 0, "must not be empty");
|
|
assert.ok(
|
|
DEFAULT_ANTIGRAVITY_CLIENT_ID.endsWith(".apps.googleusercontent.com"),
|
|
"must be a Google OAuth client ID"
|
|
);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Default (built-in) credentials → loopback preserved
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test("antigravity with default public credentials keeps loopback redirect URI", () => {
|
|
const redirectUri = resolveBrowserOAuthRedirectUri(
|
|
"antigravity",
|
|
"http://127.0.0.1:20128/callback",
|
|
{
|
|
NEXT_PUBLIC_BASE_URL: "https://omniroute.example.com",
|
|
ANTIGRAVITY_OAUTH_CLIENT_ID: DEFAULT_ANTIGRAVITY_CLIENT_ID,
|
|
ANTIGRAVITY_OAUTH_CLIENT_SECRET: "GOCSPX-SomeDefaultSecret",
|
|
}
|
|
);
|
|
|
|
assert.equal(
|
|
redirectUri,
|
|
"http://127.0.0.1:20128/callback",
|
|
"must stay on loopback when using built-in credentials"
|
|
);
|
|
});
|
|
|
|
test("agy provider with default antigravity credentials keeps loopback redirect URI", () => {
|
|
const redirectUri = resolveBrowserOAuthRedirectUri("agy", "http://localhost:20128/callback", {
|
|
NEXT_PUBLIC_BASE_URL: "https://omniroute.example.com",
|
|
ANTIGRAVITY_OAUTH_CLIENT_ID: DEFAULT_ANTIGRAVITY_CLIENT_ID,
|
|
ANTIGRAVITY_OAUTH_CLIENT_SECRET: "GOCSPX-SomeDefaultSecret",
|
|
});
|
|
|
|
assert.equal(
|
|
redirectUri,
|
|
"http://localhost:20128/callback",
|
|
"agy must inherit antigravity default credential detection"
|
|
);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Custom credentials → redirect overridden to public base URL
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test("antigravity with custom credentials switches loopback to public base URL", () => {
|
|
const redirectUri = resolveBrowserOAuthRedirectUri(
|
|
"antigravity",
|
|
"http://127.0.0.1:20128/callback",
|
|
{
|
|
NEXT_PUBLIC_BASE_URL: "https://omniroute.example.com",
|
|
ANTIGRAVITY_OAUTH_CLIENT_ID: "custom-id.apps.googleusercontent.com",
|
|
ANTIGRAVITY_OAUTH_CLIENT_SECRET: "custom-secret",
|
|
}
|
|
);
|
|
|
|
assert.equal(redirectUri, "https://omniroute.example.com/callback");
|
|
});
|
|
|
|
test("agy with custom credentials switches loopback to public base URL", () => {
|
|
const redirectUri = resolveBrowserOAuthRedirectUri("agy", "http://localhost:20128/callback", {
|
|
NEXT_PUBLIC_BASE_URL: "https://omniroute.example.com",
|
|
ANTIGRAVITY_OAUTH_CLIENT_ID: "custom-agy.apps.googleusercontent.com",
|
|
ANTIGRAVITY_OAUTH_CLIENT_SECRET: "custom-agy-secret",
|
|
});
|
|
|
|
assert.equal(redirectUri, "https://omniroute.example.com/callback");
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Edge cases — incomplete / missing credentials
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test("antigravity with only client ID (no secret) keeps loopback", () => {
|
|
const redirectUri = resolveBrowserOAuthRedirectUri(
|
|
"antigravity",
|
|
"http://127.0.0.1:20128/callback",
|
|
{
|
|
NEXT_PUBLIC_BASE_URL: "https://omniroute.example.com",
|
|
ANTIGRAVITY_OAUTH_CLIENT_ID: "custom-id.apps.googleusercontent.com",
|
|
// No secret
|
|
}
|
|
);
|
|
|
|
assert.equal(
|
|
redirectUri,
|
|
"http://127.0.0.1:20128/callback",
|
|
"incomplete credentials must not trigger override"
|
|
);
|
|
});
|
|
|
|
test("antigravity with blank/whitespace client ID keeps loopback", () => {
|
|
const redirectUri = resolveBrowserOAuthRedirectUri(
|
|
"antigravity",
|
|
"http://127.0.0.1:20128/callback",
|
|
{
|
|
NEXT_PUBLIC_BASE_URL: "https://omniroute.example.com",
|
|
ANTIGRAVITY_OAUTH_CLIENT_ID: " ",
|
|
ANTIGRAVITY_OAUTH_CLIENT_SECRET: " ",
|
|
}
|
|
);
|
|
|
|
assert.equal(
|
|
redirectUri,
|
|
"http://127.0.0.1:20128/callback",
|
|
"blank credentials must not trigger override"
|
|
);
|
|
});
|
|
|
|
test("no env object at all keeps loopback", () => {
|
|
const redirectUri = resolveBrowserOAuthRedirectUri(
|
|
"antigravity",
|
|
"http://127.0.0.1:20128/callback",
|
|
{}
|
|
);
|
|
|
|
assert.equal(redirectUri, "http://127.0.0.1:20128/callback");
|
|
});
|
|
|
|
test("no public base URL configured keeps loopback even with custom credentials", () => {
|
|
const redirectUri = resolveBrowserOAuthRedirectUri(
|
|
"antigravity",
|
|
"http://127.0.0.1:20128/callback",
|
|
{
|
|
// No NEXT_PUBLIC_BASE_URL or OMNIROUTE_PUBLIC_BASE_URL
|
|
ANTIGRAVITY_OAUTH_CLIENT_ID: "custom-id.apps.googleusercontent.com",
|
|
ANTIGRAVITY_OAUTH_CLIENT_SECRET: "custom-secret",
|
|
}
|
|
);
|
|
|
|
assert.equal(
|
|
redirectUri,
|
|
"http://127.0.0.1:20128/callback",
|
|
"no public base URL means nowhere to redirect — stay on loopback"
|
|
);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Non-Google providers are not affected
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test("non-Google provider returns redirect URI unchanged regardless of env", () => {
|
|
const redirectUri = resolveBrowserOAuthRedirectUri("claude", "http://localhost:20128/callback", {
|
|
NEXT_PUBLIC_BASE_URL: "https://omniroute.example.com",
|
|
ANTIGRAVITY_OAUTH_CLIENT_ID: "custom-id.apps.googleusercontent.com",
|
|
ANTIGRAVITY_OAUTH_CLIENT_SECRET: "custom-secret",
|
|
});
|
|
|
|
assert.equal(redirectUri, "http://localhost:20128/callback");
|
|
});
|
|
|
|
test("unknown provider returns redirect URI unchanged", () => {
|
|
const redirectUri = resolveBrowserOAuthRedirectUri(
|
|
"some-unknown-provider",
|
|
"http://localhost:20128/callback",
|
|
{
|
|
NEXT_PUBLIC_BASE_URL: "https://omniroute.example.com",
|
|
}
|
|
);
|
|
|
|
assert.equal(redirectUri, "http://localhost:20128/callback");
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Already-remote redirect URIs are not double-overridden
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test("already-remote redirect URI is not overridden even with custom credentials", () => {
|
|
const redirectUri = resolveBrowserOAuthRedirectUri(
|
|
"antigravity",
|
|
"https://my-deployment.example.com/callback",
|
|
{
|
|
NEXT_PUBLIC_BASE_URL: "https://omniroute.example.com",
|
|
ANTIGRAVITY_OAUTH_CLIENT_ID: "custom-id.apps.googleusercontent.com",
|
|
ANTIGRAVITY_OAUTH_CLIENT_SECRET: "custom-secret",
|
|
}
|
|
);
|
|
|
|
assert.equal(
|
|
redirectUri,
|
|
"https://my-deployment.example.com/callback",
|
|
"non-loopback redirect URIs must not be overridden"
|
|
);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// IPv6 loopback and localhost variants
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test("custom credentials override IPv6 loopback [::1] for antigravity", () => {
|
|
const redirectUri = resolveBrowserOAuthRedirectUri("antigravity", "http://[::1]:20128/callback", {
|
|
NEXT_PUBLIC_BASE_URL: "https://omniroute.example.com",
|
|
ANTIGRAVITY_OAUTH_CLIENT_ID: "custom-id.apps.googleusercontent.com",
|
|
ANTIGRAVITY_OAUTH_CLIENT_SECRET: "custom-secret",
|
|
});
|
|
|
|
assert.equal(redirectUri, "https://omniroute.example.com/callback");
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Path and query string preservation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test("custom callback path is preserved when overriding loopback", () => {
|
|
const redirectUri = resolveBrowserOAuthRedirectUri(
|
|
"antigravity",
|
|
"http://127.0.0.1:20128/auth/callback",
|
|
{
|
|
NEXT_PUBLIC_BASE_URL: "https://omniroute.example.com",
|
|
ANTIGRAVITY_OAUTH_CLIENT_ID: "custom-id.apps.googleusercontent.com",
|
|
ANTIGRAVITY_OAUTH_CLIENT_SECRET: "custom-secret",
|
|
}
|
|
);
|
|
|
|
assert.equal(redirectUri, "https://omniroute.example.com/auth/callback");
|
|
});
|
|
|
|
test("query string is preserved when overriding loopback", () => {
|
|
const redirectUri = resolveBrowserOAuthRedirectUri(
|
|
"antigravity",
|
|
"http://127.0.0.1:20128/callback?source=popup&nonce=abc",
|
|
{
|
|
NEXT_PUBLIC_BASE_URL: "https://omniroute.example.com",
|
|
ANTIGRAVITY_OAUTH_CLIENT_ID: "custom-id.apps.googleusercontent.com",
|
|
ANTIGRAVITY_OAUTH_CLIENT_SECRET: "custom-secret",
|
|
}
|
|
);
|
|
|
|
assert.equal(redirectUri, "https://omniroute.example.com/callback?source=popup&nonce=abc");
|
|
});
|
|
|
|
test("root path defaults to /callback when overriding loopback", () => {
|
|
const redirectUri = resolveBrowserOAuthRedirectUri("antigravity", "http://127.0.0.1:20128/", {
|
|
NEXT_PUBLIC_BASE_URL: "https://omniroute.example.com",
|
|
ANTIGRAVITY_OAUTH_CLIENT_ID: "custom-id.apps.googleusercontent.com",
|
|
ANTIGRAVITY_OAUTH_CLIENT_SECRET: "custom-secret",
|
|
});
|
|
|
|
assert.equal(redirectUri, "https://omniroute.example.com/callback");
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Public base URL trailing slash normalization
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test("trailing slash on NEXT_PUBLIC_BASE_URL is stripped", () => {
|
|
const redirectUri = resolveBrowserOAuthRedirectUri(
|
|
"antigravity",
|
|
"http://127.0.0.1:20128/callback",
|
|
{
|
|
NEXT_PUBLIC_BASE_URL: "https://omniroute.example.com/",
|
|
ANTIGRAVITY_OAUTH_CLIENT_ID: "custom-id.apps.googleusercontent.com",
|
|
ANTIGRAVITY_OAUTH_CLIENT_SECRET: "custom-secret",
|
|
}
|
|
);
|
|
|
|
assert.equal(
|
|
redirectUri,
|
|
"https://omniroute.example.com/callback",
|
|
"no double slash between base URL and path"
|
|
);
|
|
});
|
|
|
|
test("OMNIROUTE_PUBLIC_BASE_URL is used as fallback when NEXT_PUBLIC_BASE_URL is absent", () => {
|
|
const redirectUri = resolveBrowserOAuthRedirectUri(
|
|
"antigravity",
|
|
"http://127.0.0.1:20128/callback",
|
|
{
|
|
OMNIROUTE_PUBLIC_BASE_URL: "https://fallback.example.com",
|
|
ANTIGRAVITY_OAUTH_CLIENT_ID: "custom-id.apps.googleusercontent.com",
|
|
ANTIGRAVITY_OAUTH_CLIENT_SECRET: "custom-secret",
|
|
}
|
|
);
|
|
|
|
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"
|
|
);
|
|
});
|