はじめに
今回、個人開発しているWebサービスと連携したChrome拡張機能を作成しました。
Webサービス本体でFirebaseAuthenticationのGoogleログインを採用しているので、拡張機能の方でもやってみようと思い、その際つまづいた点の備忘録です。
かなり端折っている部分もあるのでご注意ください。
なお、プログラミングを始めて1年以内の業界未経験者が書いた記事です。誤りを含んでいる可能性があります。あらかじめご了承ください。
Chrome拡張機能
3つのコンテキスト
-
popup
拡張機能のアイコンをクリックしたときに出てくるUIの部分です。通常のWebページと同様、HTML・CSS・JavaScriptで構成されます。今回はReactを使用しています。 -
background_script
拡張機能の中核的な機能を制御するJSファイルです。イベントリスナーの登録や状態管理を行うほか、バックグラウンドスクリプトからしか使用できないAPIも多くあります。特定のタブに依存せず、各タブを横断した制御を行うことができます。 -
content_scripts
ページの情報を取得したり、DOMを操作したりできます。
今回、認証トークンやユーザーのログイン状態の管理はバックグラウンドスクリプトで行っています。
ポップアップは閉じられると状態やデータが破棄されてしまうため、ユーザーの認証状態など長期間の状態管理には適していません。
バックグラウンドスクリプトは拡張機能が有効な間存在し続けるため、こちらでログイン状態を管理し、ポップアップ側が必要に応じてこれにアクセスする、という形となります。
メッセージパッシング
各コンテキストはそれぞれ独立した実行環境をもつため、1つのコンテキスト内で定義された変数等に、他のコンテキストから直接アクセスすることは基本的にはできません。
しかし、メッセージパッシングを使用することでコンテキスト間でデータを共有することができます。
送信側のメッセージパッシング
chrome.runtime.sendMessage(extensionId,message,options,callback);
パラメータ | 型 | 説明 |
---|---|---|
extensionId | string | メッセージを送る拡張機能/アプリのID。省略された場合、メッセージは自身の拡張機能/アプリに送信される。 省略可 |
message | any | JSON形式の送信するメッセージ |
options | object | オプションincludeTlsChannelId (boolean, optional): TLSチャネルIDがonMessageExternalに渡されるかどうかのプロパティが含まれる。省略可 |
callback | function | 返信メッセージ受け取り用コールバック。省略可 |
受信側のメッセージパッシング
chrome.runtime.onMessage.addListener(function(message,sender,sendResponse) {...})
パラメータ | 型 | 説明 |
---|---|---|
message | any | 送信されてきたJSON形式のメッセージ |
sender | MessageSender | 送信元の情報 |
sendResponse | function | 返信するための関数 |
上記を使用ながらコンテキスト間でデータを受け渡していく形となります。
※今回はruntime
を使用しておりますが、コンテンツスクリプトとメッセージパッシングを行うにはtabs
を使用する必要があります。
詳しく知りたい方はこちらの記事がわかりやすいです
処理の流れのイメージ
- ポップアップ側はユーザのアクションに応じて、メッセージパッシングを用いてバックグラウンド側にログイン情報やトークンを要求する
- バックグラウンド側はFirebaseと通信してtoken等を取得する
- メッセージパッシングでポップアップ側に返信する
- ポップアップ側は受け取った変数で処理を行う
といった流れになります。
後で実際のコードを用いて説明します。
ClientIDの設定
通常のWebページでFirebaseAuthenticationを使用する際には、Firebaseプロジェクトを作成した時点でGoogleCloudプロジェクトも作成され、OAuth2.0クライアントIDも作成されます。
こちらのクライアントIDがGoogleログインに使用されるため、開発者はOAuth2.0の認証フローを意識することなく認証を実装できます。
しかしChrome拡張機能の場合はその特殊なコンテクストから、Oauth2.0クライアントIDを手動で設定する必要があります。
大まかな流れとしては
- 一度空のアプリをDeveloper Dashboardに登録
- Google Cloud コンソールでアプリケーションID等を入力しOauthクライアントIDを作成(あとで使います)
-
FirebaseコンソールでGoolge認証を有効にし、信頼済みドメインに
chrome-extension://アプリケーションID(ストアにアップロードしたアプリのclient_id)
を追加
という感じです。
詳しく知りたい方は下の記事がとても丁寧に解説してくださっているので是非読んでみてください。
そもそもOAuthとはなんぞや??って方はこちらの記事がめちゃくちゃわかりやすくておすすめです
manifestファイルの記述(V3)
manifestファイルとは、
- どんなファイルがあるのか
- どんな機能を利用するのか
- どんな制限をつけるのか
などの設定をjson形式で記述するファイルで、拡張機能を作成する際には必ず必要です
Chromeは現在、古いManifestV2から新しいManifestV3への移行を進めており、V3には新たな機能や変更が多数導入されています。
Chrome Web StoreではすでにManifestV2の拡張機能の公開を受け付けていないので、今回はManifestV3で作成しました。
以下が今回作成した拡張機能のManifestファイルの一部です。
{
"manifest_version": 3,
...
"permissions": ["identity", "tabs"],
"background": {
"service_worker": "background.bundle.js",
"type": "module"
},
"content_security_policy": {
"extension_pages": "script-src 'self' ; object-src 'self'",
"sandbox": "sandbox allow-scripts; script-src 'self' https://apis.google.com https://www.gstatic.com https://www.googleapis.com https://securetoken.googleapis.com; object-src 'self'"
},
"oauth2": {
"client_id": "******",
"scopes": [
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile"
]
},
"key": "******"
}
"permissions"
こちらの項目に拡張機能が使用するChromeExtensionAPIを記述します。今回はOauthに使用する"identity"
、タブの情報を取得する"tabs"
を追加しています。
"background"
バックグラウンドスクリプトを指定するための項目です。V3からはバックグラウンドスクリプトはサービスワーカーとして登録する必要があります。
"content_security_policy"
ContentSecurityPolicy(CSP)は、ページが読み込むコンテンツのソースを制限することで、不正なスクリプトから保護するために設定します。
"extension_pages": "script-src 'self' ; object-src 'self'"
で、スクリプトとオブジェクトは拡張機能自身からしか読み込めないようにしています。
manifestV3からは、新たに"sandbox"
オプションが追加され、こちらで特定のAPIからのスクリプトの読み込みを許可しています。ここではGoogleのOauthフローで使用されるドメイン群を指定しています。
"oauth2"
GoogleのOauth2.0の認証フローを使用する際にはこちらの項目への記述が必要です。
こちらの"client_id"
に先ほど取得したOauth2.0のクライアントIDを記述します
"scopes"
では、ユーザのどのような情報を取得するかを記述します
"key"
Developer Dashboardから取得してきた公開鍵です
ポップアップ(UI)での処理
ログイン処理の実際のコードはこんな感じです
const GoogleLogInButton = () => {
...
const signInWithGoogle = async () => {
await new Promise((resolve) => {
chrome.runtime.sendMessage({ type: 'sign-in' }, (response) => {
setCurrentUser(response.user);
resolve();
});
});
navigate('/save');
};
return (
<>
<button
onClick={signInWithGoogle}
className="bg-white text-gray-600 mx-auto px-4 py-2 rounded-md flex items-center cursor-pointer text-center border-2"
>
<FcGoogle />
Sign In with Google
</button>
</>
);
};
export default GoogleLogInButton;
ユーザーがSignInWithGoogleのボタンを押すと、{ type: 'sign-in' }
というメッセージがバックグラウンドスクリプトに送信されます。
バックグラウンドスクリプトから返信されたuserをcurrentUserとして設定し、ログイン状態によってUIを出し分けています。
バックグラウンドスクリプトでの処理
バックグラウンド側はこんな感じ。
const signIn = async (sendResponse) => {
try {
const token = await new Promise((resolve) => {
//IdentityAPIによる認証トークンの取得
chrome.identity.getAuthToken({ interactive: true }, resolve);
});
//取得した認証トークンをFirebaseの認証プロバイダに渡して認証情報を作成
const credential = GoogleAuthProvider.credential(null, token);
//作成した認証情報でFirebaseAuthにサインイン
const userCredential = await signInWithCredential(auth, credential);
const user = userCredential.user;
const idToken = await user.getIdToken(true);
sendResponse({ user: user, token: idToken });
} catch (error) {
console.log('Error during sign-in', error);
sendResponse({ error: error });
}
};
...
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
// メッセージの種類によって処理を分岐
switch (message.type) {
case 'sign-in':
signIn(sendResponse);
return true;
...
default:
console.log('unknown message type', message.type);
}
});
バックグラウンドスクリプトでは、まずポップアップから送信されてきたメッセージのタイプによって処理を分岐させます。今回は'sign-in'
を扱っていますが他にもログアウト処理の'sign-out'
やログイン状態を取得するだけの'sign-in-state'
等もあります。
signIn関数内では、まずChrome Identity APIを使用してGoogleのOAuth2.0認証トークンを取得します。
取得した認証トークンをFirebaseの認証プロバイダに渡して認証情報を作成し、その認証情報を使用してFirebaseAuthenticationにサインインします。
あとは必要な情報を取得し、sendResponse
でポップアップ側に返すだけです。
ところで、manifestV3からは、バックグラウンドスクリプトはサービスワーカーとして登録せねばならず、windowオブジェクトが使用できなくなったので、signInWithPopup等のポップアップメソッドは使用できなくなりました。 ↓
おわりに
今回初めてChrome拡張機能を作成してみましたが、その独特なコンテキストやV3の記事の少なさもありかなり苦戦しました。また、ボイラープレートを使用して作成したのですが、webpackの設定等でもつまづきました。公式ドキュメントはちゃんと読もう..
最後まで読んでいただきありがとうございました!
本記事が少しでも皆さんの参考になれば幸いです。