Autenticar com o Firebase em uma extensão do Chrome

Este documento mostra como usar o Firebase Authentication para conectar usuários a uma extensão do Chrome que usa o Manifest V3 .

O Firebase Authentication oferece vários métodos de autenticação para fazer login de usuários a partir de uma extensão do Chrome, alguns exigindo mais esforço de desenvolvimento do que outros.

Para usar os seguintes métodos em uma extensão Manifest V3 do Chrome, você só precisa importá-los de firebase/auth/web-extension :

  • Faça login com e-mail e senha ( createUserWithEmailAndPassword e signInWithEmailAndPassword )
  • Faça login com link de e-mail ( sendSignInLinkToEmail , isSignInWithEmailLink e signInWithEmailLink )
  • Faça login anonimamente ( signInAnonymously )
  • Faça login com um sistema de autenticação personalizado ( signInWithCustomToken )
  • Lide com a entrada do provedor de forma independente e use signInWithCredential

Os seguintes métodos de login também são suportados, mas requerem algum trabalho adicional:

  • Faça login com uma janela pop-up ( signInWithPopup , linkWithPopup e reauthenticateWithPopup )
  • Faça login redirecionando para a página de login ( signInWithRedirect , linkWithRedirect e reauthenticateWithRedirect )
  • Faça login com número de telefone com reCAPTCHA
  • Autenticação multifator SMS com reCAPTCHA
  • Proteção empresarial reCAPTCHA

Para usar esses métodos em uma extensão Manifest V3 do Chrome, você deve usar Offscreen Documents .

Use o ponto de entrada firebase/auth/web-extension

A importação de firebase/auth/web-extension torna o login de usuários de uma extensão do Chrome semelhante a um aplicativo da web.

firebase/auth/web-extension é compatível apenas com as versões do Web SDK v10.8.0 e 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;
  });

Use documentos fora da tela

Alguns métodos de autenticação, como signInWithPopup , linkWithPopup e reauthenticateWithPopup , não são diretamente compatíveis com extensões do Chrome porque exigem que o código seja carregado de fora do pacote de extensão. A partir do Manifest V3, isso não é permitido e será bloqueado pela plataforma de extensão. Para contornar isso, você pode carregar esse código dentro de um iframe usando um documento fora da tela . No documento fora da tela, implemente o fluxo de autenticação normal e faça proxy do resultado do documento fora da tela de volta para a extensão.

Este guia usa signInWithPopup como exemplo, mas o mesmo conceito se aplica a outros métodos de autenticação.

Antes de você começar

Essa técnica exige que você configure uma página da web que esteja disponível na web, que será carregada em um iframe. Qualquer host funciona para isso, inclusive o Firebase Hosting . Crie um site com o seguinte conteúdo:

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

Login federado

Se você estiver usando login federado, como login com Google, Apple, SAML ou OIDC, deverá adicionar seu ID de extensão do Chrome à lista de domínios autorizados:

  1. Abra seu projeto no console do Firebase .
  2. Na seção Autenticação , abra a página Configurações .
  3. Adicione um URI como o seguinte à lista de domínios autorizados:
    chrome-extension://CHROME_EXTENSION_ID

No arquivo de manifesto da sua extensão do Chrome, certifique-se de adicionar os seguintes URLs à lista de permissões content_security_policy :

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

Implementar autenticação

No seu documento HTML, signInWithPopup.js é o código JavaScript que trata da autenticação. Existem duas maneiras diferentes de implementar um método que é suportado diretamente na extensão:

  • Use firebase/auth em vez de firebase/auth/web-extension . O ponto de entrada web-extension é para código em execução na extensão. Embora esse código eventualmente seja executado na extensão (em um iframe, em seu documento fora da tela), o contexto em que ele está sendo executado é o web padrão.
  • Envolva a lógica de autenticação em um ouvinte postMessage , para fazer proxy da solicitação e resposta de autenticação.
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)
  }
});

Crie sua extensão do Chrome

Depois que seu site estiver no ar, você poderá usá-lo em sua extensão do Chrome.

  1. Adicione a permissão offscreen ao seu arquivo manifest.json:
  2.     {
          "name": "signInWithPopup Demo",
          "manifest_version" 3,
          "background": {
            "service_worker": "background.js"
          },
          "permissions": [
            "offscreen"
          ]
        }
        
  3. Crie o próprio documento fora da tela. Este é um arquivo HTML mínimo dentro do seu pacote de extensão que carrega a lógica do seu documento JavaScript fora da tela:
  4.     <!DOCTYPE html>
        <script src="./offscreen.js"></script>
        
  5. Inclua offscreen.js em seu pacote de extensão. Ele atua como proxy entre o site público configurado na etapa 1 e sua extensão.
  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 o documento fora da tela do seu 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;
        }
        

    Agora, quando você chamar firebaseAuth() em seu service worker, ele criará o documento fora da tela e carregará o site em um iframe. Esse iframe será processado em segundo plano e o Firebase passará pelo fluxo de autenticação padrão. Depois de resolvido ou rejeitado, o objeto de autenticação será enviado por proxy do seu iframe para o seu service worker, usando o documento fora da tela.