透過 Chrome 擴充功能登入使用者

本文說明如何使用 Identity Platform,讓使用者登入使用 Manifest V3 的 Chrome 擴充功能。

Identity Platform 提供多種驗證方法,可透過 Chrome 擴充功能讓使用者登入,其中有些方法需要比其他方法投入更多開發作業。

如要在 Manifest V3 Chrome 擴充功能中使用下列方法,您只需firebase/auth/web-extension 匯入這些方法

  • 使用電子郵件地址和密碼登入 (createUserWithEmailAndPasswordsignInWithEmailAndPassword)
  • 使用電子郵件連結登入 (sendSignInLinkToEmailisSignInWithEmailLinksignInWithEmailLink)
  • 匿名登入 (signInAnonymously)
  • 使用自訂驗證系統登入 (signInWithCustomToken)
  • 獨立處理供應商登入作業,然後使用 signInWithCredential

系統也支援下列登入方式,但需要額外處理:

  • 使用彈出式視窗登入 (signInWithPopuplinkWithPopupreauthenticateWithPopup)
  • 重新導向至登入頁面 (signInWithRedirectlinkWithRedirectreauthenticateWithRedirect) 以便登入
  • 使用 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;
  });

使用螢幕外文件

部分驗證方法 (例如 signInWithPopuplinkWithPopupreauthenticateWithPopup) 與 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 新增至授權網域清單:

  1. 前往Google Cloud 控制台的「Identity Platform」「Settings」(設定) 頁面。

    前往「設定」頁面

  2. 選取「安全性」分頁標籤。

  3. 在「已授權的網域」下方,按一下「新增網域」

  4. 輸入擴充功能的 URI。應該會類似 chrome-extension://CHROME_EXTENSION_ID

  5. 按一下「新增」。

請在 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-extensionweb-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 擴充功能中使用這項功能。

  1. offscreen 權限新增至 manifest.json 檔案:
  2.     {
          "name": "signInWithPopup Demo",
          "manifest_version" 3,
          "background": {
            "service_worker": "background.js"
          },
          "permissions": [
            "offscreen"
          ]
        }
        
  3. 建立螢幕外文件本身。這是擴充功能套件中的最小 HTML 檔案,可載入螢幕外文件 JavaScript 的邏輯:
  4.     <!DOCTYPE html>
        <script src="./offscreen.js"></script>
        
  5. 在擴充功能套件中加入 offscreen.js。它會充當在第 1 步驟中設定的公用網站與擴充功能之間的 Proxy。
  6.     // 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;
        }
        
  7. 透過 background.js 服務 worker 設定離螢幕外的文件。
  8.     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 代理至服務工作者。