Skip to content

Commit b295af3

Browse files
wobsorianojfosheejacekradko
authored
chore(js,ui,shared): Correctly display OAuth consent redirect root domains (#8700)
Co-authored-by: Jacob Foshee <jacobf@gmail.com> Co-authored-by: Jacek Radko <jacek@clerk.dev>
1 parent b0f56c9 commit b295af3

8 files changed

Lines changed: 49 additions & 18 deletions

File tree

.changeset/quiet-terms-drum.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@clerk/ui': patch
3+
'@clerk/clerk-js': patch
4+
'@clerk/shared': patch
5+
---
6+
7+
Correctly display OAuth consent redirect domains for known multi-label public suffixes.

packages/clerk-js/src/core/modules/oauthApplication/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,16 @@ import { BaseResource } from '../../resources/internal';
1010

1111
export class OAuthApplication implements OAuthApplicationNamespace {
1212
async getConsentInfo(params: GetOAuthConsentInfoParams): Promise<OAuthConsentInfo> {
13-
const { oauthClientId, scope } = params;
13+
const { oauthClientId, scope, redirectUri } = params;
14+
const search = {
15+
...(scope !== undefined && { scope }),
16+
...(redirectUri !== undefined && { redirect_uri: redirectUri }),
17+
};
1418
const json = await BaseResource._fetch<OAuthConsentInfoJSON>(
1519
{
1620
method: 'GET',
1721
path: `/me/oauth/consent/${encodeURIComponent(oauthClientId)}`,
18-
search: scope !== undefined ? { scope } : undefined,
22+
search: Object.keys(search).length > 0 ? search : undefined,
1923
},
2024
{ skipUpdateClient: true },
2125
);
@@ -31,6 +35,7 @@ export class OAuthApplication implements OAuthApplicationNamespace {
3135
oauthApplicationUrl: data.oauth_application_url,
3236
clientId: data.client_id,
3337
state: data.state,
38+
redirectDomain: data.redirect_domain,
3439
scopes:
3540
data.scopes?.map(s => ({
3641
scope: s.scope,
Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
import { useMemo } from 'react';
22

3-
import type { GetOAuthConsentInfoParams } from '../../types';
43
import { STABLE_KEYS } from '../stable-keys';
54
import { createCacheKeys } from './createCacheKeys';
65

7-
export function useOAuthConsentCacheKeys(params: { userId: string | null; oauthClientId: string; scope?: string }) {
8-
const { userId, oauthClientId, scope } = params;
6+
export function useOAuthConsentCacheKeys(params: {
7+
userId: string | null;
8+
oauthClientId: string;
9+
scope?: string;
10+
redirectUri?: string;
11+
}) {
12+
const { userId, oauthClientId, scope, redirectUri } = params;
913
return useMemo(() => {
10-
const args: Pick<GetOAuthConsentInfoParams, 'oauthClientId'> & { scope?: string } = { oauthClientId };
11-
if (scope !== undefined) {
12-
args.scope = scope;
13-
}
14+
const args = {
15+
oauthClientId,
16+
...(scope !== undefined && { scope }),
17+
...(redirectUri !== undefined && { redirectUri }),
18+
};
1419
return createCacheKeys({
1520
stablePrefix: STABLE_KEYS.OAUTH_CONSENT_INFO_KEY,
1621
authenticated: true,
@@ -21,5 +26,5 @@ export function useOAuthConsentCacheKeys(params: { userId: string | null; oauthC
2126
args,
2227
},
2328
});
24-
}, [userId, oauthClientId, scope]);
29+
}, [userId, oauthClientId, scope, redirectUri]);
2530
}

packages/shared/src/react/hooks/useOAuthConsent.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const HOOK_NAME = 'useOAuthConsent';
2727
export function useOAuthConsent(params: UseOAuthConsentParams): UseOAuthConsentReturn {
2828
useAssertWrappedByClerkProvider(HOOK_NAME);
2929

30-
const { oauthClientId: oauthClientIdParam, scope, keepPreviousData = true, enabled = true } = params;
30+
const { oauthClientId: oauthClientIdParam, scope, redirectUri, keepPreviousData = true, enabled = true } = params;
3131
const clerk = useClerkInstanceContext();
3232
const user = useUserBase();
3333

@@ -39,14 +39,15 @@ export function useOAuthConsent(params: UseOAuthConsentParams): UseOAuthConsentR
3939
userId: user?.id ?? null,
4040
oauthClientId,
4141
scope,
42+
redirectUri,
4243
});
4344

4445
const hasClientId = oauthClientId.length > 0;
4546
const queryEnabled = Boolean(user) && hasClientId && enabled && clerk.loaded;
4647

4748
const query = useClerkQuery({
4849
queryKey,
49-
queryFn: () => fetchConsentInfo(clerk, { oauthClientId, scope }),
50+
queryFn: () => fetchConsentInfo(clerk, { oauthClientId, scope, redirectUri }),
5051
enabled: queryEnabled,
5152
placeholderData: defineKeepPreviousDataFn(keepPreviousData && queryEnabled),
5253
});
@@ -59,7 +60,6 @@ export function useOAuthConsent(params: UseOAuthConsentParams): UseOAuthConsentR
5960
};
6061
}
6162

62-
function fetchConsentInfo(clerk: LoadedClerk, params: { oauthClientId: string; scope?: string }) {
63-
const { oauthClientId, scope } = params;
64-
return clerk.oauthApplication.getConsentInfo(scope !== undefined ? { oauthClientId, scope } : { oauthClientId });
63+
function fetchConsentInfo(clerk: LoadedClerk, params: { oauthClientId: string; scope?: string; redirectUri?: string }) {
64+
return clerk.oauthApplication.getConsentInfo(params);
6565
}

packages/shared/src/react/hooks/useOAuthConsent.types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { GetOAuthConsentInfoParams, OAuthConsentInfo } from '../../types';
44
/**
55
* @interface
66
*/
7-
export type UseOAuthConsentParams = Pick<GetOAuthConsentInfoParams, 'oauthClientId' | 'scope'> & {
7+
export type UseOAuthConsentParams = Pick<GetOAuthConsentInfoParams, 'oauthClientId' | 'scope' | 'redirectUri'> & {
88
/**
99
* If `true`, the previous data will be kept in the cache until new data is fetched.
1010
*

packages/shared/src/types/oauthApplication.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export interface OAuthConsentInfoJSON extends ClerkResourceJSON {
1919
oauth_application_url: string;
2020
client_id: string;
2121
state: string;
22+
redirect_domain: string | null;
2223
scopes: OAuthConsentScopeJSON[];
2324
}
2425

@@ -68,6 +69,12 @@ export type OAuthConsentInfo = {
6869
* The `state` parameter from the original authorize request.
6970
*/
7071
state: string;
72+
/**
73+
* The PSL-resolved registrable domain of the redirect URI for display on the consent screen.
74+
* Null when no redirect URI was provided, when it is not registered for the application,
75+
* or when it points to an IP address or localhost.
76+
*/
77+
redirectDomain: string | null;
7178
/**
7279
* A list of scopes the application is requesting, with descriptions and consent requirements.
7380
*/
@@ -79,6 +86,8 @@ export type GetOAuthConsentInfoParams = {
7986
oauthClientId: string;
8087
/** A space-delimited scope string from the authorize request. */
8188
scope?: string;
89+
/** The redirect URI from the authorize request. When provided, the backend returns a PSL-resolved `redirectDomain`. */
90+
redirectUri?: string;
8291
};
8392

8493
/**

packages/ui/src/components/OAuthConsent/OAuthConsent.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,11 @@ function _OAuthConsent() {
5050

5151
// Public path: fetch via hook. Disabled on the accounts portal path
5252
// (which already has all data via context) to avoid a wasted FAPI request.
53+
const redirectUri = ctx.redirectUrl ?? getRedirectUriFromSearch();
5354
const { data, isLoading, error } = useOAuthConsent({
5455
oauthClientId,
5556
scope,
57+
redirectUri: redirectUri || undefined,
5658
// TODO: Remove this once account portal is refactored to use this component
5759
enabled: !hasContextCallbacks,
5860
});
@@ -69,7 +71,7 @@ function _OAuthConsent() {
6971
const oauthApplicationName = ctx.oauthApplicationName ?? data?.oauthApplicationName ?? '';
7072
const oauthApplicationLogoUrl = ctx.oauthApplicationLogoUrl ?? data?.oauthApplicationLogoUrl;
7173
const oauthApplicationUrl = ctx.oauthApplicationUrl ?? data?.oauthApplicationUrl;
72-
const redirectUrl = ctx.redirectUrl ?? getRedirectUriFromSearch();
74+
const redirectUrl = ctx.redirectUrl ?? redirectUri;
7375

7476
const hasOrgReadScope = scopes.some(s => s.scope === USER_ORG_READ_SCOPE);
7577
const orgSelectionEnabled = !!(hasOrgReadScope && organizationSettings.enabled);
@@ -85,7 +87,7 @@ function _OAuthConsent() {
8587
const effectiveOrg = selectedOrg ?? defaultOrg;
8688

8789
const { t } = useLocalizations();
88-
const domainAction = getRedirectDisplay(redirectUrl);
90+
const domainAction = data?.redirectDomain ?? getRedirectDisplay(redirectUrl);
8991
const viewFullUrlText = t(localizationKeys('oauthConsent.viewFullUrl'));
9092

9193
// Error states only apply to the public flow.

packages/ui/src/components/OAuthConsent/__tests__/OAuthConsent.test.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const fakeConsentInfo = {
1313
oauthApplicationUrl: 'https://example.com',
1414
clientId: 'client_test',
1515
state: 'abc',
16+
redirectDomain: 'example.com',
1617
scopes: [
1718
{ scope: 'openid', description: 'View your identity', requiresConsent: true },
1819
{ scope: 'email', description: 'Access your email address', requiresConsent: true },
@@ -94,6 +95,7 @@ describe('OAuthConsent', () => {
9495

9596
expect(getConsentInfo).toHaveBeenCalledWith({
9697
oauthClientId: 'client_test',
98+
redirectUri: 'https://app.example/callback',
9799
});
98100
});
99101

@@ -204,6 +206,7 @@ describe('OAuthConsent', () => {
204206
expect(getConsentInfo).toHaveBeenCalledWith({
205207
oauthClientId: 'override_id',
206208
scope: 'openid email',
209+
redirectUri: 'https://app.example/callback',
207210
});
208211
});
209212
});

0 commit comments

Comments
 (0)