Skip to content

feat(clerk-js,chrome-extension,shared): Expand WebSSO capabilities (Content Scripts) [SDK-836] #2249

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

Closed
Closed
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
29 changes: 29 additions & 0 deletions .changeset/shiny-glasses-switch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
'@clerk/chrome-extension': major
'@clerk/clerk-js': minor
'@clerk/shared': minor
---

Expand the ability for `@clerk/chrome-extension` WebSSO to sync with host applications which use URL-based session syncing.

### How to Update

**WebSSO Local Host Permissions:**

Add the following to the top-level `content_scripts` array key in your `manifest.json` file:
```json
{
"matches": ["*://localhost/*"], // URL of your host application
"js": ["src/content.tsx"] // Path to your content script
}
```

**Content Script:**

In order to sync with your host application, you must add the following to your content script to the path specified in the `manifest.json` file above:

```ts
import { ContentScript } from '@clerk/chrome-extension';

ContentScript.init(process.env.CLERK_PUBLISHABLE_KEY || "");
```
33 changes: 7 additions & 26 deletions packages/chrome-extension/src/ClerkProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import type { ClerkProp, ClerkProviderProps as ClerkReactProviderProps } from '@
import { __internal__setErrorThrowerOptions, ClerkProvider as ClerkReactProvider } from '@clerk/clerk-react';
import React from 'react';

import type { TokenCache } from './cache';
import { ChromeStorageCache } from './cache';
import { buildClerk } from './singleton';

