跳至主要內容

為你的 Chrome 擴充功能應用程式新增驗證 (Authentication)

本指南將向你展示如何將 Logto 整合到你的 Chrome 擴充功能中。

提示:
  • 以下示範已在 Chrome v123.0.6312.87 (arm64) 上測試過。其他版本只要支援 SDK 中使用的 chrome API,應該也能正常運作。
  • 範例專案可在我們的 GitHub 儲存庫 中找到。

先決條件

安裝

npm i @logto/chrome-extension

整合

驗證流程

假設你在 Chrome 擴充功能的彈出視窗中放置了一個「登入」按鈕,驗證流程將如下所示:

對於擴充功能中的其他互動頁面,只需將 擴充功能彈出視窗 參與者替換為頁面的名稱。在本教程中,我們將專注於彈出頁面。

關於基於重導的登入

  1. 此驗證流程遵循 OpenID Connect (OIDC) 協議,Logto 強制執行嚴格的安全措施以保護使用者登入。
  2. 如果你有多個應用程式,可以使用相同的身分提供者 (IdP, Identity provider)(Logto)。一旦使用者登入其中一個應用程式,Logto 將在使用者訪問另一個應用程式時自動完成登入流程。

欲了解更多關於基於重導登入的原理和優勢,請參閱 Logto 登入體驗解析

更新 manifest.json

Logto SDK 需要在 manifest.json 中添加以下權限:

manifest.json
{
"permissions": ["identity", "storage"],
"host_permissions": ["https://*.logto.app/*"]
}
  • permissions.identity:需要用於 Chrome Identity API,用於登入和登出。
  • permissions.storage:需要用於儲存使用者的會話。
  • host_permissions:需要用於 Logto SDK 與 Logto API 進行通信。
備註:

如果你在 Logto Cloud 上使用自訂網域,則需要更新 host_permissions 以匹配你的網域。

設定背景腳本(服務工作者)

在你的 Chrome 擴充功能的背景腳本中,初始化 Logto SDK:

service-worker.js
import LogtoClient from '@logto/chrome-extension';

export const logtoClient = new LogtoClient({
endpoint: '<your-logto-endpoint>'
appId: '<your-logto-app-id>',
});

<your-logto-endpoint><your-logto-app-id> 替換為實際值。你可以在 Logto Console 中剛創建的應用程式頁面找到這些值。

如果你沒有背景腳本,可以按照 官方指南 創建一個。

資訊:

為什麼需要背景腳本?

普通擴充功能頁面如彈出視窗或選項頁無法在背景中運行,且在驗證過程中可能會被關閉。背景腳本確保驗證過程能夠正確處理。

接著,我們需要監聽來自其他擴充功能頁面的訊息並處理驗證過程:

service-worker.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
// 在下面的代碼中,由於我們為每個動作返回 `true`,因此需要調用 `sendResponse`
// 來通知發送者。你也可以在這裡處理錯誤,或使用其他方式通知發送者。

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

你可能注意到上面的代碼中使用了兩個重定向 URI。它們都是由 chrome.identity.getRedirectURL 創建的,這是一個 內建的 Chrome API,用於生成驗證流程的重定向 URL。這兩個 URI 將是:

  • https://<extension-id>.chromiumapp.org/callback 用於登入。
  • https://<extension-id>.chromiumapp.org/ 用於登出。

注意這些 URI 是不可訪問的,它們僅用於 Chrome 觸發驗證過程的特定操作。

更新 Logto 應用程式設定

現在我們需要更新 Logto 應用程式設定,以允許我們剛創建的重定向 URI。

  1. 前往 Logto Console 中的應用程式頁面。
  2. 在「重定向 URI」部分,添加 URI:https://<extension-id>.chromiumapp.org/callback
  3. 在「登出後重定向 URI」部分,添加 URI:https://<extension-id>.chromiumapp.org/
  4. 在「CORS 允許的來源」部分,添加 URI:chrome-extension://<extension-id>。Chrome 擴充功能中的 SDK 將使用此來源與 Logto API 進行通信。
  5. 點擊 保存更改

