Skip to content

Commit 4e5e7f4

Browse files
authoredDec 3, 2024··
feat(clerk-js): Add allowedRedirectProtocols (#4705)
1 parent 1c51045 commit 4e5e7f4

File tree

5 files changed

+53
-15
lines changed

5 files changed

+53
-15
lines changed
 

‎.changeset/blue-teachers-remember.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@clerk/clerk-js': minor
3+
'@clerk/types': minor
4+
---
5+
6+
Introduce a new `allowedRedirectProtocols` option to pass additional allowed protocols for user-provided redirect validation.

‎packages/clerk-js/src/core/__tests__/clerk.test.ts

+20-3
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ describe('Clerk singleton', () => {
4848
const productionPublishableKey = 'pk_live_Y2xlcmsuYWJjZWYuMTIzNDUucHJvZC5sY2xjbGVyay5jb20k';
4949

5050
const mockNavigate = jest.fn((to: string) => Promise.resolve(to));
51-
const mockedLoadOptions = { routerPush: mockNavigate, routerReplace: mockNavigate };
51+
const mockedLoadOptions = { routerDebug: true, routerPush: mockNavigate, routerReplace: mockNavigate };
5252

5353
const mockDisplayConfig = {
5454
signInUrl: 'http://test.host/sign-in',
@@ -686,7 +686,6 @@ describe('Clerk singleton', () => {
686686
const toUrl = 'https://www.origindifferent.com/';
687687
await sut.navigate(toUrl);
688688
expect(mockHref).toHaveBeenCalledWith(toUrl);
689-
expect(logSpy).not.toHaveBeenCalled();
690689
});
691690

692691
it('wraps custom navigate method in a promise if provided and it sync', async () => {
@@ -696,7 +695,6 @@ describe('Clerk singleton', () => {
696695
expect(res.then).toBeDefined();
697696
expect(mockHref).not.toHaveBeenCalled();
698697
expect(mockNavigate.mock.calls[0][0]).toBe('/path#hash');
699-
expect(logSpy).not.toHaveBeenCalled();
700698
});
701699

702700
it('logs navigation external navigation when routerDebug is enabled', async () => {
@@ -720,6 +718,25 @@ describe('Clerk singleton', () => {
720718
expect(logSpy).toHaveBeenCalledTimes(1);
721719
expect(logSpy).toHaveBeenCalledWith(`Clerk is navigating to: ${toUrl}`);
722720
});
721+
722+
it('validates the protocol of the provided URL', async () => {
723+
await sut.load({ ...mockedLoadOptions, allowedRedirectProtocols: ['gg:'] });
724+
// allowed protocol
725+
const toUrl = 'gg://some/deeply/nested/path';
726+
await sut.navigate(toUrl);
727+
expect(mockNavigate.mock.calls[0][0]).toBe(toUrl);
728+
expect(logSpy).toHaveBeenCalledTimes(1);
729+
expect(logSpy).toHaveBeenCalledWith(`Clerk is navigating to: ${toUrl}`);
730+
731+
mockNavigate.mockReset();
732+
logSpy.mockReset();
733+
734+
// disallowed protocol
735+
const badUrl = 'evil://some/deeply/nested/path';
736+
await sut.navigate(badUrl);
737+
expect(mockNavigate.mock.calls[0][0]).toBe('/');
738+
expect(logSpy).toHaveBeenCalledTimes(1);
739+
});
723740
});
724741

725742
describe('.handleRedirectCallback()', () => {

‎packages/clerk-js/src/core/clerk.ts

+13-2
Original file line numberDiff line numberDiff line change
@@ -960,7 +960,7 @@ export class Clerk implements ClerkInterface {
960960

961961
let toURL = new URL(to, window.location.href);
962962

963-
if (!ALLOWED_PROTOCOLS.includes(toURL.protocol)) {
963+
if (!this.#allowedRedirectProtocols.includes(toURL.protocol)) {
964964
console.warn(
965965
`Clerk: "${toURL.protocol}" is not a valid protocol. Redirecting to "/" instead. If you think this is a mistake, please open an issue.`,
966966
);
@@ -974,7 +974,8 @@ export class Clerk implements ClerkInterface {
974974
console.log(`Clerk is navigating to: ${toURL}`);
975975
}
976976

977-
if (toURL.origin !== window.location.origin || !customNavigate) {
977+
// Custom protocol URLs have an origin value of 'null'. In many cases, this indicates deep-linking and we want to ensure the customNavigate function is used if available.
978+
if ((toURL.origin !== 'null' && toURL.origin !== window.location.origin) || !customNavigate) {
978979
windowNavigate(toURL);
979980
return;
980981
}
@@ -2111,4 +2112,14 @@ export class Clerk implements ClerkInterface {
21112112
// ignore
21122113
}
21132114
};
2115+
2116+
get #allowedRedirectProtocols() {
2117+
let allowedProtocols = ALLOWED_PROTOCOLS;
2118+
2119+
if (this.#options.allowedRedirectProtocols) {
2120+
allowedProtocols = allowedProtocols.concat(this.#options.allowedRedirectProtocols);
2121+
}
2122+
2123+
return allowedProtocols;
2124+
}
21142125
}
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
export const CLERK_BEFORE_UNLOAD_EVENT = 'clerk:beforeunload';
22

3+
/**
4+
* Additional protocols can be provided using the `allowedRedirectProtocols` Clerk option.
5+
*/
36
export const ALLOWED_PROTOCOLS = [
47
'http:',
58
'https:',
@@ -8,16 +11,13 @@ export const ALLOWED_PROTOCOLS = [
811
'chrome-extension:',
912
];
1013

14+
/**
15+
* Helper utility to navigate via window.location.href. Also dispatches a clerk:beforeunload custom event.
16+
*
17+
* Note that this utility should **never** be called with a user-provided URL. We make no specific checks against the contents of the URL here and assume it is safe. Use `Clerk.navigate()` instead for user-provided URLs.
18+
*/
1119
export function windowNavigate(to: URL | string): void {
12-
let toURL = new URL(to, window.location.href);
13-
14-
if (!ALLOWED_PROTOCOLS.includes(toURL.protocol)) {
15-
console.warn(
16-
`Clerk: "${toURL.protocol}" is not a valid protocol. Redirecting to "/" instead. If you think this is a mistake, please open an issue.`,
17-
);
18-
toURL = new URL('/', window.location.href);
19-
}
20-
20+
const toURL = new URL(to, window.location.href);
2121
window.dispatchEvent(new CustomEvent(CLERK_BEFORE_UNLOAD_EVENT));
2222
window.location.href = toURL.href;
2323
}

‎packages/types/src/clerk.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -711,9 +711,13 @@ export type ClerkOptions = ClerkOptionsNavigation &
711711
/** This URL will be used for any redirects that might happen and needs to point to your primary application on the client-side. This option is optional for production instances and required for development instances. */
712712
signUpUrl?: string;
713713
/**
714-
* Optional array of domains used to validate against the query param of an auth redirect. If no match is made, the redirect is considered unsafe and the default redirect will be used with a warning passed to the console.
714+
* An optional array of domains to validate user-provided redirect URLs against. If no match is made, the redirect is considered unsafe and the default redirect will be used with a warning logged in the console.
715715
*/
716716
allowedRedirectOrigins?: Array<string | RegExp>;
717+
/**
718+
* An optional array of protocols to validate user-provided redirect URLs against. If no match is made, the redirect is considered unsafe and the default redirect will be used with a warning logged in the console.
719+
*/
720+
allowedRedirectProtocols?: Array<string>;
717721
/**
718722
* This option defines that the application is a satellite application.
719723
*/

0 commit comments

Comments
 (0)
Please sign in to comment.