이 문서에서는 Firebase 인증을 사용하여 Manifest V3를 사용하는 Chrome 확장 프로그램에 사용자 로그인하는 방법을 설명합니다.
Firebase 인증은 Chrome 확장 프로그램에서 사용자를 로그인 처리할 수 있는 여러 가지 인증 방법을 제공하며 일부는 다른 방법보다 개발 노력이 더 많이 필요합니다.
Manifest V3 Chrome 확장 프로그램에서 다음 방법을 사용하려면 firebase/auth/web-extension
에서 가져오기만 하면 됩니다.
- 이메일과 비밀번호로 로그인(
createUserWithEmailAndPassword
,signInWithEmailAndPassword
) - 이메일 링크로 로그인(
sendSignInLinkToEmail
,isSignInWithEmailLink
,signInWithEmailLink
) - 익명으로 로그인(
signInAnonymously
) - 커스텀 인증 시스템으로 로그인(
signInWithCustomToken
) - 제공업체 로그인을 임의로 처리한 후
signInWithCredential
사용
다음 로그인 방법도 지원되지만 몇 가지 추가 작업이 필요합니다.
- 팝업 창을 사용해 로그인(
signInWithPopup
,linkWithPopup
,reauthenticateWithPopup
) - 로그인 페이지로 리디렉션해서 로그인(
signInWithRedirect
,linkWithRedirect
,reauthenticateWithRedirect
) - reCAPTCHA를 사용하여 전화번호로 로그인
- reCAPTCHA를 사용하여 SMS 다중 인증(MFA)
- reCAPTCHA Enterprise 보호
Manifest V3 Chrome 확장 프로그램에서 이러한 메서드를 사용하려면 오프스크린 문서를 사용해야 합니다.
firebase/auth/web-extension 진입점 사용
firebase/auth/web-extension
에서 가져오면 웹 앱과 유사한 Chrome 확장 프로그램에서 사용자가 로그인할 수 있습니다.
firebase/auth/web-extension은 웹 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 호스팅을 포함한 모든 호스트가 가능합니다. 다음 콘텐츠가 있는 웹사이트를 만듭니다.
<!DOCTYPE html> <html> <head> <title>signInWithPopup</title> <script src="signInWithPopup.js"></script> </head> <body><h1>signInWithPopup</h1></body> </html>
제휴 로그인
Google 계정, Apple, SAML, OIDC로 로그인과 같은 제휴 로그인을 사용하는 경우 승인된 도메인 목록에 Chrome 확장 프로그램 ID를 추가해야 합니다.
- Firebase Console에서 프로젝트를 엽니다.
- 인증 섹션에서 설정 페이지를 엽니다.
- 승인된 도메인 목록에 다음 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
를 사용합니다.web-extension
진입점은 확장 프로그램 내에서 실행되는 코드에 사용됩니다. 이 코드는 최종적으로 확장 프로그램(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 확장 프로그램에서 사용할 수 있습니다.
- manifest.json 파일에
offscreen
권한을 추가합니다. - 오프스크린 문서를 만듭니다. 이는 확장 프로그램 패키지 내부에 있는 최소 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; }
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에서 서비스 워커로 프록시됩니다.