記得將 <extension-id> 替換為你的實際擴充功能 ID。你可以在 chrome://extensions 頁面找到擴充功能 ID。

在彈出視窗中添加登入和登出按鈕

我們快完成了!讓我們在彈出頁面中添加登入和登出按鈕及其他必要的邏輯。

popup.html 文件中:

popup.html
<button id="sign-in">登入</button> <button id="sign-out">登出</button>

popup.js 文件中(假設 popup.js 已包含在 popup.html 中):

popup.js
document.getElementById('sign-in').addEventListener('click', async () => {
await chrome.runtime.sendMessage({ action: 'signIn' });
// 登入完成(或失敗),你可以在這裡更新 UI。
});

document.getElementById('sign-out').addEventListener('click', async () => {
await chrome.runtime.sendMessage({ action: 'signOut' });
// 登出完成(或失敗),你可以在這裡更新 UI。
});

檢查點:測試驗證流程

現在你可以在 Chrome 擴充功能中測試驗證流程:

  1. 打開擴充功能彈出視窗。
  2. 點擊「登入」按鈕。
  3. 你將被重定向到 Logto 登入頁面。
  4. 使用你的 Logto 帳戶登入。
  5. 你將被重定向回 Chrome。

檢查驗證狀態

由於 Chrome 提供統一的存儲 API,除了登入和登出流程外,所有其他 Logto SDK 方法都可以直接在彈出頁面中使用。

在你的 popup.js 中,你可以重用在背景腳本中創建的 LogtoClient 實例,或使用相同的配置創建一個新實例:

popup.js
import LogtoClient from '@logto/chrome-extension';

const logtoClient = new LogtoClient({
endpoint: '<your-logto-endpoint>'
appId: '<your-logto-app-id>',
});

// 或重用在背景腳本中創建的 logtoClient 實例
import { logtoClient } from './service-worker.js';

然後你可以創建一個函數來加載驗證狀態和使用者的資料:

popup.js
const loadAuthenticationState = async () => {
const isAuthenticated = await logtoClient.isAuthenticated();
// 根據驗證狀態更新 UI

if (isAuthenticated) {
const user = await logtoClient.getIdTokenClaims(); // { sub: '...', email: '...', ... }
// 使用使用者的資料更新 UI
}
};

你也可以將 loadAuthenticationState 函數與登入和登出邏輯結合:

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();
});

以下是具有驗證狀態的彈出頁面示例:

彈出頁面

其他考量

  • 服務工作者打包:如果你使用像 Webpack 或 Rollup 這樣的打包工具,你需要明確將目標設置為 browser 或類似選項,以避免不必要的 Node.js 模組打包。
  • 模組解析:Logto Chrome 擴充功能 SDK 是一個僅支持 ESM 的模組。

查看我們的 範例專案 以獲取完整的 TypeScript、Rollup 和其他配置示例。

獲取使用者資訊

顯示使用者資訊

要顯示使用者資訊,你可以使用 logtoClient.getIdTokenClaims() 方法。例如,在你的 Home 頁面中:

Home.js
const userInfo = await logtoClient.getIdTokenClaims();

// 為 ID 權杖 (ID token) 宣告 (Claims) 生成顯示表格
const table = document.createElement('table');
const thead = document.createElement('thead');
const tr = document.createElement('tr');
const thName = document.createElement('th');
const thValue = document.createElement('th');
thName.innerHTML = 'Name';
thValue.innerHTML = 'Value';
tr.append(thName, thValue);
thead.append(tr);
table.append(thead);

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

for (const [key, value] of Object.entries(userInfo)) {
const tr = document.createElement('tr');
const tdName = document.createElement('td');
const tdValue = document.createElement('td');
tdName.innerHTML = key;
tdValue.innerHTML = typeof value === 'string' ? value : JSON.stringify(value);
tr.append(tdName, tdValue);
tbody.append(tr);
}

table.append(tbody);

請求額外的宣告 (Claims)