Clerk.sdkMetadata = {
Expand All @@ -16,30 +14,21 @@ __internal__setErrorThrowerOptions({
packageName: '@clerk/chrome-extension',
});

type WebSSOClerkProviderCustomProps =
| {
syncSessionWithTab?: false;
tokenCache?: never;
}
| {
syncSessionWithTab: true;
tokenCache?: TokenCache;
};
type WebSSOClerkProviderCustomProps = {
syncSessionWithTab?: boolean;
};

type WebSSOClerkProviderProps = ClerkReactProviderProps & WebSSOClerkProviderCustomProps;

const WebSSOClerkProvider = (props: WebSSOClerkProviderProps): JSX.Element | null => {
const { children, tokenCache: runtimeTokenCache, ...rest } = props;
const { children, ...rest } = props;
const { publishableKey = '' } = props;

const [clerkInstance, setClerkInstance] = React.useState<ClerkProp>(null);

// When syncSessionWithTab is set tokenCache is an optional parameter that defaults to ChromeStorageCache
const tokenCache = runtimeTokenCache || ChromeStorageCache;

React.useEffect(() => {
void (async () => {
setClerkInstance(await buildClerk({ publishableKey, tokenCache }));
setClerkInstance(await buildClerk({ publishableKey }));
})();
}, []);

Expand Down Expand Up @@ -73,14 +62,6 @@ const StandaloneClerkProvider = (props: ClerkReactProviderProps): JSX.Element =>

type ChromeExtensionClerkProviderProps = WebSSOClerkProviderProps;

export function ClerkProvider(props: ChromeExtensionClerkProviderProps): JSX.Element | null {
const { tokenCache, syncSessionWithTab, ...rest } = props;
return syncSessionWithTab ? (
<WebSSOClerkProvider
{...props}
tokenCache={tokenCache}
/>
) : (
<StandaloneClerkProvider {...rest} />
);
export function ClerkProvider({ syncSessionWithTab, ...rest }: ChromeExtensionClerkProviderProps): JSX.Element | null {
return syncSessionWithTab ? <WebSSOClerkProvider {...rest} /> : <StandaloneClerkProvider {...rest} />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ exports[`public exports should not include a breaking change 1`] = `
"ClerkLoaded",
"ClerkLoading",
"ClerkProvider",
"ContentScript",
"CreateOrganization",
"EmailLinkErrorCode",
"Experimental__Gate",
Expand Down
31 changes: 0 additions & 31 deletions packages/chrome-extension/src/cache.ts

This file was deleted.

1 change: 1 addition & 0 deletions packages/chrome-extension/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const STORAGE_KEY_CLIENT_JWT = '__clerk_client_jwt';
55 changes: 55 additions & 0 deletions packages/chrome-extension/src/content.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { parsePublishableKey } from '@clerk/shared';
import { createExtensionSyncManager, events } from '@clerk/shared/extensionSyncManager';

import { STORAGE_KEY_CLIENT_JWT } from './constants';
import { ClerkChromeExtensionError, logErrorHandler } from './errors';
import { ChromeStorageCache } from './utils/storage';

export const ContentScript = {
init(publishableKey: string) {
try {
// Ensure we have a publishable key
if (!publishableKey) {
throw new ClerkChromeExtensionError('Missing publishable key.');
}

// Parse the publishable key
const { frontendApi, instanceType } = parsePublishableKey(publishableKey) || {};

// Ensure we have a valid publishable key
if (!frontendApi || !instanceType) {
throw new ClerkChromeExtensionError('Invalid publishable key.');
}

// Ensure we're in a development environment
if (instanceType !== 'development') {
throw new ClerkChromeExtensionError(`
You're attempting to load the Clerk Chrome Extension content script in an unsupported environment.
Please update your manifest.json to exclude production URLs in content_scripts.
`);
}

// Create an extension sync manager
const extensionSyncManager = createExtensionSyncManager();

// Listen for token update events from other Clerk hosts
extensionSyncManager.on(events.DevJWTUpdate, ({ data }) => {
// Ignore events from other Clerk hosts
if (data.frontendApi !== frontendApi) {
console.log('Received a token update event for a different Clerk host. Ignoring.');
return;
}

const KEY = ChromeStorageCache.createKey(data.frontendApi, STORAGE_KEY_CLIENT_JWT);

if (data.action === 'set') {
void ChromeStorageCache.set(KEY, data.token);
} else if (data.action === 'remove') {
void ChromeStorageCache.remove(KEY);
}
});
} catch (e) {
logErrorHandler(e as Error);
}
},
};
10 changes: 10 additions & 0 deletions packages/chrome-extension/src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// error handler that logs the error (used in cookie retrieval and token saving)
export const logErrorHandler = (err: Error) => console.error(err);

export class ClerkChromeExtensionError extends Error {
clerk: boolean = true;

constructor(message: string) {
super(`[Clerk: Chrome Extension]: ${message}`);
}
}
3 changes: 1 addition & 2 deletions packages/chrome-extension/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// eslint-disable-next-line import/export
export * from '@clerk/clerk-react';
export { ContentScript } from './content';

// order matters since we want override @clerk/clerk-react ClerkProvider
// eslint-disable-next-line import/export
export { ClerkProvider } from './ClerkProvider';
69 changes: 37 additions & 32 deletions packages/chrome-extension/src/singleton.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,58 @@
import { Clerk } from '@clerk/clerk-js';
import type { ClerkProp } from '@clerk/clerk-react';
import { parsePublishableKey } from '@clerk/shared';

import type { TokenCache } from './cache';
import { convertPublishableKeyToFrontendAPIOrigin, getClientCookie } from './utils';

const KEY = '__clerk_client_jwt';
import { STORAGE_KEY_CLIENT_JWT } from './constants';
import { logErrorHandler } from './errors';
import { getClientCookie } from './utils/cookies';
import { ChromeStorageCache } from './utils/storage';

export let clerk: ClerkProp;

type BuildClerkOptions = {
publishableKey: string;
tokenCache: TokenCache;
};

// error handler that logs the error (used in cookie retrieval and token saving)
const logErrorHandler = (err: Error) => console.error(err);
export async function buildClerk({ publishableKey }: BuildClerkOptions): Promise<ClerkProp> {
if (clerk) {
return clerk;
}

export async function buildClerk({ publishableKey, tokenCache }: BuildClerkOptions): Promise<ClerkProp> {
if (!clerk) {
const clerkFrontendAPIOrigin = convertPublishableKeyToFrontendAPIOrigin(publishableKey);
const { frontendApi, instanceType } = parsePublishableKey(publishableKey) || {};

const clientCookie = await getClientCookie(clerkFrontendAPIOrigin).catch(logErrorHandler);
if (!frontendApi || !instanceType) {
throw new Error('Invalid publishable key.');
}

// TODO: Listen to client cookie changes and sync updates
// https://developer.chrome.com/docs/extensions/reference/cookies/#event-onChanged
const clientCookie = await getClientCookie(frontendApi).catch(logErrorHandler);

if (clientCookie) {
await tokenCache.saveToken(KEY, clientCookie.value).catch(logErrorHandler);
}
// TODO: Listen to client cookie changes and sync updates
// https://developer.chrome.com/docs/extensions/reference/cookies/#event-onChanged

clerk = new Clerk(publishableKey);
const KEY = ChromeStorageCache.createKey(frontendApi, STORAGE_KEY_CLIENT_JWT);

// @ts-expect-error
clerk.__unstable__onBeforeRequest(async requestInit => {
requestInit.credentials = 'omit';
requestInit.url?.searchParams.append('_is_native', '1');
if (clientCookie) {
await ChromeStorageCache.set(KEY, clientCookie.value).catch(logErrorHandler);
}

const jwt = await tokenCache.getToken(KEY);
(requestInit.headers as Headers).set('authorization', jwt || '');
});
clerk = new Clerk(publishableKey);

// @ts-expect-error
clerk.__unstable__onAfterResponse(async (_, response) => {
const authHeader = response.headers.get('authorization');
if (authHeader) {
await tokenCache.saveToken(KEY, authHeader);
}
});
}
// @ts-expect-error - Clerk doesn't expose this unstable method
clerk.__unstable__onBeforeRequest(async requestInit => {
requestInit.credentials = 'omit';
requestInit.url?.searchParams.append('_is_native', '1');

const jwt = await ChromeStorageCache.get(KEY);
(requestInit.headers as Headers).set('authorization', jwt || '');
});

// @ts-expect-error - Clerk doesn't expose this unstable method
clerk.__unstable__onAfterResponse(async (_, response) => {
const authHeader = response.headers.get('authorization');
if (authHeader) {
await ChromeStorageCache.set(KEY, authHeader);
}
});

return clerk;
}
67 changes: 0 additions & 67 deletions packages/chrome-extension/src/utils.test.ts

This file was deleted.

17 changes: 0 additions & 17 deletions packages/chrome-extension/src/utils.ts

This file was deleted.

Loading