From e13c2e7212b9872e4cb208330d329cbccb9aa088 Mon Sep 17 00:00:00 2001 From: Nicolas Angelo Date: Tue, 30 Jun 2026 09:19:11 -0400 Subject: [PATCH 1/3] fix(electron): default allowedRedirectProtocols to the renderer scheme --- ...lectron-auto-allowed-redirect-protocols.md | 9 ++++ .../react/__tests__/ClerkProvider.test.tsx | 46 +++++++++++++++++++ packages/electron/src/react/index.tsx | 31 ++++++++++++- 3 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 .changeset/electron-auto-allowed-redirect-protocols.md diff --git a/.changeset/electron-auto-allowed-redirect-protocols.md b/.changeset/electron-auto-allowed-redirect-protocols.md new file mode 100644 index 00000000000..ee5d301fb1b --- /dev/null +++ b/.changeset/electron-auto-allowed-redirect-protocols.md @@ -0,0 +1,9 @@ +--- +'@clerk/electron': patch +--- + +`` from `@clerk/electron/react` now allows the renderer's own custom scheme as a redirect protocol by default, so apps no longer need to set `allowedRedirectProtocols={[':']}` manually. + +The renderer is served from the scheme registered via `createClerkBridge({ renderer: { scheme } })` (e.g. `clerk://app`), and Clerk's prebuilt UI renders in-app cross-links (such as "Sign up" on the sign-in card) as absolute anchors against that origin. The provider reads `window.location.protocol` and adds it to clerk-js's redirect allowlist automatically. Standard web protocols (`http`/`https`) are already allowed, so only a custom scheme is added. + +An explicit `allowedRedirectProtocols` value is always respected as-is — including an empty array — so you can fully override the default. diff --git a/packages/electron/src/react/__tests__/ClerkProvider.test.tsx b/packages/electron/src/react/__tests__/ClerkProvider.test.tsx index 096bb49097a..62cbe8c0ff6 100644 --- a/packages/electron/src/react/__tests__/ClerkProvider.test.tsx +++ b/packages/electron/src/react/__tests__/ClerkProvider.test.tsx @@ -92,6 +92,52 @@ describe('Electron ClerkProvider', () => { }); }); + it('defaults allowedRedirectProtocols to the renderer custom scheme', () => { + (window as unknown as { location: { protocol: string } }).location = { protocol: 'clerk:' }; + + renderToStaticMarkup(App); + + expect(capturedProviderProps?.allowedRedirectProtocols).toEqual(['clerk:']); + }); + + it('does not add standard web protocols to allowedRedirectProtocols', () => { + (window as unknown as { location: { protocol: string } }).location = { protocol: 'https:' }; + + renderToStaticMarkup(App); + + expect(capturedProviderProps?.allowedRedirectProtocols).toBeUndefined(); + }); + + it('respects an explicit allowedRedirectProtocols value over the scheme default', () => { + (window as unknown as { location: { protocol: string } }).location = { protocol: 'clerk:' }; + + renderToStaticMarkup( + + App + , + ); + + expect(capturedProviderProps?.allowedRedirectProtocols).toEqual(['myscheme:']); + }); + + it('respects an explicit empty allowedRedirectProtocols array', () => { + (window as unknown as { location: { protocol: string } }).location = { protocol: 'clerk:' }; + + renderToStaticMarkup( + + App + , + ); + + expect(capturedProviderProps?.allowedRedirectProtocols).toEqual([]); + }); + it('registers an OAuth transport backed by the Electron bridge', async () => { oauthTransport.getRedirectUrl.mockResolvedValue('my-app://renderer/'); oauthTransport.open.mockResolvedValue({ callbackUrl: 'my-app://renderer/?code=123' }); diff --git a/packages/electron/src/react/index.tsx b/packages/electron/src/react/index.tsx index 4d8418d52f8..dcc8fb2ba1e 100644 --- a/packages/electron/src/react/index.tsx +++ b/packages/electron/src/react/index.tsx @@ -78,7 +78,34 @@ function createOAuthTransport(): ClerkOAuthTransport | undefined { }; } -export function ClerkProvider({ children, publishableKey, passkeys, ...props }: ClerkProviderProps): JSX.Element { +/** + * Allow the renderer's own custom scheme as a redirect protocol by default. + * + * The renderer is served from the scheme registered via `createClerkBridge({ renderer: { scheme } })` + * (e.g. `clerk://app`), so `window.location.protocol` is that scheme. Clerk's prebuilt UI renders + * in-app cross-links (e.g. "Sign up" on the sign-in card) as absolute anchors against the current + * origin — `clerk://app/sign-up` — which clerk-js's navigate allowlist would otherwise reject. The + * standard web protocols are already allowed by clerk-js, so only a custom scheme needs adding. + * + * Returns `undefined` for the built-in protocols (and outside a browser) so nothing redundant is set. + */ +function defaultAllowedRedirectProtocols(): string[] | undefined { + const protocol = typeof window !== 'undefined' ? window.location?.protocol : undefined; + + if (!protocol || protocol === 'http:' || protocol === 'https:') { + return undefined; + } + + return [protocol]; +} + +export function ClerkProvider({ + children, + publishableKey, + passkeys, + allowedRedirectProtocols, + ...props +}: ClerkProviderProps): JSX.Element { const clerk = createClerkInstance(publishableKey, passkeys); const oauthTransport = createOAuthTransport(); const clerkUI = loadClerkUI(publishableKey, props); @@ -88,6 +115,8 @@ export function ClerkProvider({ children, publishableKey, passkeys, ...props }: {...props} Clerk={clerk} __internal_oauthTransport={oauthTransport} + // Default to the renderer's own scheme; an explicit value (even `[]`) is respected as-is. + allowedRedirectProtocols={allowedRedirectProtocols ?? defaultAllowedRedirectProtocols()} publishableKey={publishableKey} standardBrowser={false} ui={{ ClerkUI: clerkUI }} From f4941cad25057430ada0778cd552c4e65e82d414 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 30 Jun 2026 13:31:50 -0700 Subject: [PATCH 2/3] chore: clean up --- .../electron-auto-allowed-redirect-protocols.md | 4 +--- .../src/react/__tests__/ClerkProvider.test.tsx | 8 ++++++++ packages/electron/src/react/index.tsx | 15 ++++----------- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/.changeset/electron-auto-allowed-redirect-protocols.md b/.changeset/electron-auto-allowed-redirect-protocols.md index ee5d301fb1b..2fea342cfed 100644 --- a/.changeset/electron-auto-allowed-redirect-protocols.md +++ b/.changeset/electron-auto-allowed-redirect-protocols.md @@ -4,6 +4,4 @@ `` from `@clerk/electron/react` now allows the renderer's own custom scheme as a redirect protocol by default, so apps no longer need to set `allowedRedirectProtocols={[':']}` manually. -The renderer is served from the scheme registered via `createClerkBridge({ renderer: { scheme } })` (e.g. `clerk://app`), and Clerk's prebuilt UI renders in-app cross-links (such as "Sign up" on the sign-in card) as absolute anchors against that origin. The provider reads `window.location.protocol` and adds it to clerk-js's redirect allowlist automatically. Standard web protocols (`http`/`https`) are already allowed, so only a custom scheme is added. - -An explicit `allowedRedirectProtocols` value is always respected as-is — including an empty array — so you can fully override the default. +This applies when the renderer is served from the custom scheme registered with `createClerkBridge({ renderer })`. Local `file:` renderers are not allowlisted automatically, and explicit `allowedRedirectProtocols` values are still respected. diff --git a/packages/electron/src/react/__tests__/ClerkProvider.test.tsx b/packages/electron/src/react/__tests__/ClerkProvider.test.tsx index 62cbe8c0ff6..52072b4a99a 100644 --- a/packages/electron/src/react/__tests__/ClerkProvider.test.tsx +++ b/packages/electron/src/react/__tests__/ClerkProvider.test.tsx @@ -108,6 +108,14 @@ describe('Electron ClerkProvider', () => { expect(capturedProviderProps?.allowedRedirectProtocols).toBeUndefined(); }); + it('does not add file protocol to allowedRedirectProtocols', () => { + (window as unknown as { location: { protocol: string } }).location = { protocol: 'file:' }; + + renderToStaticMarkup(App); + + expect(capturedProviderProps?.allowedRedirectProtocols).toBeUndefined(); + }); + it('respects an explicit allowedRedirectProtocols value over the scheme default', () => { (window as unknown as { location: { protocol: string } }).location = { protocol: 'clerk:' }; diff --git a/packages/electron/src/react/index.tsx b/packages/electron/src/react/index.tsx index dcc8fb2ba1e..19a07449162 100644 --- a/packages/electron/src/react/index.tsx +++ b/packages/electron/src/react/index.tsx @@ -1,5 +1,6 @@ import type { ClerkProviderProps as ReactClerkProviderProps } from '@clerk/react'; import { InternalClerkProvider as ReactClerkProvider } from '@clerk/react/internal'; +import { ALLOWED_PROTOCOLS } from '@clerk/shared/internal/clerk-js/windowNavigate'; import { loadClerkUIScript } from '@clerk/shared/loadClerkJsScript'; import type { ClerkUIConstructor } from '@clerk/shared/ui'; import type { ReactNode } from 'react'; @@ -79,20 +80,13 @@ function createOAuthTransport(): ClerkOAuthTransport | undefined { } /** - * Allow the renderer's own custom scheme as a redirect protocol by default. - * - * The renderer is served from the scheme registered via `createClerkBridge({ renderer: { scheme } })` - * (e.g. `clerk://app`), so `window.location.protocol` is that scheme. Clerk's prebuilt UI renders - * in-app cross-links (e.g. "Sign up" on the sign-in card) as absolute anchors against the current - * origin — `clerk://app/sign-up` — which clerk-js's navigate allowlist would otherwise reject. The - * standard web protocols are already allowed by clerk-js, so only a custom scheme needs adding. - * - * Returns `undefined` for the built-in protocols (and outside a browser) so nothing redundant is set. + * Infer the custom renderer scheme registered with `createClerkBridge({ renderer })`. + * Built-in Clerk protocols and local file renderers are not inferred. */ function defaultAllowedRedirectProtocols(): string[] | undefined { const protocol = typeof window !== 'undefined' ? window.location?.protocol : undefined; - if (!protocol || protocol === 'http:' || protocol === 'https:') { + if (!protocol || ALLOWED_PROTOCOLS.includes(protocol) || protocol === 'file:') { return undefined; } @@ -115,7 +109,6 @@ export function ClerkProvider({ {...props} Clerk={clerk} __internal_oauthTransport={oauthTransport} - // Default to the renderer's own scheme; an explicit value (even `[]`) is respected as-is. allowedRedirectProtocols={allowedRedirectProtocols ?? defaultAllowedRedirectProtocols()} publishableKey={publishableKey} standardBrowser={false} From 74d00ce5c2345730474b3f6928c258e8f2ff0d21 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 30 Jun 2026 13:35:36 -0700 Subject: [PATCH 3/3] chore: properly stub window protocol --- .../src/react/__tests__/ClerkProvider.test.tsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/electron/src/react/__tests__/ClerkProvider.test.tsx b/packages/electron/src/react/__tests__/ClerkProvider.test.tsx index 52072b4a99a..db19521fe50 100644 --- a/packages/electron/src/react/__tests__/ClerkProvider.test.tsx +++ b/packages/electron/src/react/__tests__/ClerkProvider.test.tsx @@ -40,6 +40,13 @@ vi.mock('@clerk/shared/loadClerkJsScript', async importOriginal => ({ loadClerkUIScript, })); +function stubWindowProtocol(protocol: string) { + vi.stubGlobal('window', { + ...window, + location: { protocol }, + }); +} + describe('Electron ClerkProvider', () => { const tokenCache = { clearToken: vi.fn(), @@ -93,7 +100,7 @@ describe('Electron ClerkProvider', () => { }); it('defaults allowedRedirectProtocols to the renderer custom scheme', () => { - (window as unknown as { location: { protocol: string } }).location = { protocol: 'clerk:' }; + stubWindowProtocol('clerk:'); renderToStaticMarkup(App); @@ -101,7 +108,7 @@ describe('Electron ClerkProvider', () => { }); it('does not add standard web protocols to allowedRedirectProtocols', () => { - (window as unknown as { location: { protocol: string } }).location = { protocol: 'https:' }; + stubWindowProtocol('https:'); renderToStaticMarkup(App); @@ -109,7 +116,7 @@ describe('Electron ClerkProvider', () => { }); it('does not add file protocol to allowedRedirectProtocols', () => { - (window as unknown as { location: { protocol: string } }).location = { protocol: 'file:' }; + stubWindowProtocol('file:'); renderToStaticMarkup(App); @@ -117,7 +124,7 @@ describe('Electron ClerkProvider', () => { }); it('respects an explicit allowedRedirectProtocols value over the scheme default', () => { - (window as unknown as { location: { protocol: string } }).location = { protocol: 'clerk:' }; + stubWindowProtocol('clerk:'); renderToStaticMarkup( { }); it('respects an explicit empty allowedRedirectProtocols array', () => { - (window as unknown as { location: { protocol: string } }).location = { protocol: 'clerk:' }; + stubWindowProtocol('clerk:'); renderToStaticMarkup(