你可能會發現從 getIdTokenClaims() 返回的物件中缺少一些使用者資訊。這是因為 OAuth 2.0 和 OpenID Connect (OIDC) 的設計遵循最小權限原則 (PoLP, Principle of Least Privilege),而 Logto 是基於這些標準構建的。

預設情況下,僅返回有限的宣告 (Claims)。如果你需要更多資訊,可以請求額外的權限範圍 (Scopes) 以存取更多宣告。

資訊:

「宣告 (Claim)」是對主體所做的斷言;「權限範圍 (Scope)」是一組宣告。在目前的情況下,宣告是關於使用者的一部分資訊。

以下是權限範圍與宣告關係的非規範性範例:

提示:

「sub」宣告表示「主體 (Subject)」,即使用者的唯一識別符(例如使用者 ID)。

Logto SDK 將始終請求三個權限範圍:openidprofileoffline_access

要請求額外的權限範圍 (Scopes),你可以配置 Logto 的設定:

index.js
import LogtoClient, { UserScope } from '@logto/browser';

const logtoClient = new LogtoClient({
appId: '<your-application-id>',
endpoint: '<your-logto-endpoint>',
scopes: [UserScope.Email, UserScope.Phone],
});

然後你可以在 logtoClient.getIdTokenClaims() 的返回值中訪問額外的宣告 (Claims):

const claims = await getIdTokenClaims();
// 現在你可以訪問額外的宣告 (Claims) `claims.email`、`claims.phone` 等。

需要網路請求的宣告 (Claims)

為了防止 ID 權杖 (ID token) 膨脹,某些宣告 (Claims) 需要透過網路請求來獲取。例如,即使在權限範圍 (Scopes) 中請求了 custom_data 宣告,它也不會包含在使用者物件中。要存取這些宣告,你可以使用 logtoClient.fetchUserInfo() 方法

const userInfo = await logtoClient.fetchUserInfo();
// 現在你可以訪問宣告 (Claim) `userInfo.custom_data`
此方法將透過請求 userinfo 端點來獲取使用者資訊。要了解更多可用的權限範圍 (Scopes) 和宣告 (Claims),請參閱 權限範圍 (Scopes) 和宣告 (Claims) 部分。

權限範圍 (Scopes) 和宣告 (Claims)

Logto 採用 OIDC 權限範圍 (Scopes) 與宣告 (Claims) 慣例 來定義從 ID 權杖 (ID token) 及 OIDC userinfo 端點 取得使用者資訊時的權限範圍與宣告。無論「權限範圍 (Scope)」還是「宣告 (Claim)」,皆為 OAuth 2.0 與 OpenID Connect (OIDC) 規範中的術語。

對於標準 OIDC 宣告 (Claims),其是否包含於 ID 權杖 (ID token) 內,完全取決於所請求的權限範圍 (Scopes)。擴充宣告(如 custom_dataorganizations)則可透過 自訂 ID 權杖 (Custom ID token) 設定,額外配置於 ID 權杖中。

以下是支援的權限範圍 (Scopes) 及對應的宣告 (Claims) 清單:

標準 OIDC 權限範圍 (Scopes)

openid(預設)

Claim nameTypeDescription
substring使用者的唯一識別符 (The unique identifier of the user)

profile(預設)

