diff --git a/.changeset/local-cimd-loopback-only.md b/.changeset/local-cimd-loopback-only.md new file mode 100644 index 0000000000..2ff9f675fa --- /dev/null +++ b/.changeset/local-cimd-loopback-only.md @@ -0,0 +1,5 @@ +--- +"executor": patch +--- + +Use the hosted loopback OAuth client metadata document only when Executor is opened on a loopback origin. A local Executor reached through a public reverse proxy now publishes its own client metadata document, so providers that support Client ID Metadata Documents no longer reject its callback as an invalid redirect URI. diff --git a/packages/react/src/plugins/oauth-sign-in.test.ts b/packages/react/src/plugins/oauth-sign-in.test.ts index ba5e89fe6a..bb75de9984 100644 --- a/packages/react/src/plugins/oauth-sign-in.test.ts +++ b/packages/react/src/plugins/oauth-sign-in.test.ts @@ -50,6 +50,26 @@ describe("oauthClientIdMetadataDocumentUrl", () => { }), ).toBe("https://executor.sh/api/oauth/client-id-metadata/local.json"); }); + + it("uses the hosted local document on an IPv6 loopback origin", () => { + setLocation("http://[::1]:4788/integrations/posthog"); + + expect( + oauthClientIdMetadataDocumentUrl({ + hostedBaseUrl: "https://executor.sh", + }), + ).toBe("https://executor.sh/api/oauth/client-id-metadata/local.json"); + }); + + it("uses the current origin behind a public reverse proxy", () => { + setLocation("https://executor-mcp.example.com/integrations/posthog"); + + expect( + oauthClientIdMetadataDocumentUrl({ + hostedBaseUrl: "https://executor.sh", + }), + ).toBe("https://executor-mcp.example.com/api/oauth/client-id-metadata/default.json"); + }); }); describe("oauthCallbackUrl", () => { diff --git a/packages/react/src/plugins/oauth-sign-in.tsx b/packages/react/src/plugins/oauth-sign-in.tsx index f46d6378e5..ba069671f2 100644 --- a/packages/react/src/plugins/oauth-sign-in.tsx +++ b/packages/react/src/plugins/oauth-sign-in.tsx @@ -147,12 +147,21 @@ const configuredClientIdMetadataBaseUrl = (): string | undefined => { return value ? value : undefined; }; +// The hosted local document only lists loopback callbacks, so a local +// Executor reached through a public origin (a reverse proxy) must publish its +// own document instead. +const isLoopbackHostname = (hostname: string): boolean => + hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]"; + export function oauthClientIdMetadataDocumentUrl(options?: { readonly hostedBaseUrl?: string | null; readonly path?: string; }): string { const hostedBaseUrl = options?.hostedBaseUrl ?? configuredClientIdMetadataBaseUrl(); - if (hostedBaseUrl) { + if ( + hostedBaseUrl && + (typeof window === "undefined" || isLoopbackHostname(window.location.hostname)) + ) { return new URL(OAUTH_CLIENT_ID_METADATA_DOCUMENT_LOCAL_PATH, hostedBaseUrl).toString(); }