Skip to content

feat(clerk-js): Added integrity attribute support #4918

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/shaggy-students-jog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/nextjs': minor
'@clerk/shared': minor
'@clerk/clerk-react': minor
---

Added ability to pass `integrity` attribute from provider to Script
3 changes: 2 additions & 1 deletion packages/nextjs/src/utils/clerk-js-script.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();

/**
Expand All @@ -28,6 +28,7 @@ function ClerkJSScript(props: ClerkJSScriptProps) {
clerkJSVersion,
clerkJSVariant,
nonce,
integrity,
};
const scriptUrl = clerkJsScriptUrl(options);

Expand Down
6 changes: 6 additions & 0 deletions packages/react/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ export type IsomorphicClerkOptions = Without<ClerkOptions, 'isSatellite'> & {
* 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;

/**
Expand Down
33 changes: 33 additions & 0 deletions packages/shared/src/__tests__/loadClerkJsScript.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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()', () => {
Expand Down Expand Up @@ -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);
Expand Down
6 changes: 6 additions & 0 deletions packages/shared/src/loadClerkJsScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type LoadClerkJsScriptOptions = Without<ClerkOptions, 'isSatellite'> & {
proxyUrl?: string;
domain?: string;
nonce?: string;
integrity?: string;
};

/**
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -128,6 +130,10 @@ const buildClerkJsScriptAttributes = (options: LoadClerkJsScriptOptions) => {
obj.nonce = options.nonce;
}

if (options.integrity) {
obj.integrity = options.integrity;
}

return obj;
};

Expand Down
7 changes: 6 additions & 1 deletion packages/shared/src/loadScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLScriptElement> {
const { async, defer, beforeLoad, crossOrigin, nonce } = opts || {};
const { async, defer, beforeLoad, crossOrigin, nonce, integrity } = opts || {};

const load = () => {
return new Promise<HTMLScriptElement>((resolve, reject) => {
Expand All @@ -26,7 +27,11 @@ export async function loadScript(src = '', opts: LoadScriptOptions): Promise<HTM

const script = document.createElement('script');



if (crossOrigin) script.setAttribute('crossorigin', crossOrigin);
if (integrity) script.setAttribute('integrity', integrity);

script.async = async || false;
script.defer = defer || false;

Expand Down