اصالت‌سنجی با Firebase در افزونه Chrome

این سند نحوه استفاده از Firebase Authentication را برای ورود کاربران به سیستم افزونه Chrome که از Manifest V3 استفاده می‌کند نشان می‌دهد.

‫Firebase Authentication روش‌های اصالت‌سنجی متعددی برای ورود به سیستم کاربران از افزونه Chrome ارائه می‌دهد که برخی‌از آن‌ها نسبت به بقیه به تلاش توسعه بیشتری نیاز دارند.

برای استفاده از روش‌های زیر در افزونه Chrome با «مانیفست نسخه ۳»، فقط باید آن‌ها را از firebase/auth/web-extension وارد کنید:

  • ورود به سیستم با ایمیل و گذرواژه (createUserWithEmailAndPassword و signInWithEmailAndPassword)
  • ورود به سیستم با پیوند ایمیل (sendSignInLinkToEmail،‏ isSignInWithEmailLink، و signInWithEmailLink)
  • ورود به سیستم به‌صورت ناشناس (signInAnonymously)
  • ورود به سیستم با سیستم اصالت‌سنجی سفارشی (signInWithCustomToken)
  • ورود به سیستم ارائه‌دهنده را به‌طور مستقل مدیریت کنید، سپس از signInWithCredential استفاده کنید

روش‌های ورود به سیستم زیر نیز پشتیبانی می‌شوند اما به کار اضافی نیاز دارند:

  • ورود به سیستم با پنجره بالاپر (signInWithPopup،‏ linkWithPopup، و reauthenticateWithPopup)
  • با هدایت شدن به صفحه ورود به سیستم وارد شوید (signInWithRedirect،‏ linkWithRedirect، و reauthenticateWithRedirect)
  • ورود به سیستم با شماره تلفن با reCAPTCHA
  • اصالت‌سنجی چندعاملی پیامکی با reCAPTCHA
  • محافظت reCAPTCHA Enterprise

برای استفاده از این روش‌ها در افزونه Chrome با «مانیفست نسخه ۳»، باید از اسناد خارج از صفحه استفاده کنید.

از نقطه ورود firebase/auth/web-extension استفاده کنید

وارد کردن از firebase/auth/web-extension باعث می‌شود ورود به سیستم کاربران از افزونه Chrome مشابه برنامه وب باشد.

‫firebase/auth/web-extension فقط در نسخه‌های v10.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 سازگار نیستند، زیرا این روش‌ها نیازمند بار شدن کد از خارج بسته افزونه هستند. از «مانیفست نسخه ۳» به بعد، این کار مجاز نیست و پلاتفرم افزونه آن را مسدود می‌کند. برای دور زدن این محدودیت، می‌توانید آن کد را در یک 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 خود را به فهرست دامنه‌های مجاز اضافه کنید:

  1. در کنسول Firebase، به امنیت > اصالت‌سنجی بروید.

  2. در برگه تنظیمات، در بخش دامنه‌های مجاز، روی افزودن دامنه کلیک کنید، سپس یک شناسه منبع یکنواخت (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 کد جاوا اسکریپتی است که احراز هویت را مدیریت می‌کند. دو روش مختلف برای پیاده‌سازی روشی که مستقیماً در افزونه پشتیبانی می‌شود وجود دارد:

  • از firebase/auth/web-extension در کد افزونه‌تان مثل نوشتارهای پس‌زمینه‌ای، کارگران سرویس، یا نوشتارهای بالاپری استفاده کنید. فقط در iframe خارج از صفحه از firebase/auth استفاده کنید، زیرا آن 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 را در بسته افزونه‌تان قرار دهید. این صفحه به‌عنوان پراکسی بین وب‌سایت عمومی که در مرحله ۱ راه‌اندازی شده است و افزونه شما عمل می‌کند.
  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 راه‌اندازی کنید.
  8.     import { getAuth } from 'firebase/auth/web-extension';
    
        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 شما به کارمند سرویس شما وکالت داده می‌شود.