diff --git a/.changeset/shaggy-students-jog.md b/.changeset/shaggy-students-jog.md new file mode 100644 index 00000000000..738978137b4 --- /dev/null +++ b/.changeset/shaggy-students-jog.md @@ -0,0 +1,7 @@ +--- +'@clerk/nextjs': minor +'@clerk/shared': minor +'@clerk/clerk-react': minor +--- + +Added ability to pass `integrity` attribute from provider to Script diff --git a/packages/nextjs/src/utils/clerk-js-script.tsx b/packages/nextjs/src/utils/clerk-js-script.tsx index cc39f3534e6..f5a707f946e 100644 --- a/packages/nextjs/src/utils/clerk-js-script.tsx +++ b/packages/nextjs/src/utils/clerk-js-script.tsx @@ -10,7 +10,7 @@ type ClerkJSScriptProps = { }; function ClerkJSScript(props: ClerkJSScriptProps) { - const { publishableKey, clerkJSUrl, clerkJSVersion, clerkJSVariant, nonce } = useClerkNextOptions(); + const { publishableKey, clerkJSUrl, clerkJSVersion, clerkJSVariant, nonce, integrity } = useClerkNextOptions(); const { domain, proxyUrl } = useClerk(); /** @@ -28,6 +28,7 @@ function ClerkJSScript(props: ClerkJSScriptProps) { clerkJSVersion, clerkJSVariant, nonce, + integrity, }; const scriptUrl = clerkJsScriptUrl(options); diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts index b24a28a4df0..e0461dc8abe 100644 --- a/packages/react/src/types.ts +++ b/packages/react/src/types.ts @@ -43,6 +43,12 @@ export type IsomorphicClerkOptions = Without & { * This nonce value will be passed through to the `@clerk/clerk-js` script tag. Use it to implement a [strict-dynamic CSP](https://clerk.com/docs/security/clerk-csp#implementing-a-strict-dynamic-csp). Requires the `dynamic` prop to also be set. */ nonce?: string; + /** + * This integrity value will be passed through to the `@clerk/clerk-js` script tag. Should only by used in conjunction with `clerkJSVersion. + * Hashes can be generated with tools such as [https://www.srihash.org/](https://www.srihash.org/) + * @note You can use this to implement [Subresource Integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) + */ + integrity?: string; } & MultiDomainAndOrProxy; /** diff --git a/packages/shared/src/__tests__/loadClerkJsScript.test.ts b/packages/shared/src/__tests__/loadClerkJsScript.test.ts index 2e259e10292..28168a7f19a 100644 --- a/packages/shared/src/__tests__/loadClerkJsScript.test.ts +++ b/packages/shared/src/__tests__/loadClerkJsScript.test.ts @@ -12,6 +12,9 @@ jest.mock('../loadScript'); setClerkJsLoadingErrorPackageName('@clerk/clerk-react'); const jsPackageMajorVersion = getMajorVersion(JS_PACKAGE_VERSION); +const fakeNonce = 'fakeNonce123'; +const fakeSRIHash = 'fakeSRIHash456'; + describe('loadClerkJsScript(options)', () => { const mockPublishableKey = 'pk_test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk'; @@ -71,6 +74,27 @@ describe('loadClerkJsScript(options)', () => { 'Clerk: Failed to load Clerk', ); }); + + test('loads script with nonce and integrity attributes', async () => { + await loadClerkJsScript({ + publishableKey: mockPublishableKey, + nonce: fakeNonce, + integrity: fakeSRIHash, + }); + + expect(loadScript).toHaveBeenCalledWith( + expect.stringContaining( + `https://foo-bar-13.clerk.accounts.dev/npm/@clerk/clerk-js@${jsPackageMajorVersion}/dist/clerk.browser.js`, + ), + expect.objectContaining({ + async: true, + crossOrigin: 'anonymous', + nonce: fakeNonce, + integrity: fakeSRIHash, + beforeLoad: expect.any(Function), + }), + ); + }); }); describe('clerkJsScriptUrl()', () => { @@ -136,6 +160,15 @@ describe('buildClerkJsScriptAttributes()', () => { { 'data-clerk-publishable-key': mockPublishableKey, 'data-clerk-proxy-url': mockProxyUrl }, ], ['no options', {}, {}], + [ + 'with nonce and integrity', + { publishableKey: mockPublishableKey, nonce: fakeNonce, integrity: fakeSRIHash }, + { + 'data-clerk-publishable-key': mockPublishableKey, + nonce: fakeNonce, + integrity: fakeSRIHash, + }, + ], ])('returns correct attributes with %s', (_, input, expected) => { // @ts-ignore input loses correct type because of empty object expect(buildClerkJsScriptAttributes(input)).toEqual(expected); diff --git a/packages/shared/src/loadClerkJsScript.ts b/packages/shared/src/loadClerkJsScript.ts index 97647e3f0bd..42de099e613 100644 --- a/packages/shared/src/loadClerkJsScript.ts +++ b/packages/shared/src/loadClerkJsScript.ts @@ -32,6 +32,7 @@ type LoadClerkJsScriptOptions = Without & { proxyUrl?: string; domain?: string; nonce?: string; + integrity?: string; }; /** @@ -71,6 +72,7 @@ const loadClerkJsScript = async (opts?: LoadClerkJsScriptOptions) => { async: true, crossOrigin: 'anonymous', nonce: opts.nonce, + integrity: opts.integrity, beforeLoad: applyClerkJsScriptAttributes(opts), }).catch(() => { throw new Error(FAILED_TO_LOAD_ERROR); @@ -128,6 +130,10 @@ const buildClerkJsScriptAttributes = (options: LoadClerkJsScriptOptions) => { obj.nonce = options.nonce; } + if (options.integrity) { + obj.integrity = options.integrity; + } + return obj; }; diff --git a/packages/shared/src/loadScript.ts b/packages/shared/src/loadScript.ts index fd8b9077e1e..579115e9179 100644 --- a/packages/shared/src/loadScript.ts +++ b/packages/shared/src/loadScript.ts @@ -8,11 +8,12 @@ type LoadScriptOptions = { defer?: boolean; crossOrigin?: 'anonymous' | 'use-credentials'; nonce?: string; + integrity?: string; beforeLoad?: (script: HTMLScriptElement) => void; }; export async function loadScript(src = '', opts: LoadScriptOptions): Promise { - const { async, defer, beforeLoad, crossOrigin, nonce } = opts || {}; + const { async, defer, beforeLoad, crossOrigin, nonce, integrity } = opts || {}; const load = () => { return new Promise((resolve, reject) => { @@ -26,7 +27,11 @@ export async function loadScript(src = '', opts: LoadScriptOptions): Promise