Skip to content
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

refactor(console): add chrome extension guide #6178

Merged
merged 1 commit into from
Jul 5, 2024
Merged
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
5 changes: 5 additions & 0 deletions .changeset/long-worms-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@logto/console": patch
---

add Chrome extension guide
8 changes: 8 additions & 0 deletions packages/console/src/assets/docs/guides/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -60,6 +61,13 @@ export const guides: Readonly<Guide[]> = 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',
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

<Steps>

<Step
title="Installation"
subtitle="Install Logto SDK for your project"
>

<NpmLikeInstallation packageName="@logto/chrome-extension" />

</Step>

<Step title="Understand the authentication flow">

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.

<RegardingRedirectBasedSignIn />

</Step>

<Step title="Configure your extension">

### 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.

<InlineNotification>
If you are using a custom domain on Logto Cloud, you need to update the `host_permissions` to match your domain.
</InlineNotification>

### 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: '<your-logto-endpoint>'
appId: '<your-logto-app-id>',
});
```

Replace `<your-logto-endpoint>` and `<your-logto-app-id>` 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.

<InlineNotification>
**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.
</InlineNotification>

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://<extension-id>.chromiumapp.org/callback` for sign-in.
- `https://<extension-id>.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.

</Step>

<Step title="Update Logto application settings">

As we mentioned in the previous step, we need to update the Logto application settings to allow the redirect URIs we just created (`https://<extension-id>.chromiumapp.org/callback`):

<UriInputField name="redirectUris" />

And the post sign-out redirect URI (`https://<extension-id>.chromiumapp.org/`):

<UriInputField name="postLogoutRedirectUris" />

Finally, the CORS allowed origins should include the extension's origin (`chrome-extension://<extension-id>`). The SDK in Chrome extension will use this origin to communicate with the Logto APIs.

<UriInputField type="customClientMetadata" name="corsAllowedOrigins" />

Don't forget to replace `<extension-id>` with your actual extension ID and click the "Save" button.

</Step>

<Step title="Add sign-in and sign-out buttons">

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"
<button id="sign-in">Sign in</button> <button id="sign-out">Sign out</button>
```

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.
});
```

</Step>

<Step title="Checkpoint: Test the authentication flow">

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.

</Step>

<Step title="Check authentication state">

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: '<your-logto-endpoint>'
appId: '<your-logto-app-id>',
});

// 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:

<img src={extensionPopup} alt="Popup page" width="100%" />

</Step>

<Step title="Other considerations">

- **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.

</Step>

</Steps>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"order": 1.1
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ApplicationType } from '@logto/schemas';

import { type GuideMetadata } from '../types';

const metadata: Readonly<GuideMetadata> = 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;
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading