Autentica con Firebase en una extensión de Chrome

En este documento, se muestra cómo usar Firebase Authentication para hacer que los usuarios accedan a una extensión de Chrome que usa Manifest V3.

Firebase Authentication proporciona varios métodos de autenticación para que los usuarios accedan desde una extensión de Chrome. Algunos requieren más esfuerzo de desarrollo que otros.

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

  • Acceder con un correo electrónico y una contraseña (createUserWithEmailAndPassword y signInWithEmailAndPassword)
  • Acceder con un vínculo de correo electrónico (sendSignInLinkToEmail, isSignInWithEmailLink y signInWithEmailLink)
  • Acceder de manera anónima (signInAnonymously)
  • Acceder con un sistema de autenticación personalizado (signInWithCustomToken)
  • Controlar el acceso de proveedor de forma independiente y, luego, usar signInWithCredential

También se admiten los siguientes métodos de acceso, pero requieren esfuerzo adicional:

  • Acceder con una ventana emergente (signInWithPopup, linkWithPopup y reauthenticateWithPopup)
  • Acceder redireccionando a la página de acceso (signInWithRedirect, linkWithRedirect y reauthenticateWithRedirect)
  • Acceso con un número de teléfono y reCAPTCHA
  • Autenticación de varios factores a través de SMS con reCAPTCHA
  • Protección de reCAPTCHA Enterprise

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

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

La importación desde firebase/auth/web-extension hace que los usuarios que accedan desde una extensión de Chrome sean similares a una app web.

firebase/auth/web-extension solo es compatible con las versiones 10.8.0 y posteriores del SDK web.

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

Usa 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 se cargue código desde fuera del paquete de extensiones. A partir de Manifest V3, esto no está permitido y la plataforma de extensiones lo bloqueará. Para solucionar este problema, puedes cargar ese código dentro de un iframe con un documento fuera de pantalla. En el documento fuera de pantalla, implementa el flujo de autenticación habitual y usa un proxy en el resultado del documento fuera de pantalla para volver a la extensión.

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

Antes de comenzar

Esta técnica requiere que configures una página web que esté disponible en la Web y que cargues en un iframe. Cualquier host funciona 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>

Acceso federado

Si usas el acceso federado, como Acceder con Google, Apple, OIDC o SAML, debes agregar el ID de la extensión de Chrome a la lista de dominios autorizados:

  1. Abre el proyecto en Firebase console.
  2. En la sección Authentication, abre la página Configuración.
  3. Agrega un URI como el siguiente a la lista de dominios autorizados:
    chrome-extension://CHROME_EXTENSION_ID

En el archivo de manifiesto de tu extensión de Chrome, asegúrate de agregar las siguientes URLs a la lista de entidades permitidas content_security_policy:

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

Implementa la autenticación

En tu documento HTML, signInWithPopup.js es el código JavaScript que controla la autenticación. Hay dos maneras diferentes de implementar un método que se admita directamente en la extensión:

  • Usa 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 tu documento fuera de pantalla), el contexto en el que se ejecuta es el sitio web estándar.
  • Une la lógica de autenticación en un objeto de escucha postMessagepostMessage para poder usar un proxy en 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 el sitio web esté activo, podrás usarlo en la extensión de Chrome.

  1. Agrega el permiso offscreen al archivo manifest.json:
  2.     {
          "name": "signInWithPopup Demo",
          "manifest_version" 3,
          "background": {
            "service_worker": "background.js"
          },
          "permissions": [
            "offscreen"
          ]
        }
        
  3. Crea el documento fuera de pantalla. Se trata de un archivo HTML mínimo dentro del paquete de extensión que carga la lógica del código JavaScript del documento fuera de pantalla:
  4.     <!DOCTYPE html>
        <script src="./offscreen.js"></script>
        
  5. Incluye offscreen.js en tu paquete de extensión. Funciona como el proxy entre el sitio web público configurado en el paso 1 y tu 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. Por último, configura el documento fuera de pantalla desde tu service worker 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 llames a firebaseAuth() en tu service worker, se creará el documento fuera de pantalla y se 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á a través de proxy desde tu iframe hasta tu service worker usando el documento fuera de pantalla.