Skip to content

Commit 0e0cc1f

Browse files
authored
feat(shared): Enhance publishable key validation (#6266)
1 parent 50fc006 commit 0e0cc1f

File tree

4 files changed

+223
-23
lines changed

4 files changed

+223
-23
lines changed

.changeset/smooth-papayas-try.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/shared': patch
3+
---
4+
5+
Enhancing publishable key parsing and validation logic to validate expected format

.typedoc/__tests__/__snapshots__/file-structure.test.ts.snap

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,11 @@ exports[`Typedoc output > should have a deliberate file structure 1`] = `
7676
"types/without.mdx",
7777
"shared/api-url-from-publishable-key.mdx",
7878
"shared/build-clerk-js-script-attributes.mdx",
79+
"shared/build-publishable-key.mdx",
7980
"shared/camel-to-snake.mdx",
8081
"shared/clerk-js-script-url.mdx",
8182
"shared/clerk-runtime-error.mdx",
83+
"shared/create-dev-or-staging-url-cache.mdx",
8284
"shared/create-path-matcher.mdx",
8385
"shared/deep-camel-to-snake.mdx",
8486
"shared/deep-snake-to-camel.mdx",
@@ -87,14 +89,20 @@ exports[`Typedoc output > should have a deliberate file structure 1`] = `
8789
"shared/extract-dev-browser-jwt-from-url.mdx",
8890
"shared/fast-deep-merge-and-replace.mdx",
8991
"shared/get-clerk-js-major-version-or-tag.mdx",
92+
"shared/get-cookie-suffix.mdx",
9093
"shared/get-env-variable.mdx",
9194
"shared/get-non-undefined-values.mdx",
9295
"shared/get-script-url.mdx",
96+
"shared/get-suffixed-cookie-name.mdx",
9397
"shared/icon-image-url.mdx",
9498
"shared/in-browser.mdx",
9599
"shared/is-browser-online.mdx",
96100
"shared/is-clerk-runtime-error.mdx",
101+
"shared/is-development-from-publishable-key.mdx",
102+
"shared/is-development-from-secret-key.mdx",
97103
"shared/is-ipv4-address.mdx",
104+
"shared/is-production-from-publishable-key.mdx",
105+
"shared/is-production-from-secret-key.mdx",
98106
"shared/is-publishable-key.mdx",
99107
"shared/is-staging.mdx",
100108
"shared/is-truthy.mdx",
@@ -105,6 +113,7 @@ exports[`Typedoc output > should have a deliberate file structure 1`] = `
105113
"shared/pages-or-infinite-options.mdx",
106114
"shared/paginated-hook-config.mdx",
107115
"shared/paginated-resources.mdx",
116+
"shared/parse-publishable-key.mdx",
108117
"shared/read-json-file.mdx",
109118
"shared/set-clerk-js-loading-error-package-name.mdx",
110119
"shared/snake-to-camel.mdx",

packages/shared/src/__tests__/keys.test.ts

Lines changed: 68 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212

1313
describe('buildPublishableKey(frontendApi)', () => {
1414
const cases = [
15-
['example.clerk.accounts.dev', 'pk_live_ZXhhbXBsZS5jbGVyay5hY2NvdW50cy5kZXYk'],
15+
['fake-clerk-test.clerk.accounts.dev', 'pk_live_ZmFrZS1jbGVyay10ZXN0LmNsZXJrLmFjY291bnRzLmRldiQ='],
1616
['foo-bar-13.clerk.accounts.dev', 'pk_test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk'],
1717
['clerk.boring.sawfly-91.lcl.dev', 'pk_test_Y2xlcmsuYm9yaW5nLnNhd2ZseS05MS5sY2wuZGV2JA=='],
1818
['clerk.boring.sawfly-91.lclclerk.com', 'pk_test_Y2xlcmsuYm9yaW5nLnNhd2ZseS05MS5sY2xjbGVyay5jb20k'],
@@ -34,12 +34,8 @@ describe('parsePublishableKey(key)', () => {
3434
['', null],
3535
['whatever', null],
3636
[
37-
'pk_live_ZXhhbXBsZS5jbGVyay5hY2NvdW50cy5kZXYk',
38-
{ instanceType: 'production', frontendApi: 'example.clerk.accounts.dev' },
39-
],
40-
[
41-
'pk_test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk',
42-
{ instanceType: 'development', frontendApi: 'foo-bar-13.clerk.accounts.dev' },
37+
'pk_live_ZmFrZS1jbGVyay10ZXN0LmNsZXJrLmFjY291bnRzLmRldiQ=',
38+
{ instanceType: 'production', frontendApi: 'fake-clerk-test.clerk.accounts.dev' },
4339
],
4440
[
4541
'pk_test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk',
@@ -54,6 +50,30 @@ describe('parsePublishableKey(key)', () => {
5450
expect(result).toEqual(expectedPublishableKey);
5551
});
5652

53+
it('returns null for keys with extra characters after $', () => {
54+
expect(parsePublishableKey('pk_live_ZmFrZS1jbGVyay1tYWxmb3JtZWQuY2xlcmsuYWNjb3VudHMuZGV2JGV4dHJh')).toBeNull();
55+
});
56+
57+
it('throws an error for keys with extra characters after $ when fatal: true', () => {
58+
expect(() =>
59+
parsePublishableKey('pk_live_ZmFrZS1jbGVyay1tYWxmb3JtZWQuY2xlcmsuYWNjb3VudHMuZGV2JGV4dHJh', { fatal: true }),
60+
).toThrowError('Publishable key not valid.');
61+
});
62+
63+
it('returns null for keys with multiple $ characters', () => {
64+
expect(parsePublishableKey('pk_live_ZmFrZS1jbGVyay1tdWx0aXBsZS5jbGVyay5hY2NvdW50cy5kZXYkJA==')).toBeNull();
65+
});
66+
67+
it('returns null for keys without proper domain format', () => {
68+
expect(parsePublishableKey('pk_live_aW52YWxpZGtleSQ=')).toBeNull();
69+
});
70+
71+
it('throws an error if the key cannot be decoded when fatal: true', () => {
72+
expect(() => parsePublishableKey('pk_live_invalid!@#$', { fatal: true })).toThrowError(
73+
'Publishable key not valid.',
74+
);
75+
});
76+
5777
it('throws an error if the key is not a valid publishable key, when fatal: true', () => {
5878
expect(() => parsePublishableKey('fake_pk', { fatal: true })).toThrowError('Publishable key not valid.');
5979
});
@@ -65,15 +85,22 @@ describe('parsePublishableKey(key)', () => {
6585
});
6686

6787
it('applies the proxyUrl if provided', () => {
68-
expect(parsePublishableKey('pk_live_Y2xlcmsuY2xlcmsuZGV2JA==', { proxyUrl: 'example.com/__clerk' })).toEqual({
88+
expect(
89+
parsePublishableKey('pk_live_ZmFrZS1jbGVyay10ZXN0LmNsZXJrLmFjY291bnRzLmRldiQ=', {
90+
proxyUrl: 'example.com/__clerk',
91+
}),
92+
).toEqual({
6993
frontendApi: 'example.com/__clerk',
7094
instanceType: 'production',
7195
});
7296
});
7397

7498
it('applies the domain if provided for production keys and isSatellite is true', () => {
7599
expect(
76-
parsePublishableKey('pk_live_Y2xlcmsuY2xlcmsuZGV2JA==', { domain: 'example.com', isSatellite: true }),
100+
parsePublishableKey('pk_live_ZmFrZS1jbGVyay10ZXN0LmNsZXJrLmFjY291bnRzLmRldiQ=', {
101+
domain: 'example.com',
102+
isSatellite: true,
103+
}),
77104
).toEqual({
78105
frontendApi: 'clerk.example.com',
79106
instanceType: 'production',
@@ -82,9 +109,12 @@ describe('parsePublishableKey(key)', () => {
82109

83110
it('ignores domain for production keys when isSatellite is false', () => {
84111
expect(
85-
parsePublishableKey('pk_live_Y2xlcmsuY2xlcmsuZGV2JA==', { domain: 'example.com', isSatellite: false }),
112+
parsePublishableKey('pk_live_ZmFrZS1jbGVyay10ZXN0LmNsZXJrLmFjY291bnRzLmRldiQ=', {
113+
domain: 'example.com',
114+
isSatellite: false,
115+
}),
86116
).toEqual({
87-
frontendApi: 'clerk.clerk.dev',
117+
frontendApi: 'fake-clerk-test.clerk.accounts.dev',
88118
instanceType: 'production',
89119
});
90120
});
@@ -101,12 +131,33 @@ describe('parsePublishableKey(key)', () => {
101131

102132
describe('isPublishableKey(key)', () => {
103133
it('returns true if the key is a valid publishable key', () => {
104-
expect(isPublishableKey('pk_live_Y2xlcmsuY2xlcmsuZGV2JA==')).toBe(true);
134+
expect(isPublishableKey('pk_live_ZmFrZS1jbGVyay10ZXN0LmNsZXJrLmFjY291bnRzLmRldiQ=')).toBe(true);
135+
expect(isPublishableKey('pk_test_Y2xlcmsuY2xlcmsuZGV2JA==')).toBe(true);
105136
});
106137

107138
it('returns false if the key is not a valid publishable key', () => {
108139
expect(isPublishableKey('clerk.clerk.com')).toBe(false);
109140
});
141+
142+
it('returns false if the key has invalid structure', () => {
143+
expect(isPublishableKey('pk_live')).toBe(false);
144+
expect(isPublishableKey('pk_live_')).toBe(false);
145+
expect(isPublishableKey('pk_live_invalid')).toBe(false);
146+
});
147+
148+
it('returns false if the decoded key has extra characters after $', () => {
149+
expect(isPublishableKey('pk_live_ZmFrZS1jbGVyay1tYWxmb3JtZWQuY2xlcmsuYWNjb3VudHMuZGV2JGV4dHJh')).toBe(false);
150+
expect(isPublishableKey('pk_test_Y2xlcmsuY2xlcmsuZGV2JGV4dHJh')).toBe(false);
151+
});
152+
153+
it('returns false if the decoded key has multiple $ characters', () => {
154+
expect(isPublishableKey('pk_live_ZmFrZS1jbGVyay1tdWx0aXBsZS5jbGVyay5hY2NvdW50cy5kZXYkJA==')).toBe(false);
155+
expect(isPublishableKey('pk_live_JGZha2UtY2xlcmstcHJlZml4LmNsZXJrLmFjY291bnRzLmRldiQ=')).toBe(false);
156+
});
157+
158+
it('returns false if the decoded key does not look like a domain', () => {
159+
expect(isPublishableKey('pk_live_aW52YWxpZGtleSQ=')).toBe(false);
160+
});
110161
});
111162

112163
describe('isDevOrStagingUrl(url)', () => {
@@ -138,9 +189,9 @@ describe('isDevOrStagingUrl(url)', () => {
138189

139190
describe('isDevelopmentFromPublishableKey(key)', () => {
140191
const cases: Array<[string, boolean]> = [
141-
['pk_live_ZXhhbXBsZS5jbGVyay5hY2NvdW50cy5kZXYk', false],
192+
['pk_live_ZmFrZS1jbGVyay10ZXN0LmNsZXJrLmFjY291bnRzLmRldiQ=', false],
142193
['pk_test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk', true],
143-
['live_ZXhhbXBsZS5jbGVyay5hY2NvdW50cy5kZXYk', false],
194+
['live_ZmFrZS1jbGVyay10ZXN0LmNsZXJrLmFjY291bnRzLmRldiQ=', false],
144195
['test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk', true],
145196
];
146197

@@ -152,9 +203,9 @@ describe('isDevelopmentFromPublishableKey(key)', () => {
152203

153204
describe('isProductionFromPublishableKey(key)', () => {
154205
const cases: Array<[string, boolean]> = [
155-
['pk_live_ZXhhbXBsZS5jbGVyay5hY2NvdW50cy5kZXYk', true],
206+
['pk_live_ZmFrZS1jbGVyay10ZXN0LmNsZXJrLmFjY291bnRzLmRldiQ=', true],
156207
['pk_test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk', false],
157-
['live_ZXhhbXBsZS5jbGVyay5hY2NvdW50cy5kZXYk', true],
208+
['live_ZmFrZS1jbGVyay10ZXN0LmNsZXJrLmFjY291bnRzLmRldiQ=', true],
158209
['test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk', false],
159210
];
160211

@@ -194,7 +245,7 @@ describe('isProductionFromSecretKey(key)', () => {
194245

195246
describe('getCookieSuffix(publishableKey, subtle?)', () => {
196247
const cases: Array<[string, string]> = [
197-
['pk_live_Y2xlcmsuY2xlcmsuZGV2JA', '1Z8AzTQD'],
248+
['pk_live_ZmFrZS1jbGVyay10ZXN0LmNsZXJrLmFjY291bnRzLmRldiQ=', 'qReyu04C'],
198249
['pk_test_Y2xlcmsuY2xlcmsuZGV2JA', 'QvfNY2dr'],
199250
];
200251

0 commit comments

Comments
 (0)