Autenticarse con Firebase en una extensión de Chrome

Este documento le muestra cómo usar Firebase Authentication para iniciar sesión en una extensión de Chrome que usa Manifest V3 .

Firebase Authentication proporciona múltiples métodos de autenticación para que los usuarios inicien sesión desde una extensión de Chrome, algunos de los cuales requieren más esfuerzo de desarrollo que otros.

Para utilizar los siguientes métodos en una extensión de Chrome Manifest V3, solo necesita importarlos desde firebase/auth/web-extension :

  • Inicie sesión con correo electrónico y contraseña ( createUserWithEmailAndPassword y signInWithEmailAndPassword )
  • Inicie sesión con un enlace de correo electrónico ( sendSignInLinkToEmail , isSignInWithEmailLink y signInWithEmailLink )
  • Iniciar sesión de forma anónima ( signInAnonymously )
  • Inicie sesión con un sistema de autenticación personalizado ( signInWithCustomToken )
  • Maneje el inicio de sesión del proveedor de forma independiente y luego use signInWithCredential

Los siguientes métodos de inicio de sesión también son compatibles, pero requieren trabajo adicional:

  • Inicie sesión con una ventana emergente ( signInWithPopup , linkWithPopup y reauthenticateWithPopup )
  • Inicie sesión redirigiendo a la página de inicio de sesión ( signInWithRedirect , linkWithRedirect y reauthenticateWithRedirect )
  • Iniciar sesión con número de teléfono con reCAPTCHA
  • Autenticación multifactor por SMS con reCAPTCHA
  • Protección empresarial reCAPTCHA

Para utilizar estos métodos en una extensión de Chrome Manifest V3, debe utilizar Documentos fuera de pantalla .

Utilice el punto de entrada firebase/auth/web-extension

Importar desde firebase/auth/web-extension hace que el inicio de sesión de los usuarios desde una extensión de Chrome sea similar a una aplicación web.

firebase/auth/web-extension solo se admite en las versiones del SDK web v10.8.0 y superiores.

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

Utilice documentos fuera de pantalla

Algunos métodos de autenticación, como signInWithPopup , linkWithPopup y reauthenticateWithPopup , no son directamente compatibles con las extensiones de Chrome porque requieren que el código se cargue desde fuera del paquete de extensión. A partir de Manifest V3, esto no está permitido y será bloqueado por la plataforma de extensión. Para solucionar esto, puede cargar ese código dentro de un iframe usando un documento fuera de la pantalla . En el documento fuera de pantalla, implemente el flujo de autenticación normal y envíe el resultado del documento fuera de pantalla a la extensión.

Esta guía utiliza signInWithPopup como ejemplo, pero el mismo concepto se aplica a otros métodos de autenticación.

Antes de que empieces

Esta técnica requiere que configures una página web que esté disponible en la web, que cargarás en un iframe. Cualquier host sirve para esto, incluido Firebase Hosting . Crea un sitio web con el siguiente contenido:

<!DOCTYPE html>
<html>
  <head>
    <title>signInWithPopup</title>
    <script src="signInWithPopup.js"></script>
  </head>
  <body><h1>signInWithPopup</h1></body>
</html>

Inicio de sesión federado

Si utiliza el inicio de sesión federado, como el inicio de sesión con Google, Apple, SAML u OIDC, debe agregar su ID de extensión de Chrome a la lista de dominios autorizados:

  1. Abra su proyecto en Firebase console .
  2. En la sección Autenticación , abra la página Configuración .
  3. Agregue un URI como el siguiente a la lista de dominios autorizados:
    chrome-extension://CHROME_EXTENSION_ID

En el archivo de manifiesto de su extensión de Chrome, asegúrese de agregar las siguientes URL a la lista de permitidos content_security_policy :

  • https://apis.google.com
  • https://www.gstatic.com
  • https://www.googleapis.com
  • https://securetoken.googleapis.com

Implementar autenticación

En su documento HTML, signInWithPopup.js es el código JavaScript que maneja la autenticación. Hay dos formas diferentes de implementar un método directamente compatible con la extensión:

  • Utilice firebase/auth en lugar de firebase/auth/web-extension . El punto de entrada web-extension es para el código que se ejecuta dentro de la extensión. Si bien este código finalmente se ejecuta en la extensión (en un iframe, en su documento fuera de la pantalla), el contexto en el que se ejecuta es el web estándar.
  • Envuelva la lógica de autenticación en un oyente postMessage para representar la solicitud y la respuesta de autenticación.
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)
  }
});

Crea tu extensión de Chrome

Una vez que su sitio web esté activo, podrá usarlo en su extensión de Chrome.

  1. Agregue el permiso offscreen a su archivo manifest.json:
  2.     {
          "name": "signInWithPopup Demo",
          "manifest_version" 3,
          "background": {
            "service_worker": "background.js"
          },
          "permissions": [
            "offscreen"
          ]
        }
        
  3. Crea el documento fuera de pantalla. Este es un archivo HTML mínimo dentro de su paquete de extensión que carga la lógica de su documento JavaScript fuera de pantalla:
  4.     <!DOCTYPE html>
        <script src="./offscreen.js"></script>
        
  5. Incluya offscreen.js en su paquete de extensión. Actúa como proxy entre el sitio web público configurado en el paso 1 y su extensión.
  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. Configure el documento fuera de pantalla desde su trabajador del servicio background.js.
  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;
        }
        

    Ahora, cuando llame firebaseAuth() dentro de su trabajador de servicio, creará el documento fuera de la pantalla y cargará el sitio en un iframe. Ese iframe se procesará en segundo plano y Firebase pasará por el flujo de autenticación estándar. Una vez que se haya resuelto o rechazado, el objeto de autenticación se enviará desde su iframe a su trabajador de servicio, utilizando el documento fuera de la pantalla.