Claim nameTypeDescription
namestring使用者全名 (The full name of the user)
usernamestring使用者名稱 (The username of the user)
picturestring終端使用者大頭貼的 URL。此 URL 必須指向圖片檔案(如 PNG、JPEG 或 GIF),而非包含圖片的網頁。請注意,此 URL 應明確指向適合描述終端使用者的個人照片,而非任意由終端使用者拍攝的照片。(URL of the End-User's profile picture. This URL MUST refer to an image file (for example, a PNG, JPEG, or GIF image file), rather than to a Web page containing an image. Note that this URL SHOULD specifically reference a profile photo of the End-User suitable for displaying when describing the End-User, rather than an arbitrary photo taken by the End-User.)
created_atnumber終端使用者建立時間。以自 Unix epoch(1970-01-01T00:00:00Z)以來的毫秒數表示。(Time the End-User was created. The time is represented as the number of milliseconds since the Unix epoch (1970-01-01T00:00:00Z).)
updated_atnumber終端使用者資訊最後更新時間。以自 Unix epoch(1970-01-01T00:00:00Z)以來的毫秒數表示。(Time the End-User's information was last updated. The time is represented as the number of milliseconds since the Unix epoch (1970-01-01T00:00:00Z).)

其他 標準宣告 (Standard claims) 包含 family_namegiven_namemiddle_namenicknamepreferred_usernameprofilewebsitegenderbirthdatezoneinfolocale 也會包含在 profile 權限範圍內,無需額外請求 userinfo endpoint。與上表宣告不同的是,這些宣告僅在其值不為空時才會回傳,而上表宣告若值為空則會回傳 null

備註:

與標準宣告不同,created_atupdated_at 宣告使用毫秒而非秒為單位。

email

Claim nameTypeDescription
emailstring使用者的電子郵件地址 (The email address of the user)
email_verifiedboolean電子郵件地址是否已驗證 (Whether the email address has been verified)

phone

Claim nameTypeDescription
phone_numberstring使用者的電話號碼 (The phone number of the user)
phone_number_verifiedboolean電話號碼是否已驗證 (Whether the phone number has been verified)

address

請參閱 OpenID Connect Core 1.0 以瞭解 address 宣告的詳細資訊。

資訊:

標註為 (預設) 的權限範圍 (Scopes) 會由 Logto SDK 自動請求。當請求對應權限範圍時,標準 OIDC 權限範圍下的宣告 (Claims) 會始終包含於 ID 權杖 (ID token) 中,且無法關閉。

擴充權限範圍 (Extended scopes)

以下權限範圍由 Logto 擴充,會透過 userinfo endpoint 回傳宣告 (Claims)。這些宣告也可透過 Console > Custom JWT 設定直接包含於 ID 權杖 (ID token) 中。詳情請參閱 自訂 ID 權杖 (Custom ID token)

custom_data

Claim nameTypeDescriptionIncluded in ID token by default
custom_dataobject使用者的自訂資料 (The custom data of the user)

identities

Claim nameTypeDescriptionIncluded in ID token by default
identitiesobject使用者的連結身分 (The linked identities of the user)
sso_identitiesarray使用者的連結 SSO 身分 (The linked SSO identities of the user)

roles

Claim nameTypeDescriptionIncluded in ID token by default
rolesstring[]使用者的角色 (The roles of the user)

urn:logto:scope:organizations

Claim nameTypeDescriptionIncluded in ID token by default
organizationsstring[]使用者所屬的組織 ID (The organization IDs the user belongs to)
organization_dataobject[]使用者所屬的組織資料 (The organization data the user belongs to)
備註:

這些組織宣告 (Organization claims) 也可在使用 不透明權杖 (Opaque token) 時,透過 userinfo endpoint 取得。然而,不透明權杖無法作為組織權杖 (Organization tokens) 來存取組織專屬資源。詳見 不透明權杖與組織 (Opaque token and organizations)

urn:logto:scope:organization_roles

Claim nameTypeDescriptionIncluded in ID token by default
organization_rolesstring[]使用者所屬組織角色,格式為 <organization_id>:<role_name> (The organization roles the user belongs to with the format of <organization_id>:<role_name>)

API 資源

我們建議先閱讀 🔐 角色型存取控制 (RBAC, Role-Based Access Control),以瞭解 Logto RBAC 的基本概念以及如何正確設定 API 資源。

配置 Logto 用戶端

一旦你設定了 API 資源,就可以在應用程式中配置 Logto 時新增它們:

index.js
import LogtoClient from '@logto/browser';

const logtoClient = new LogtoClient({
// ...other configs
resources: ['https://shopping.your-app.com/api', 'https://store.your-app.com/api'], // 新增 API 資源 (API resources)
});

每個 API 資源都有其自身的權限(權限範圍)。

例如,https://shopping.your-app.com/api 資源具有 shopping:readshopping:write 權限,而 https://store.your-app.com/api 資源具有 store:readstore:write 權限。

要請求這些權限,你可以在應用程式中配置 Logto 時新增它們:

index.js
import LogtoClient from '@logto/chrome-extension';

const logtoClient = new LogtoClient({
// ...other configs
scopes: ['shopping:read', 'shopping:write', 'store:read', 'store:write'],
resources: ['https://shopping.your-app.com/api', 'https://store.your-app.com/api'], // 新增 API 資源 (API resources)
});

你可能會注意到權限範圍是獨立於 API 資源定義的。這是因為 OAuth 2.0 的資源標示符 (Resource Indicators) 指定請求的最終權限範圍將是所有目標服務中所有權限範圍的笛卡兒積。

因此,在上述情況中,權限範圍可以從 Logto 的定義中簡化,兩個 API 資源都可以擁有 read write 權限範圍而不需要前綴。然後,在 Logto 配置中:

index.js
import LogtoClient, { UserScope } from '@logto/chrome-extension';

const logtoClient = new LogtoClient({
// ...other configs
scopes: ['read', 'write'],
resources: ['https://shopping.your-app.com/api', 'https://store.your-app.com/api'],
});

對於每個 API 資源,它將請求 readwrite 權限範圍。

備註:

請求未在 API 資源中定義的權限範圍是可以的。例如,即使 API 資源中沒有可用的 email 權限範圍,你也可以請求 email 權限範圍。不可用的權限範圍將被安全地忽略。

成功登入後,Logto 將根據使用者的角色向 API 資源發出適當的權限範圍。

取得 API 資源的存取權杖 (Access token)

要獲取特定 API 資源的存取權杖 (Access token),你可以使用 getAccessToken 方法:

const accessToken = await logtoClient.getAccessToken('https://store.your-app.com/api');
console.log('存取權杖 (Access token)', accessToken);

此方法將返回一個 JWT 存取權杖 (Access token),當使用者擁有相關權限時,可以用來存取 API 資源。如果當前快取的存取權杖 (Access token) 已過期,此方法將自動嘗試使用重新整理權杖 (Refresh token) 獲取新的存取權杖 (Access token)。

取得組織權杖 (Organization tokens)

如果你對組織 (Organization) 不熟悉,請閱讀 🏢 組織(多租戶,Multi-tenancy) 以開始了解。

在配置 Logto client 時,你需要新增 UserScope.Organizations 權限範圍 (scope):

index.js
import LogtoClient, { UserScope } from '@logto/chrome-extension';

const logtoClient = new LogtoClient({
// ...other configs
scopes: [UserScope.Organizations],
});

使用者登入後,你可以為使用者獲取組織權杖 (organization token):

index.js
// 從 userInfo 獲取 organizationIds

const claims = await logtoClient.getIdTokenClaims();
const organizationIds = claims.organizations;

/**
* 或從 ID 權杖 (ID token) 宣告 (Claims) 中獲取
*
* const claims = await logtoClient.getIdTokenClaims();
* const organizationIds = claims.organizations;
*/

// 獲取組織權杖 (Organization access token)
if (organizationIds.length > 0) {
const organizationId = organizationIds[0];
const organizationAccessToken = await logtoClient.getOrganizationToken(organizationId);
console.log('組織權杖 (Organization access token)', organizationAccessToken);
}

./code/_scopes-and-claims-code.mdx./code/_config-organization-code.mdx

將存取權杖 (Access token) 附加到請求標頭

將權杖放入 HTTP 標頭的 Authorization 欄位,使用 Bearer 格式(Bearer YOUR_TOKEN),即可完成。

備註:

Bearer 權杖的整合流程可能會根據你使用的框架或請求者而有所不同。選擇適合你的方式來應用請求的 Authorization 標頭。

延伸閱讀

終端使用者流程:驗證流程、帳號流程與組織流程 (End-user flows: authentication flows, account flows, and organization flows) 設定連接器 (Configure connectors) 授權 (Authorization)