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
ysignInWithEmailAndPassword
) - Acceder con un vínculo de correo electrónico (
sendSignInLinkToEmail
,isSignInWithEmailLink
ysignInWithEmailLink
) - 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
yreauthenticateWithPopup
) - Acceder redireccionando a la página de acceso (
signInWithRedirect
,linkWithRedirect
yreauthenticateWithRedirect
) - 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:
- Abre tu proyecto en la Firebase console.
- En la sección Authentication, abre la página Configuración.
- 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 defirebase/auth/web-extension
. El punto de entradaweb-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 postMessage
postMessage
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.
- Agrega el permiso
offscreen
al archivo manifest.json: - 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:
- 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. - Por último, configura el documento fuera de pantalla desde tu service worker background.js.
{ "name": "signInWithPopup Demo", "manifest_version" 3, "background": { "service_worker": "background.js" }, "permissions": [ "offscreen" ] }
<!DOCTYPE html> <script src="./offscreen.js"></script>
// 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; }
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.