From 25dc581a92ceb24b780fdc50e998189c581dddd5 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Thu, 28 May 2026 15:51:32 -0700 Subject: [PATCH 1/6] chore(clerk-js,ui,shared): Correctly display OAuth consent redirect domains for known multi-label public suffixes --- .changeset/quiet-terms-drum.md | 5 +++++ .../core/modules/oauthApplication/index.ts | 8 ++++++-- .../src/react/hooks/useOAuthConsent.shared.ts | 20 ++++++++++++------- .../src/react/hooks/useOAuthConsent.tsx | 15 +++++++++----- .../src/react/hooks/useOAuthConsent.types.ts | 2 +- packages/shared/src/types/oauthApplication.ts | 9 +++++++++ .../components/OAuthConsent/OAuthConsent.tsx | 6 ++++-- 7 files changed, 48 insertions(+), 17 deletions(-) create mode 100644 .changeset/quiet-terms-drum.md diff --git a/.changeset/quiet-terms-drum.md b/.changeset/quiet-terms-drum.md new file mode 100644 index 00000000000..0ebfc583525 --- /dev/null +++ b/.changeset/quiet-terms-drum.md @@ -0,0 +1,5 @@ +--- +'@clerk/ui': patch +--- + +Correctly display OAuth consent redirect domains for known multi-label public suffixes. diff --git a/packages/clerk-js/src/core/modules/oauthApplication/index.ts b/packages/clerk-js/src/core/modules/oauthApplication/index.ts index 11d54c099af..d24d074c657 100644 --- a/packages/clerk-js/src/core/modules/oauthApplication/index.ts +++ b/packages/clerk-js/src/core/modules/oauthApplication/index.ts @@ -10,12 +10,15 @@ import { BaseResource } from '../../resources/internal'; export class OAuthApplication implements OAuthApplicationNamespace { async getConsentInfo(params: GetOAuthConsentInfoParams): Promise { - const { oauthClientId, scope } = params; + const { oauthClientId, scope, redirectUri } = params; + const search: Record = {}; + if (scope !== undefined) search.scope = scope; + if (redirectUri !== undefined) search.redirect_uri = redirectUri; const json = await BaseResource._fetch( { method: 'GET', path: `/me/oauth/consent/${encodeURIComponent(oauthClientId)}`, - search: scope !== undefined ? { scope } : undefined, + search: Object.keys(search).length > 0 ? search : undefined, }, { skipUpdateClient: true }, ); @@ -31,6 +34,7 @@ export class OAuthApplication implements OAuthApplicationNamespace { oauthApplicationUrl: data.oauth_application_url, clientId: data.client_id, state: data.state, + redirectDomain: data.redirect_domain, scopes: data.scopes?.map(s => ({ scope: s.scope, diff --git a/packages/shared/src/react/hooks/useOAuthConsent.shared.ts b/packages/shared/src/react/hooks/useOAuthConsent.shared.ts index 9b21222ffe1..d086bb70595 100644 --- a/packages/shared/src/react/hooks/useOAuthConsent.shared.ts +++ b/packages/shared/src/react/hooks/useOAuthConsent.shared.ts @@ -4,13 +4,19 @@ import type { GetOAuthConsentInfoParams } from '../../types'; import { STABLE_KEYS } from '../stable-keys'; import { createCacheKeys } from './createCacheKeys'; -export function useOAuthConsentCacheKeys(params: { userId: string | null; oauthClientId: string; scope?: string }) { - const { userId, oauthClientId, scope } = params; +export function useOAuthConsentCacheKeys(params: { + userId: string | null; + oauthClientId: string; + scope?: string; + redirectUri?: string; +}) { + const { userId, oauthClientId, scope, redirectUri } = params; return useMemo(() => { - const args: Pick & { scope?: string } = { oauthClientId }; - if (scope !== undefined) { - args.scope = scope; - } + const args: Pick & { scope?: string; redirectUri?: string } = { + oauthClientId, + }; + if (scope !== undefined) args.scope = scope; + if (redirectUri !== undefined) args.redirectUri = redirectUri; return createCacheKeys({ stablePrefix: STABLE_KEYS.OAUTH_CONSENT_INFO_KEY, authenticated: true, @@ -21,5 +27,5 @@ export function useOAuthConsentCacheKeys(params: { userId: string | null; oauthC args, }, }); - }, [userId, oauthClientId, scope]); + }, [userId, oauthClientId, scope, redirectUri]); } diff --git a/packages/shared/src/react/hooks/useOAuthConsent.tsx b/packages/shared/src/react/hooks/useOAuthConsent.tsx index 674654a8bea..2ed35da8cc2 100644 --- a/packages/shared/src/react/hooks/useOAuthConsent.tsx +++ b/packages/shared/src/react/hooks/useOAuthConsent.tsx @@ -27,7 +27,7 @@ const HOOK_NAME = 'useOAuthConsent'; export function useOAuthConsent(params: UseOAuthConsentParams): UseOAuthConsentReturn { useAssertWrappedByClerkProvider(HOOK_NAME); - const { oauthClientId: oauthClientIdParam, scope, keepPreviousData = true, enabled = true } = params; + const { oauthClientId: oauthClientIdParam, scope, redirectUri, keepPreviousData = true, enabled = true } = params; const clerk = useClerkInstanceContext(); const user = useUserBase(); @@ -39,6 +39,7 @@ export function useOAuthConsent(params: UseOAuthConsentParams): UseOAuthConsentR userId: user?.id ?? null, oauthClientId, scope, + redirectUri, }); const hasClientId = oauthClientId.length > 0; @@ -46,7 +47,7 @@ export function useOAuthConsent(params: UseOAuthConsentParams): UseOAuthConsentR const query = useClerkQuery({ queryKey, - queryFn: () => fetchConsentInfo(clerk, { oauthClientId, scope }), + queryFn: () => fetchConsentInfo(clerk, { oauthClientId, scope, redirectUri }), enabled: queryEnabled, placeholderData: defineKeepPreviousDataFn(keepPreviousData && queryEnabled), }); @@ -59,7 +60,11 @@ export function useOAuthConsent(params: UseOAuthConsentParams): UseOAuthConsentR }; } -function fetchConsentInfo(clerk: LoadedClerk, params: { oauthClientId: string; scope?: string }) { - const { oauthClientId, scope } = params; - return clerk.oauthApplication.getConsentInfo(scope !== undefined ? { oauthClientId, scope } : { oauthClientId }); +function fetchConsentInfo(clerk: LoadedClerk, params: { oauthClientId: string; scope?: string; redirectUri?: string }) { + const { oauthClientId, scope, redirectUri } = params; + return clerk.oauthApplication.getConsentInfo({ + oauthClientId, + ...(scope !== undefined && { scope }), + ...(redirectUri !== undefined && { redirectUri }), + }); } diff --git a/packages/shared/src/react/hooks/useOAuthConsent.types.ts b/packages/shared/src/react/hooks/useOAuthConsent.types.ts index 8fde2f44516..659157bbe57 100644 --- a/packages/shared/src/react/hooks/useOAuthConsent.types.ts +++ b/packages/shared/src/react/hooks/useOAuthConsent.types.ts @@ -4,7 +4,7 @@ import type { GetOAuthConsentInfoParams, OAuthConsentInfo } from '../../types'; /** * @interface */ -export type UseOAuthConsentParams = Pick & { +export type UseOAuthConsentParams = Pick & { /** * If `true`, the previous data will be kept in the cache until new data is fetched. * diff --git a/packages/shared/src/types/oauthApplication.ts b/packages/shared/src/types/oauthApplication.ts index 7bb270e38ce..3d633999e72 100644 --- a/packages/shared/src/types/oauthApplication.ts +++ b/packages/shared/src/types/oauthApplication.ts @@ -19,6 +19,7 @@ export interface OAuthConsentInfoJSON extends ClerkResourceJSON { oauth_application_url: string; client_id: string; state: string; + redirect_domain: string | null; scopes: OAuthConsentScopeJSON[]; } @@ -68,6 +69,12 @@ export type OAuthConsentInfo = { * The `state` parameter from the original authorize request. */ state: string; + /** + * The PSL-resolved registrable domain of the redirect URI for display on the consent screen. + * Null when no redirect URI was provided, when it is not registered for the application, + * or when it points to an IP address or localhost. + */ + redirectDomain: string | null; /** * A list of scopes the application is requesting, with descriptions and consent requirements. */ @@ -79,6 +86,8 @@ export type GetOAuthConsentInfoParams = { oauthClientId: string; /** A space-delimited scope string from the authorize request. */ scope?: string; + /** The redirect URI from the authorize request. When provided, the backend returns a PSL-resolved `redirectDomain`. */ + redirectUri?: string; }; /** diff --git a/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx b/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx index 9910905c096..c3f0007ae43 100644 --- a/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx +++ b/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx @@ -50,9 +50,11 @@ function _OAuthConsent() { // Public path: fetch via hook. Disabled on the accounts portal path // (which already has all data via context) to avoid a wasted FAPI request. + const redirectUri = getRedirectUriFromSearch(); const { data, isLoading, error } = useOAuthConsent({ oauthClientId, scope, + redirectUri: redirectUri || undefined, // TODO: Remove this once account portal is refactored to use this component enabled: !hasContextCallbacks, }); @@ -69,7 +71,7 @@ function _OAuthConsent() { const oauthApplicationName = ctx.oauthApplicationName ?? data?.oauthApplicationName ?? ''; const oauthApplicationLogoUrl = ctx.oauthApplicationLogoUrl ?? data?.oauthApplicationLogoUrl; const oauthApplicationUrl = ctx.oauthApplicationUrl ?? data?.oauthApplicationUrl; - const redirectUrl = ctx.redirectUrl ?? getRedirectUriFromSearch(); + const redirectUrl = ctx.redirectUrl ?? redirectUri; const hasOrgReadScope = scopes.some(s => s.scope === USER_ORG_READ_SCOPE); const orgSelectionEnabled = !!(hasOrgReadScope && organizationSettings.enabled); @@ -85,7 +87,7 @@ function _OAuthConsent() { const effectiveOrg = selectedOrg ?? defaultOrg; const { t } = useLocalizations(); - const domainAction = getRedirectDisplay(redirectUrl); + const domainAction = data?.redirectDomain ?? getRedirectDisplay(redirectUrl); const viewFullUrlText = t(localizationKeys('oauthConsent.viewFullUrl')); // Error states only apply to the public flow. From 9c0e45daf20425592f9a1de96a6523f812b9ba9b Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Fri, 29 May 2026 10:37:54 -0700 Subject: [PATCH 2/6] chore: clean up --- .../clerk-js/src/core/modules/oauthApplication/index.ts | 7 ++++--- packages/shared/src/react/hooks/useOAuthConsent.shared.ts | 7 +++---- packages/shared/src/react/hooks/useOAuthConsent.tsx | 7 +------ 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/packages/clerk-js/src/core/modules/oauthApplication/index.ts b/packages/clerk-js/src/core/modules/oauthApplication/index.ts index d24d074c657..5907bb51979 100644 --- a/packages/clerk-js/src/core/modules/oauthApplication/index.ts +++ b/packages/clerk-js/src/core/modules/oauthApplication/index.ts @@ -11,9 +11,10 @@ import { BaseResource } from '../../resources/internal'; export class OAuthApplication implements OAuthApplicationNamespace { async getConsentInfo(params: GetOAuthConsentInfoParams): Promise { const { oauthClientId, scope, redirectUri } = params; - const search: Record = {}; - if (scope !== undefined) search.scope = scope; - if (redirectUri !== undefined) search.redirect_uri = redirectUri; + const search = { + ...(scope !== undefined && { scope }), + ...(redirectUri !== undefined && { redirect_uri: redirectUri }), + }; const json = await BaseResource._fetch( { method: 'GET', diff --git a/packages/shared/src/react/hooks/useOAuthConsent.shared.ts b/packages/shared/src/react/hooks/useOAuthConsent.shared.ts index d086bb70595..55568933167 100644 --- a/packages/shared/src/react/hooks/useOAuthConsent.shared.ts +++ b/packages/shared/src/react/hooks/useOAuthConsent.shared.ts @@ -1,6 +1,5 @@ import { useMemo } from 'react'; -import type { GetOAuthConsentInfoParams } from '../../types'; import { STABLE_KEYS } from '../stable-keys'; import { createCacheKeys } from './createCacheKeys'; @@ -12,11 +11,11 @@ export function useOAuthConsentCacheKeys(params: { }) { const { userId, oauthClientId, scope, redirectUri } = params; return useMemo(() => { - const args: Pick & { scope?: string; redirectUri?: string } = { + const args = { oauthClientId, + ...(scope !== undefined && { scope }), + ...(redirectUri !== undefined && { redirectUri }), }; - if (scope !== undefined) args.scope = scope; - if (redirectUri !== undefined) args.redirectUri = redirectUri; return createCacheKeys({ stablePrefix: STABLE_KEYS.OAUTH_CONSENT_INFO_KEY, authenticated: true, diff --git a/packages/shared/src/react/hooks/useOAuthConsent.tsx b/packages/shared/src/react/hooks/useOAuthConsent.tsx index 2ed35da8cc2..c7a7513f494 100644 --- a/packages/shared/src/react/hooks/useOAuthConsent.tsx +++ b/packages/shared/src/react/hooks/useOAuthConsent.tsx @@ -61,10 +61,5 @@ export function useOAuthConsent(params: UseOAuthConsentParams): UseOAuthConsentR } function fetchConsentInfo(clerk: LoadedClerk, params: { oauthClientId: string; scope?: string; redirectUri?: string }) { - const { oauthClientId, scope, redirectUri } = params; - return clerk.oauthApplication.getConsentInfo({ - oauthClientId, - ...(scope !== undefined && { scope }), - ...(redirectUri !== undefined && { redirectUri }), - }); + return clerk.oauthApplication.getConsentInfo(params); } From fca1261d2cc638391661a1968b299efa989718e1 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Fri, 29 May 2026 10:49:15 -0700 Subject: [PATCH 3/6] fix tests --- .../components/OAuthConsent/__tests__/OAuthConsent.test.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/ui/src/components/OAuthConsent/__tests__/OAuthConsent.test.tsx b/packages/ui/src/components/OAuthConsent/__tests__/OAuthConsent.test.tsx index 917c7c4953c..0fc4c2aac5b 100644 --- a/packages/ui/src/components/OAuthConsent/__tests__/OAuthConsent.test.tsx +++ b/packages/ui/src/components/OAuthConsent/__tests__/OAuthConsent.test.tsx @@ -13,6 +13,7 @@ const fakeConsentInfo = { oauthApplicationUrl: 'https://example.com', clientId: 'client_test', state: 'abc', + redirectDomain: 'example.com', scopes: [ { scope: 'openid', description: 'View your identity', requiresConsent: true }, { scope: 'email', description: 'Access your email address', requiresConsent: true }, @@ -94,6 +95,7 @@ describe('OAuthConsent', () => { expect(getConsentInfo).toHaveBeenCalledWith({ oauthClientId: 'client_test', + redirectUri: 'https://app.example/callback', }); }); @@ -204,6 +206,7 @@ describe('OAuthConsent', () => { expect(getConsentInfo).toHaveBeenCalledWith({ oauthClientId: 'override_id', scope: 'openid email', + redirectUri: 'https://app.example/callback', }); }); }); From f54230a1f816c0f512a435e84ee07269864cbff2 Mon Sep 17 00:00:00 2001 From: Robert Soriano Date: Fri, 29 May 2026 13:48:42 -0700 Subject: [PATCH 4/6] Apply suggestions from code review Co-authored-by: Jacob Foshee --- packages/ui/src/components/OAuthConsent/OAuthConsent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx b/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx index c3f0007ae43..23dc78da070 100644 --- a/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx +++ b/packages/ui/src/components/OAuthConsent/OAuthConsent.tsx @@ -50,7 +50,7 @@ function _OAuthConsent() { // Public path: fetch via hook. Disabled on the accounts portal path // (which already has all data via context) to avoid a wasted FAPI request. - const redirectUri = getRedirectUriFromSearch(); + const redirectUri = ctx.redirectUrl ?? getRedirectUriFromSearch(); const { data, isLoading, error } = useOAuthConsent({ oauthClientId, scope, From 25e35c1e69279c1a6bc499c894f4c0596f4f375c Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Mon, 1 Jun 2026 10:40:32 -0700 Subject: [PATCH 5/6] chore: update changeset --- .changeset/quiet-terms-drum.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.changeset/quiet-terms-drum.md b/.changeset/quiet-terms-drum.md index 0ebfc583525..362d7a14bb9 100644 --- a/.changeset/quiet-terms-drum.md +++ b/.changeset/quiet-terms-drum.md @@ -1,5 +1,7 @@ --- '@clerk/ui': patch +'@clerk/js': patch +'@clerk/shared': patch --- Correctly display OAuth consent redirect domains for known multi-label public suffixes. From dfa4c1f9adf40178f77cccf129a0cfb6a3c7220b Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Mon, 1 Jun 2026 10:41:14 -0700 Subject: [PATCH 6/6] fix changeset --- .changeset/quiet-terms-drum.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/quiet-terms-drum.md b/.changeset/quiet-terms-drum.md index 362d7a14bb9..6a8105d859b 100644 --- a/.changeset/quiet-terms-drum.md +++ b/.changeset/quiet-terms-drum.md @@ -1,6 +1,6 @@ --- '@clerk/ui': patch -'@clerk/js': patch +'@clerk/clerk-js': patch '@clerk/shared': patch ---