From 14590923cefb03a82e888475d7b7bd870afb3a03 Mon Sep 17 00:00:00 2001 From: gabrielmeloc22 Date: Wed, 22 Apr 2026 18:33:06 -0300 Subject: [PATCH 1/3] fix: correct force redirect behavior for oauth --- packages/shared/src/internal/clerk-js/redirectUrls.ts | 11 ++++++++++- packages/shared/src/internal/clerk-js/url.ts | 11 +++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/shared/src/internal/clerk-js/redirectUrls.ts b/packages/shared/src/internal/clerk-js/redirectUrls.ts index cf9cdaf9386..1bcae1dd1cc 100644 --- a/packages/shared/src/internal/clerk-js/redirectUrls.ts +++ b/packages/shared/src/internal/clerk-js/redirectUrls.ts @@ -1,7 +1,7 @@ import { applyFunctionToObj, filterProps, removeUndefined } from '../../object'; import type { ClerkOptions, RedirectOptions } from '../../types'; import { camelToSnake } from '../../underscore'; -import { isAllowedRedirect, relativeToAbsoluteUrl } from './url'; +import { isAllowedRedirect, isFAPIInitiatedFlowPath, relativeToAbsoluteUrl } from './url'; type ComponentMode = 'modal' | 'mounted'; @@ -99,6 +99,15 @@ export class RedirectUrls { const forceKey = `${prefix}ForceRedirectUrl` as const; const fallbackKey = `${prefix}FallbackRedirectUrl` as const; + // FAPI-initiated flow redirect URLs (e.g. /oauth/authorize/continue) must + // take highest priority to ensure the IDP authorization flow completes. + // Without this, a configured signInForceRedirectUrl would override the + // redirect_url needed to resume the OAuth IDP flow after sign-in. + const fapiRedirectUrl = this.fromSearchParams.redirectUrl; + if (fapiRedirectUrl && isFAPIInitiatedFlowPath(fapiRedirectUrl)) { + return fapiRedirectUrl; + } + let result; // Prioritize forceRedirectUrl result = this.fromSearchParams[forceKey] || this.fromProps[forceKey] || this.fromOptions[forceKey]; diff --git a/packages/shared/src/internal/clerk-js/url.ts b/packages/shared/src/internal/clerk-js/url.ts index 6c524febf4a..6f76e467188 100644 --- a/packages/shared/src/internal/clerk-js/url.ts +++ b/packages/shared/src/internal/clerk-js/url.ts @@ -400,6 +400,7 @@ export const pathFromFullPath = (fullPath: string) => { const frontendApiRedirectPathsWithUserInput: string[] = [ '/oauth/authorize', // OAuth2 identify provider flow + '/oauth/authorize/continue', // OAuth 2 identity provider continuation ]; const frontendApiRedirectPathsNoUserInput: string[] = [ @@ -423,6 +424,16 @@ export function requiresUserInput(redirectUrl: string): boolean { return frontendApiRedirectPathsWithUserInput.includes(url.pathname); } +export function isFAPIInitiatedFlowPath(url: string): boolean { + try { + const parsed = new URL(url, DUMMY_URL_BASE); + const allFAPIFlowPaths = [...frontendApiRedirectPathsWithUserInput, ...frontendApiRedirectPathsNoUserInput]; + return allFAPIFlowPaths.includes(parsed.pathname); + } catch { + return false; + } +} + export const isAllowedRedirect = (allowedRedirectOrigins: Array | undefined, currentOrigin: string) => (_url: URL | string) => { let url = _url; From eec0d826b15f94b5e7988c54e1099d535168e7db Mon Sep 17 00:00:00 2001 From: gabrielmeloc22 Date: Thu, 23 Apr 2026 11:15:33 -0300 Subject: [PATCH 2/3] fix: apply coderabbit suggestions --- .../src/internal/clerk-js/__tests__/url.test.ts | 4 +++- packages/shared/src/internal/clerk-js/url.ts | 16 ++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/shared/src/internal/clerk-js/__tests__/url.test.ts b/packages/shared/src/internal/clerk-js/__tests__/url.test.ts index 63927fae344..ddd8fd20bd2 100644 --- a/packages/shared/src/internal/clerk-js/__tests__/url.test.ts +++ b/packages/shared/src/internal/clerk-js/__tests__/url.test.ts @@ -497,6 +497,7 @@ describe('isRedirectForFAPIInitiatedFlow(frontendAp: string, redirectUrl: string ['clerk.foo.bar-53.lcl.dev', 'foo', false], ['clerk.foo.bar-53.lcl.dev', 'https://clerk.foo.bar-53.lcl.dev/deadbeef.', false], ['clerk.foo.bar-53.lcl.dev', 'https://clerk.foo.bar-53.lcl.dev/oauth/authorize', true], + ['clerk.foo.bar-53.lcl.dev', 'https://clerk.foo.bar-53.lcl.dev/oauth/authorize/continue', true], ['clerk.foo.bar-53.lcl.dev', 'https://clerk.foo.bar-53.lcl.dev/v1/verify', true], ['clerk.foo.bar-53.lcl.dev', 'https://clerk.foo.bar-53.lcl.dev/v1/tickets/accept', true], ['clerk.foo.bar-53.lcl.dev', 'https://clerk.foo.bar-53.lcl.dev/oauth/authorize-with-immediate-redirect', true], @@ -518,9 +519,10 @@ describe('requiresUserInput(redirectUrl: string)', () => { ['foo', false], ['https://clerk.foo.bar-53.lcl.dev/deadbeef.', false], ['https://clerk.foo.bar-53.lcl.dev/oauth/authorize', true], + ['https://clerk.foo.bar-53.lcl.dev/oauth/authorize/continue', true], ['https://clerk.foo.bar-53.lcl.dev/v1/verify', false], ['https://clerk.foo.bar-53.lcl.dev/v1/tickets/accept', false], - ['https://clerk.foo.bar-53.lcl.dev/oauth/authorize-with-immediate-redirect', false], + ['https://clerk.foo.bar-53.lcl.dev/oauth/authorize-with-immediate-redirect', true], ['https://clerk.foo.bar-53.lcl.dev/oauth/end_session', false], ['https://google.com', false], ['https://google.com/v1/verify', false], diff --git a/packages/shared/src/internal/clerk-js/url.ts b/packages/shared/src/internal/clerk-js/url.ts index 6f76e467188..4c411dcf8b2 100644 --- a/packages/shared/src/internal/clerk-js/url.ts +++ b/packages/shared/src/internal/clerk-js/url.ts @@ -401,12 +401,12 @@ export const pathFromFullPath = (fullPath: string) => { const frontendApiRedirectPathsWithUserInput: string[] = [ '/oauth/authorize', // OAuth2 identify provider flow '/oauth/authorize/continue', // OAuth 2 identity provider continuation + '/oauth/authorize-with-immediate-redirect', // OAuth 2 identity provider (requires sign-in first) ]; const frontendApiRedirectPathsNoUserInput: string[] = [ '/v1/verify', // magic links '/v1/tickets/accept', // ticket flow - '/oauth/authorize-with-immediate-redirect', // OAuth 2 identity provider '/oauth/end_session', // OIDC logout ]; @@ -424,9 +424,21 @@ export function requiresUserInput(redirectUrl: string): boolean { return frontendApiRedirectPathsWithUserInput.includes(url.pathname); } +/** + * Checks if a URL points to a known FAPI-initiated flow endpoint. + * Only matches absolute URLs whose origin differs from the current window origin, + * preventing customer app routes from accidentally matching FAPI paths. + */ export function isFAPIInitiatedFlowPath(url: string): boolean { try { - const parsed = new URL(url, DUMMY_URL_BASE); + // Only match absolute URLs — relative paths like "/oauth/authorize" should not match + // as they would be customer app routes, not FAPI endpoints. + // new URL(url) without a base will throw for relative URLs. + const parsed = new URL(url); + // Ensure the URL is not on the same origin as the current page (FAPI is always cross-origin) + if (typeof window !== 'undefined' && parsed.origin === window.location.origin) { + return false; + } const allFAPIFlowPaths = [...frontendApiRedirectPathsWithUserInput, ...frontendApiRedirectPathsNoUserInput]; return allFAPIFlowPaths.includes(parsed.pathname); } catch { From e3bdd9bded033f73b4cb76099bd7d4344f1ecb6d Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Thu, 23 Apr 2026 15:50:02 -0400 Subject: [PATCH 3/3] Add changeset --- .changeset/red-windows-train.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/red-windows-train.md diff --git a/.changeset/red-windows-train.md b/.changeset/red-windows-train.md new file mode 100644 index 00000000000..47e582ac8b1 --- /dev/null +++ b/.changeset/red-windows-train.md @@ -0,0 +1,5 @@ +--- +'@clerk/shared': patch +--- + +Fix OAuth application flows to handle redirect to `redirect_url` from `/oauth/authorize/continue`