В этом документе показано, как использовать Firebase Authentication для авторизации пользователей в расширении Chrome, использующем Manifest V3 .
Firebase Authentication предоставляет несколько методов аутентификации для входа пользователей из расширения Chrome, некоторые из которых требуют больших усилий по разработке, чем другие.
Для использования следующих методов в расширении Chrome с Manifest V3 достаточно импортировать их из firebase/auth/web-extension :
- Войдите в систему, используя адрес электронной почты и пароль (
createUserWithEmailAndPasswordиsignInWithEmailAndPassword). - Войти по ссылке из электронной почты (
sendSignInLinkToEmail,isSignInWithEmailLinkиsignInWithEmailLink) - Войти анонимно (
signInAnonymously) - Вход с использованием собственной системы аутентификации (
signInWithCustomToken) - Обрабатывайте вход в систему от имени поставщика услуг независимо, а затем используйте
signInWithCredential
Поддерживаются также следующие способы входа в систему, но они требуют дополнительной доработки:
- Вход через всплывающее окно (
signInWithPopup,linkWithPopupиreauthenticateWithPopup) - Для входа в систему необходимо перенаправить пользователя на страницу авторизации (используя
signInWithRedirect,linkWithRedirectиreauthenticateWithRedirect). - Войти с помощью номера телефона и reCAPTCHA
- Многофакторная аутентификация по SMS с использованием reCAPTCHA
- reCAPTCHA Enterprise protection
Для использования этих методов в расширении Chrome Manifest V3 необходимо использовать Offscreen Documents .
Используйте точку входа firebase/auth/web-extension.
Импорт из firebase/auth/web-extension делает вход пользователей через расширение Chrome аналогичным входу в веб-приложение.
Функция firebase/auth/web-extension поддерживается только в версиях Web SDK 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, поскольку требуют загрузки кода извне пакета расширения. Начиная с Manifest V3, это запрещено и будет блокироваться платформой расширений. Чтобы обойти это ограничение, вы можете загрузить этот код внутри iframe, используя документ, находящийся вне экрана . В документе, находящемся вне экрана, реализуйте обычный процесс аутентификации и перенаправьте результат из документа обратно в расширение.
В этом руководстве в качестве примера используется signInWithPopup , но тот же принцип применим и к другим методам аутентификации.
Прежде чем начать
Этот метод требует создания веб-страницы, доступной в интернете, которую вы будете загружать в iframe. Для этого подойдёт любой хостинг, включая Firebase Hosting . Создайте веб-сайт со следующим содержимым:
<!DOCTYPE html>
<html>
<head>
<title>signInWithPopup</title>
<script src="signInWithPopup.js"></script>
</head>
<body><h1>signInWithPopup</h1></body>
</html>Федеративный вход
Если вы используете федеративный вход в систему, например, вход через Google, Apple, SAML или OIDC, вам необходимо добавить идентификатор вашего расширения Chrome в список авторизованных доменов:
В консоли Firebase перейдите в раздел Безопасность > Аутентификация .
На вкладке «Настройки» , в разделе «Авторизованные домены» , нажмите «Добавить домен» , а затем добавьте URI следующего вида:
chrome-extension://CHROME_EXTENSION_ID
В файле манифеста вашего расширения Chrome убедитесь, что вы добавили следующие URL-адреса в список разрешенных content_security_policy :
-
https://apis.google.com -
https://www.gstatic.com -
https://www.googleapis.com -
https://securetoken.googleapis.com
Внедрить аутентификацию
В вашем HTML-документе файл signInWithPopup.js — это JavaScript-код, обрабатывающий аутентификацию. Существует два разных способа реализовать метод, который напрямую поддерживается расширением:
- Используйте
firebase/auth/web-extensionв коде вашего расширения, например, в фоновых скриптах, сервис-воркерах или скриптах всплывающих окон. Используйтеfirebase/authтолько внутри iframe, расположенного вне видимой области экрана, поскольку этот 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.
- Добавьте разрешение
offscreenв файл manifest.json: - Создайте сам документ, отображаемый вне экрана. Это минимальный HTML-файл внутри вашего пакета расширения, который загружает логику JavaScript вашего документа, отображаемого вне экрана:
- Включите
offscreen.jsв пакет вашего расширения. Он будет выступать в качестве прокси-сервера между общедоступным веб-сайтом, настроенным на шаге 1, и вашим расширением. - Настройте документ, находящийся вне экрана, с помощью сервис-воркера 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; }
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 в ваш сервис-воркер, используя документ вне экрана.