From 490a7b0264472d09c480fed8ccf27e5a7524b17a Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Thu, 4 Jul 2024 17:47:18 +0800 Subject: [PATCH] refactor(console): add chrome extension guide --- .changeset/long-worms-refuse.md | 5 + .../console/src/assets/docs/guides/index.tsx | 8 + .../guides/spa-chrome-extension/README.mdx | 238 ++++++++++++++++++ .../guides/spa-chrome-extension/config.json | 3 + .../spa-chrome-extension/extension-popup.webp | Bin 0 -> 14648 bytes .../docs/guides/spa-chrome-extension/index.ts | 16 ++ .../docs/guides/spa-chrome-extension/logo.svg | 26 ++ .../mdx-components/UriInputField/index.tsx | 53 ++-- packages/console/src/types/guide.ts | 10 +- 9 files changed, 340 insertions(+), 19 deletions(-) create mode 100644 .changeset/long-worms-refuse.md create mode 100644 packages/console/src/assets/docs/guides/spa-chrome-extension/README.mdx create mode 100644 packages/console/src/assets/docs/guides/spa-chrome-extension/config.json create mode 100644 packages/console/src/assets/docs/guides/spa-chrome-extension/extension-popup.webp create mode 100644 packages/console/src/assets/docs/guides/spa-chrome-extension/index.ts create mode 100644 packages/console/src/assets/docs/guides/spa-chrome-extension/logo.svg diff --git a/.changeset/long-worms-refuse.md b/.changeset/long-worms-refuse.md new file mode 100644 index 00000000000..6d0454c6973 --- /dev/null +++ b/.changeset/long-worms-refuse.md @@ -0,0 +1,5 @@ +--- +"@logto/console": patch +--- + +add Chrome extension guide diff --git a/packages/console/src/assets/docs/guides/index.tsx b/packages/console/src/assets/docs/guides/index.tsx index 5582af3ee99..d83d09817d4 100644 --- a/packages/console/src/assets/docs/guides/index.tsx +++ b/packages/console/src/assets/docs/guides/index.tsx @@ -12,6 +12,7 @@ import nativeExpo from './native-expo/index'; import nativeFlutter from './native-flutter/index'; import nativeIosSwift from './native-ios-swift/index'; import spaAngular from './spa-angular/index'; +import spaChromeExtension from './spa-chrome-extension/index'; import spaReact from './spa-react/index'; import spaVanilla from './spa-vanilla/index'; import spaVue from './spa-vue/index'; @@ -60,6 +61,13 @@ export const guides: Readonly = Object.freeze([ Component: lazy(async () => import('./spa-angular/README.mdx')), metadata: spaAngular, }, + { + order: 1.1, + id: 'spa-chrome-extension', + Logo: lazy(async () => import('./spa-chrome-extension/logo.svg')), + Component: lazy(async () => import('./spa-chrome-extension/README.mdx')), + metadata: spaChromeExtension, + }, { order: 1.1, id: 'spa-react', diff --git a/packages/console/src/assets/docs/guides/spa-chrome-extension/README.mdx b/packages/console/src/assets/docs/guides/spa-chrome-extension/README.mdx new file mode 100644 index 00000000000..98d22ecee13 --- /dev/null +++ b/packages/console/src/assets/docs/guides/spa-chrome-extension/README.mdx @@ -0,0 +1,238 @@ +import UriInputField from '@/mdx-components/UriInputField'; +import InlineNotification from '@/ds-components/InlineNotification'; +import Steps from '@/mdx-components/Steps'; +import Step from '@/mdx-components/Step'; +import NpmLikeInstallation from '@/mdx-components/NpmLikeInstallation'; + +import RegardingRedirectBasedSignIn from '../../fragments/_regarding-redirect-based-sign-in.md'; + +import extensionPopup from './extension-popup.webp'; + + + + + + + + + + + +Assuming you put a "Sign in" button in your Chrome extension's popup, the authentication flow will look like this: + +```mermaid +sequenceDiagram + participant A as Extension popup + participant B as Extension service worker + participant C as Logto sign-in experience + + A->>B: Invokes sign-in + B->>C: Redirects to Logto + C->>C: User signs in + C->>B: Redirects back to extension + B->>A: Notifies the popup +``` + +For other interactive pages in your extension, you just need to replace the `Extension popup` participant with the page's name. In this tutorial, we will focus on the popup page. + + + + + + + +### Update the `manifest.json` + +Logto SDK requires the following permissions in the `manifest.json`: + +```json title="manifest.json" +{ + "permissions": ["identity", "storage"], + "host_permissions": ["https://*.logto.app/*"] +} +``` + +- `permissions.identity`: Required for the Chrome Identity API, which is used to sign in and sign out. +- `permissions.storage`: Required for storing the user's session. +- `host_permissions`: Required for the Logto SDK to communicate with the Logto APIs. + + +If you are using a custom domain on Logto Cloud, you need to update the `host_permissions` to match your domain. + + +### Set up a background script (service worker) + +In your Chrome extension's background script, initialize the Logto SDK: + +```js title="service-worker.js" +import LogtoClient from '@logto/chrome-extension'; + +export const logtoClient = new LogtoClient({ + endpoint: '' + appId: '', +}); +``` + +Replace `` and `` with the actual values. You can find these values in the application page you just created in the Logto Console. + +If you don't have a background script, you can follow the [official guide](https://developer.chrome.com/docs/extensions/develop/concepts/service-workers/basics) to create one. + + +**Why do we need a background script?** + +Normal extension pages like the popup or options page can't run in the background, and they have the possibility to be closed during the authentication process. A background script ensures the authentication process can be properly handled. + + +Then, we need to listen to the message from other extension pages and handle the authentication process: + +```js title="service-worker.js" +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + // In the below code, since we return `true` for each action, we need to call `sendResponse` + // to notify the sender. You can also handle errors here, or use other ways to notify the sender. + + if (message.action === 'signIn') { + const redirectUri = chrome.identity.getRedirectURL('/callback'); + logtoClient.signIn(redirectUri).finally(sendResponse); + return true; + } + + if (message.action === 'signOut') { + const redirectUri = chrome.identity.getRedirectURL(); + logtoClient.signOut(redirectUri).finally(sendResponse); + return true; + } + + return false; +}); +``` + +You may notice there are two redirect URIs used in the code above. They are both created by `chrome.identity.getRedirectURL`, which is a [built-in Chrome API](https://developer.chrome.com/docs/extensions/reference/api/identity#method-getRedirectURL) to generate a redirect URL for auth flows. The two URIs will be: + +- `https://.chromiumapp.org/callback` for sign-in. +- `https://.chromiumapp.org/` for sign-out. + +Note that these URIs are not accessible, and they are only used for Chrome to trigger specific actions for the authentication process. + + + + + +As we mentioned in the previous step, we need to update the Logto application settings to allow the redirect URIs we just created (`https://.chromiumapp.org/callback`): + + + +And the post sign-out redirect URI (`https://.chromiumapp.org/`): + + + +Finally, the CORS allowed origins should include the extension's origin (`chrome-extension://`). The SDK in Chrome extension will use this origin to communicate with the Logto APIs. + + + +Don't forget to replace `` with your actual extension ID and click the "Save" button. + + + + + +We're almost there! Let's add the sign-in and sign-out buttons and other necessary logic to the popup page. + +In the `popup.html` file: + +```html title="popup.html" + +``` + +In the `popup.js` file (assuming `popup.js` is included in the `popup.html`): + +```js title="popup.js" +document.getElementById('sign-in').addEventListener('click', async () => { + await chrome.runtime.sendMessage({ action: 'signIn' }); + // Sign-in completed (or failed), you can update the UI here. +}); + +document.getElementById('sign-out').addEventListener('click', async () => { + await chrome.runtime.sendMessage({ action: 'signOut' }); + // Sign-out completed (or failed), you can update the UI here. +}); +``` + + + + + +Now you can test the authentication flow in your Chrome extension: + +1. Open the extension popup. +2. Click on the "Sign in" button. +3. You will be redirected to the Logto sign-in page. +4. Sign in with your Logto account. +5. You will be redirected back to the Chrome. + + + + + +Since Chrome provide unified storage APIs, rather than the sign-in and sign-out flow, all other Logto SDK methods can be used in the popup page directly. + +In your `popup.js`, you can reuse the `LogtoClient` instance created in the background script, or create a new one with the same configuration: + +```js title="popup.js" +import LogtoClient from '@logto/chrome-extension'; + +const logtoClient = new LogtoClient({ + endpoint: '' + appId: '', +}); + +// Or reuse the logtoClient instance created in the background script +import { logtoClient } from './service-worker.js'; +``` + +Then you can create a function to load the authentication state and user's profile: + +```js title="popup.js" +const loadAuthenticationState = async () => { + const isAuthenticated = await logtoClient.isAuthenticated(); + // Update the UI based on the authentication state + + if (isAuthenticated) { + const user = await logtoClient.getIdTokenClaims(); // { sub: '...', email: '...', ... } + // Update the UI with the user's profile + } +}; +``` + +You can also combine the `loadAuthenticationState` function with the sign-in and sign-out logic: + +```js title="popup.js" +document.getElementById('sign-in').addEventListener('click', async () => { + await chrome.runtime.sendMessage({ action: 'signIn' }); + await loadAuthenticationState(); +}); + +document.getElementById('sign-out').addEventListener('click', async () => { + await chrome.runtime.sendMessage({ action: 'signOut' }); + await loadAuthenticationState(); +}); +``` + +Here's an example of the popup page with the authentication state: + +Popup page + + + + + +- **Service worker bundling**: If you use a bundler like Webpack or Rollup, you need to explicitly set the target to `browser` or similar to avoid unnecessary bundling of Node.js modules. +- **Module resolution**: Logto Chrome extension SDK is an ESM-only module. + +See our [sample project](https://github.com/logto-io/js/tree/HEAD/packages/chrome-extension-sample) for a complete example with TypeScript, Rollup, and other configurations. + + + + diff --git a/packages/console/src/assets/docs/guides/spa-chrome-extension/config.json b/packages/console/src/assets/docs/guides/spa-chrome-extension/config.json new file mode 100644 index 00000000000..4721ad2f793 --- /dev/null +++ b/packages/console/src/assets/docs/guides/spa-chrome-extension/config.json @@ -0,0 +1,3 @@ +{ + "order": 1.1 +} diff --git a/packages/console/src/assets/docs/guides/spa-chrome-extension/extension-popup.webp b/packages/console/src/assets/docs/guides/spa-chrome-extension/extension-popup.webp new file mode 100644 index 0000000000000000000000000000000000000000..9e136c06e252ea6b86677b44130bd672f363bc5c GIT binary patch literal 14648 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�T|PLeiXi%`cH}TeIYq@wy+S!=;X}Fml15RZZ+iw}SK7)*GERf1N zsH@2sZ+_9CtZNVKD^g;@uie&RfVOgNAk6Q&^Tb6#-WatoupSe^y5D@5sah~~U%b*% zVBH`*ZN~{E^9t}BOHT*nEMrSB&&9ZAwX3sngYyM7&?By@1=;SWO~G}xYe8^>hMWG? z%OD7y(c0L__??qcpXkbewBLIK3&_*REHmFApXYMS5&+x&J^rBvy$i7I| z^pc0?-3VVlrwoMESdkqNl|*|oH3_wMgU(bl9MA=}VdVg=BYrA2bH`asjP{40>{?sr$y_7X$DB^ zO3R9LW*f&pVvGE#K9T7OZ=7hlSP~ul0Zy_98mcK$KzB)Z6Bjv4?Wb%U@A|`9uTXS9 zwkh$*GnCn=iPrD_&3}1345qVl{F89)na@AsH-02Cj(m~3=Y6MO`c%-++&7GXL=9qz zI&@f*4c2p}Aw!R$cLqKk7`mo*bRlgopdY|dA2P_+Ih@g2@YCEUS}F?Cq>XFvLt4I8 z^%(lA;DgTcHJAo%dl!~kUdOfd=equxI}6wjXH}jd%xkp`?k&IQ_jb)C=BfcMq3IfG zZ)eF%Ezxys)YZe1PrBhI5*;t|=IXsla37bQ#3rAlE^#*!k(0Efi92Ihrh4 zDT#A+yesrFi%Cjq29|!iyp2aG`==ck?I89(#MTOiMYc-l_^rO7&7z$X-l#JyA~6D< z8lgoUN{+e`*T{h75C@h+vSNoN?NQ6x$Qt4fk*Sh?ylOS!p@}>%?bRCSBC4Ewj26mo zmXFW|O#r3Gp;`f3O^xl2IgY2XyY{6#%R;|XY_$QmwE#b@ISO#*F$_0MvksrXb2^N_ zlM5BtvlkAOS>uhRv`*e}owWqltsNGyFh?60O7jF}hdOFNyVsEVO>oHJZloJ~UH zR_Hy*@upAw6vx-FIYUITotdlqQ2fEnHExXzI+bd64bAOhtUklnHSv=4Eefd}xtW~) zZ&{+X;G93)JaWcvkg6$SiZ8`T5CWFRj{BGsqRzH~+WkGNP3OB!gBU2FNCMtMgEBuq z4SIQ)gg67owh}-4XzoVthI2}Ggr^VXmT;L=O{Sm3cK+ZCN>t=DtoBbk^QS)^&L4}l z)m`Ca)P}3$-;)_rxT8KFKMK}y^EKI~tdGg1 zXZ)mWp=g(}o6U>e%|=t3VWcU}E@yKGm{LSVTMyG2*uCx T0CsYQu0Hxwol<6ie~$kL<|mb% literal 0 HcmV?d00001 diff --git a/packages/console/src/assets/docs/guides/spa-chrome-extension/index.ts b/packages/console/src/assets/docs/guides/spa-chrome-extension/index.ts new file mode 100644 index 00000000000..90ad8eb36eb --- /dev/null +++ b/packages/console/src/assets/docs/guides/spa-chrome-extension/index.ts @@ -0,0 +1,16 @@ +import { ApplicationType } from '@logto/schemas'; + +import { type GuideMetadata } from '../types'; + +const metadata: Readonly = Object.freeze({ + name: 'Chrome extension', + description: 'Build a Chrome extension with Logto.', + target: ApplicationType.SPA, + sample: { + repo: 'js', + path: 'packages/chrome-extension-sample', + }, + fullGuide: 'chrome-extension', +}); + +export default metadata; diff --git a/packages/console/src/assets/docs/guides/spa-chrome-extension/logo.svg b/packages/console/src/assets/docs/guides/spa-chrome-extension/logo.svg new file mode 100644 index 00000000000..4fc53255e9c --- /dev/null +++ b/packages/console/src/assets/docs/guides/spa-chrome-extension/logo.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/console/src/mdx-components/UriInputField/index.tsx b/packages/console/src/mdx-components/UriInputField/index.tsx index 677b8a5a222..36cd2d3000d 100644 --- a/packages/console/src/mdx-components/UriInputField/index.tsx +++ b/packages/console/src/mdx-components/UriInputField/index.tsx @@ -17,19 +17,41 @@ import { } from '@/ds-components/MultiTextInput/utils'; import type { RequestError } from '@/hooks/use-api'; import useApi from '@/hooks/use-api'; -import type { GuideForm } from '@/types/guide'; +import type { + CustomClientMetadataKey, + GuideForm, + Name, + OidcClientMetadataKey, +} from '@/types/guide'; import { trySubmitSafe } from '@/utils/form'; import { uriValidator } from '@/utils/validator'; import * as styles from './index.module.scss'; -type Props = { - readonly name: 'redirectUris' | 'postLogoutRedirectUris'; - /** The default value of the input field when there's no data. */ - readonly defaultValue?: string; -}; +const nameToKey: Record = Object.freeze({ + redirectUris: 'application_details.redirect_uri', + postLogoutRedirectUris: 'application_details.post_sign_out_redirect_uri', + corsAllowedOrigins: 'application_details.cors_allowed_origins', +}); + +type Props = + | { + readonly name: OidcClientMetadataKey; + readonly type?: 'oidcClientMetadata'; + /** The default value of the input field when there's no data. */ + readonly defaultValue?: string; + } + | { + readonly name: CustomClientMetadataKey; + readonly type: 'customClientMetadata'; + /** The default value of the input field when there's no data. */ + readonly defaultValue?: string; + }; + +function UriInputField(props: Props) { + const { name, defaultValue } = props; + const type = props.type ?? 'oidcClientMetadata'; -function UriInputField({ name, defaultValue }: Props) { const methods = useForm>(); const { control, @@ -45,10 +67,7 @@ function UriInputField({ name, defaultValue }: Props) { const ref = useRef(null); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const api = useApi(); - const title: AdminConsoleKey = - name === 'redirectUris' - ? 'application_details.redirect_uri' - : 'application_details.post_sign_out_redirect_uri'; + const title: AdminConsoleKey = nameToKey[name]; const onSubmit = trySubmitSafe(async (value: string[]) => { if (!appId) { @@ -57,7 +76,7 @@ function UriInputField({ name, defaultValue }: Props) { const updatedApp = await api .patch(`api/applications/${appId}`, { json: { - oidcClientMetadata: { + [type]: { [name]: value.filter(Boolean), }, }, @@ -77,9 +96,13 @@ function UriInputField({ name, defaultValue }: Props) { } }; - const clientMetadata = data?.oidcClientMetadata[name]; - const defaultValueArray = clientMetadata?.length - ? clientMetadata + const dataValue = + props.type === 'customClientMetadata' + ? data?.customClientMetadata[props.name] + : data?.oidcClientMetadata[props.name]; + + const defaultValueArray = dataValue?.length + ? dataValue : conditional(defaultValue && [defaultValue]); return ( diff --git a/packages/console/src/types/guide.ts b/packages/console/src/types/guide.ts index 9a22384dc8a..df47b51949d 100644 --- a/packages/console/src/types/guide.ts +++ b/packages/console/src/types/guide.ts @@ -1,4 +1,6 @@ -export type GuideForm = { - redirectUris: string[]; - postLogoutRedirectUris: string[]; -}; +export type OidcClientMetadataKey = 'redirectUris' | 'postLogoutRedirectUris'; +export type CustomClientMetadataKey = 'corsAllowedOrigins'; + +export type Name = OidcClientMetadataKey | CustomClientMetadataKey; + +export type GuideForm = Record;