このドキュメントでは、Identity Platform を使用してユーザーを Manifest V3 を使用する Chrome 拡張機能にログインさせる方法について説明します。
Identity Platform には、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 多要素認証
- reCAPTCHA Enterprise 保護
Manifest V3 Chrome 拡張機能でこれらの方法を使用するには、画面外ドキュメントを使用する必要があります。
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 拡張機能 ID を承認済みドメインのリストに追加する必要があります。
Google Cloud コンソールで Identity Platform の [設定] ページに移動します。
[セキュリティ] タブを選択します。
[承認済みドメイン] で、[ドメインの追加] をクリックします。
拡張機能の URI を入力します。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 コードです。拡張機能で直接サポートされるメソッドを実装する方法は 2 つあります。
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 Service Worker から画面外ドキュメントをセットアップします。
{ "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; }
これで、Service Worker 内で firebaseAuth()
を呼び出すと、画面外ドキュメントが作成され、サイトが iframe に読み込まれます。その iframe はバックグラウンドで動作し、Firebase は標準の認証フローを通過します。解決または拒否されると、画面外ドキュメントを使用して、iframe から Service Worker に認証オブジェクトがプロキシされます。