本文說明如何使用 Identity Platform,讓使用者登入使用 Manifest V3 的 Chrome 擴充功能。
Identity Platform 提供多種驗證方法,可透過 Chrome 擴充功能讓使用者登入,其中有些方法需要比其他方法投入更多開發作業。
如要在 Manifest V3 Chrome 擴充功能中使用下列方法,您只需從 firebase/auth/web-extension
匯入這些方法:
- 使用電子郵件地址和密碼登入 (
createUserWithEmailAndPassword
和signInWithEmailAndPassword
) - 使用電子郵件連結登入 (
sendSignInLinkToEmail
、isSignInWithEmailLink
和signInWithEmailLink
) - 匿名登入 (
signInAnonymously
) - 使用自訂驗證系統登入 (
signInWithCustomToken
) - 獨立處理供應商登入作業,然後使用
signInWithCredential
系統也支援下列登入方式,但需要額外處理:
- 使用彈出式視窗登入 (
signInWithPopup
、linkWithPopup
和reauthenticateWithPopup
) - 重新導向至登入頁面 (
signInWithRedirect
、linkWithRedirect
和reauthenticateWithRedirect
) 以便登入 - 使用 reCAPTCHA 搭配電話號碼登入
- 使用 reCAPTCHA 進行簡訊多重驗證
- reCAPTCHA Enterprise 防護功能
如要在 Manifest V3 Chrome 擴充功能中使用這些方法,您必須使用螢幕外文件。
使用 firebase/auth/web-extension 進入點
從 firebase/auth/web-extension
匯入後,使用者就能透過 Chrome 擴充功能登入,就像是使用網頁應用程式一樣。
firebase/auth/web-extension 僅支援 Web SDK 10.8.0 以上版本。
import { getAuth, signInWithEmailAndPassword } from 'firebase/auth/web-extension'; const auth = getAuth(); signInWithEmailAndPassword(auth, email, password) .then((userCredential) => { // Signed in const user = userCredential.user; // ... }) .catch((error) => { const errorCode = error.code; const errorMessage = error.message; });
使用螢幕外文件
部分驗證方法 (例如 signInWithPopup
、linkWithPopup
和 reauthenticateWithPopup
) 與 Chrome 擴充功能不相容,因為這些方法需要從擴充功能套件外部載入程式碼。自 Manifest V3 起,這項操作已遭禁止,且會遭到擴充功能平台封鎖。如要解決這個問題,您可以使用離線文件,在 iframe 中載入該程式碼。在螢幕外文件中,實作一般驗證流程,並將螢幕外文件的結果代理回擴充功能。
本指南以 signInWithPopup
做為範例,但其他驗證方法也適用相同的概念。
事前準備
使用這項技巧時,您必須設定可在網路上使用的網頁,並載入至 iframe。任何主機都可以用於此用途,包括 Firebase 託管。建立含有以下內容的網站:
<!DOCTYPE html> <html> <head> <title>signInWithPopup</title> <script src="signInWithPopup.js"></script> </head> <body><h1>signInWithPopup</h1></body> </html>
聯合登入
如果您使用聯合登入功能 (例如使用 Google、Apple、SAML 或 OIDC 登入),請務必將 Chrome 擴充功能 ID 新增至授權網域清單:
前往Google Cloud 控制台的「Identity Platform」「Settings」(設定) 頁面。
選取「安全性」分頁標籤。
在「已授權的網域」下方,按一下「新增網域」。
輸入擴充功能的 URI。應該會類似
chrome-extension://CHROME_EXTENSION_ID
。按一下「新增」。
請在 Chrome 擴充功能的資訊清單檔案中,將下列網址新增至 content_security_policy
許可清單:
https://apis.google.com
https://www.gstatic.com
https://www.googleapis.com
https://securetoken.googleapis.com
導入驗證機制
在 HTML 文件中,signInWithPopup.js 是負責處理驗證的 JavaScript 程式碼。您可以透過兩種方式實作擴充功能直接支援的方法:
- 請使用
firebase/auth
,而非firebase/auth/web-extension
。web-extension
進入點適用於在擴充功能中執行的程式碼。雖然這段程式碼最終會在擴充功能 (在 iframe 中,在離螢幕外的文件中) 執行,但執行的環境是標準網頁。 - 將驗證邏輯包裝在
postMessage
事件監聽器中,以便代理驗證要求和回應。
import { signInWithPopup, GoogleAuthProvider, getAuth } from'firebase/auth'; import { initializeApp } from 'firebase/app'; import firebaseConfig from './firebaseConfig.js' const app = initializeApp(firebaseConfig); const auth = getAuth(); // This code runs inside of an iframe in the extension's offscreen document. // This gives you a reference to the parent frame, i.e. the offscreen document. // You will need this to assign the targetOrigin for postMessage. const PARENT_FRAME = document.location.ancestorOrigins[0]; // This demo uses the Google auth provider, but any supported provider works. // Make sure that you enable any provider you want to use in the Firebase Console. // https://console.firebase.google.com/project/_/authentication/providers const PROVIDER = new GoogleAuthProvider(); function sendResponse(result) { globalThis.parent.self.postMessage(JSON.stringify(result), PARENT_FRAME); } globalThis.addEventListener('message', function({data}) { if (data.initAuth) { // Opens the Google sign-in page in a popup, inside of an iframe in the // extension's offscreen document. // To centralize logic, all respones are forwarded to the parent frame, // which goes on to forward them to the extension's service worker. signInWithPopup(auth, PROVIDER) .then(sendResponse) .catch(sendResponse) } });
建構 Chrome 擴充功能
網站上線後,您就可以在 Chrome 擴充功能中使用這項功能。
- 將
offscreen
權限新增至 manifest.json 檔案: - 建立螢幕外文件本身。這是擴充功能套件中的最小 HTML 檔案,可載入螢幕外文件 JavaScript 的邏輯:
- 在擴充功能套件中加入
offscreen.js
。它會充當在第 1 步驟中設定的公用網站與擴充功能之間的 Proxy。 - 透過 background.js 服務 worker 設定離螢幕外的文件。
{ "name": "signInWithPopup Demo", "manifest_version" 3, "background": { "service_worker": "background.js" }, "permissions": [ "offscreen" ] }
<!DOCTYPE html> <script src="./offscreen.js"></script>
// This URL must point to the public site const _URL = 'https://example.com/signInWithPopupExample'; const iframe = document.createElement('iframe'); iframe.src = _URL; document.documentElement.appendChild(iframe); chrome.runtime.onMessage.addListener(handleChromeMessages); function handleChromeMessages(message, sender, sendResponse) { // Extensions may have an number of other reasons to send messages, so you // should filter out any that are not meant for the offscreen document. if (message.target !== 'offscreen') { return false; } function handleIframeMessage({data}) { try { if (data.startsWith('!_{')) { // Other parts of the Firebase library send messages using postMessage. // You don't care about them in this context, so return early. return; } data = JSON.parse(data); self.removeEventListener('message', handleIframeMessage); sendResponse(data); } catch (e) { console.log(`json parse failed - ${e.message}`); } } globalThis.addEventListener('message', handleIframeMessage, false); // Initialize the authentication flow in the iframed document. You must set the // second argument (targetOrigin) of the message in order for it to be successfully // delivered. iframe.contentWindow.postMessage({"initAuth": true}, new URL(_URL).origin); return true; }
const OFFSCREEN_DOCUMENT_PATH = '/offscreen.html'; // A global promise to avoid concurrency issues let creatingOffscreenDocument; // Chrome only allows for a single offscreenDocument. This is a helper function // that returns a boolean indicating if a document is already active. async function hasDocument() { // Check all windows controlled by the service worker to see if one // of them is the offscreen document with the given path const matchedClients = await clients.matchAll(); return matchedClients.some( (c) => c.url === chrome.runtime.getURL(OFFSCREEN_DOCUMENT_PATH) ); } async function setupOffscreenDocument(path) { // If we do not have a document, we are already setup and can skip if (!(await hasDocument())) { // create offscreen document if (creating) { await creating; } else { creating = chrome.offscreen.createDocument({ url: path, reasons: [ chrome.offscreen.Reason.DOM_SCRAPING ], justification: 'authentication' }); await creating; creating = null; } } } async function closeOffscreenDocument() { if (!(await hasDocument())) { return; } await chrome.offscreen.closeDocument(); } function getAuth() { return new Promise(async (resolve, reject) => { const auth = await chrome.runtime.sendMessage({ type: 'firebase-auth', target: 'offscreen' }); auth?.name !== 'FirebaseError' ? resolve(auth) : reject(auth); }) } async function firebaseAuth() { await setupOffscreenDocument(OFFSCREEN_DOCUMENT_PATH); const auth = await getAuth() .then((auth) => { console.log('User Authenticated', auth); return auth; }) .catch(err => { if (err.code === 'auth/operation-not-allowed') { console.error('You must enable an OAuth provider in the Firebase' + ' console in order to use signInWithPopup. This sample' + ' uses Google by default.'); } else { console.error(err); return err; } }) .finally(closeOffscreenDocument) return auth; }
現在,當您在服務工作者中呼叫 firebaseAuth()
時,系統會建立離螢幕外的文件,並在 iframe 中載入網站。該 iframe 會在背景處理,而 Firebase 會執行標準驗證流程。解決或拒絕後,系統會使用離線文件,將驗證物件從 iframe 代理至服務工作者。