-
Notifications
You must be signed in to change notification settings - Fork 279
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(clerk-js,chrome-extension,shared): Expand WebSSO capabilities (C…
…ontent Scripts) [SDK-836]
- Loading branch information
Showing
20 changed files
with
384 additions
and
202 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 || ""); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export const STORAGE_KEY_CLIENT_JWT = '__clerk_client_jwt'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}`); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.