跳至主要內容

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

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

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

先決條件

  • 一個 Logto Cloud 帳戶或 自託管 Logto
  • 在 Logto Console 中建立的單頁應用程式 (SPA)。
  • 一個 Chrome 擴充功能專案。

安裝

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 endpoint) 獲取使用者資訊的權限範圍和宣告。無論是「權限範圍 (Scope)」還是「宣告 (Claim)」,都是 OAuth 2.0 和 OpenID Connect (OIDC) 規範中的術語。

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

openid

宣告名稱類型描述需要使用者資訊嗎?
substring使用者的唯一識別符

profile

宣告名稱類型描述需要使用者資訊嗎?
namestring使用者的全名
usernamestring使用者的用戶名
picturestring使用者個人資料圖片的 URL。此 URL 必須指向圖像文件(例如 PNG、JPEG 或 GIF 圖像文件),而不是包含圖像的網頁。請注意,此 URL 應特別參考適合在描述使用者時顯示的個人資料照片,而不是使用者拍攝的任意照片。
created_atnumber使用者創建的時間。時間以自 Unix epoch(1970-01-01T00:00:00Z)以來的毫秒數表示。
updated_atnumber使用者資訊最後更新的時間。時間以自 Unix epoch(1970-01-01T00:00:00Z)以來的毫秒數表示。

其他 標準宣告 包括 family_namegiven_namemiddle_namenicknamepreferred_usernameprofilewebsitegenderbirthdatezoneinfolocale 也將包含在 profile 權限範圍中,無需請求使用者資訊端點。與上述宣告的不同之處在於,這些宣告僅在其值不為空時返回,而上述宣告在值為空時將返回 null

備註:

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

email

宣告名稱類型描述需要使用者資訊嗎?
emailstring使用者的電子郵件地址
email_verifiedboolean電子郵件地址是否已驗證

phone

宣告名稱類型描述需要使用者資訊嗎?
phone_numberstring使用者的電話號碼
phone_number_verifiedboolean電話號碼是否已驗證

address

請參閱 OpenID Connect Core 1.0 以獲取地址宣告的詳細資訊。

custom_data

宣告名稱類型描述需要使用者資訊嗎?
custom_dataobject使用者的自訂資料

identities

宣告名稱類型描述需要使用者資訊嗎?
identitiesobject使用者的連結身分
sso_identitiesarray使用者的連結 SSO 身分

roles

宣告名稱類型描述需要使用者資訊嗎?
rolesstring[]使用者的角色

urn:logto:scope:organizations

宣告名稱類型描述需要使用者資訊嗎?
organizationsstring[]使用者所屬的組織 ID
organization_dataobject[]使用者所屬的組織資料

urn:logto:scope:organization_roles

宣告名稱類型描述需要使用者資訊嗎?
organization_rolesstring[]使用者所屬的組織角色,格式為 <organization_id>:<role_name>

考慮到效能和資料大小,如果「需要使用者資訊嗎?」為「是」,則表示該宣告不會顯示在 ID 權杖中,而會在 使用者資訊端點 回應中返回。

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 標頭。

延伸閱讀

終端使用者流程:驗證流程、帳戶流程與組織流程 配置連接器 保護你的 API