Chrome拡張+React+Firebaseで認証機能を実装する
経緯
今回、個人開発したBookmark-BoardというWEBアプリのChrome拡張機能を開発しました。
このChrome拡張ではFirebase Auth(Google認証)で認証を行っているのですが、その際に色々とつまづいた点があったため、忘備録として登録から公開までの一連の機能開発の手順を整理してみました。前提
以下の条件を前提に進めます。
- 既にFirebaseのプロジェクトを作成している
- ReactおよびTypescriptを使用する
今回はサンプルとして最低限の認証機能を持ったChrome拡張アプリを作成します。
準備
Chrome Web Store developerへの登録
Chrome拡張を開発するためにはChrome Web Store developerに登録します。
以降、Developer Dashboardからアプリのアップロード等行うことになります。
Chrome App ID取得
Chrome拡張でFirebaseのAuth(Google認証)を使うためにはoauth2のclient_idとアプリの公開鍵が必要になります。(client_id
はChromeにログイン中ユーザのGoogle認証情報をidentityを用いて取得したいために使用。key
はChrome拡張のIDを一意に固定するため取得するために使用。keyについての詳細はこちら)
これらはChrome Web Storeにアップロードしてからでないと入手することができません(アップロードについては公開まで行う必要はなく、ドラフト状態で問題ありません)。
そのために、一度空っぽのアプリを登録しこれらの情報を作成しておきます。準備するものは以下のような一式となります。
chrome-plugin-sample(フォルダ)
|- manifest.json
{
"manifest_version": 2,
"name": "Firebase Auth in Chrome Extension Sample",
"description": "This sample shows how to authorize Firebase in a Chrome extension using a Google account.",
"version": "0.1",
"permissions": [
"identity"
]
}
これをzipしてDeveloper Dashboardにアップロードします
zip -r chrome-plugin-sample.zip chrome-plugin-sample
Tips: client_idとkey(公開鍵)の確認方法
Developer Dashboardから以降の手順で必要になるclient_idと公開鍵を確認することができます。
OAuth Clientの作成
拡張機能をFirebaseアプリに認証させるためにOAuth Client IDが必要となります。
コンソールにアクセスし、先ほどストアにアップロードしたアプリのclient_idを用いて、OAuth Client IDを作成します。
Firebaseプロジェクトの設定
拡張機能ではGoogle認証を用いるため、Firebaseアプリの認証方法としてGoogleによる認証を許可しておく必要があります。
- Firebaseコンソール→Authentication→Sign in methodに移動し、Google認証を有効にします
- 同様に、信頼済みドメインに
chrome-extension://アプリケーションID(ストアにアップロードしたアプリのclient_id)
を追加しておきます
アプリの開発
Chrome拡張でFirebaseの認証を行えるまでの実装をここから作っていきます(ここで作成するのは最低限認証機能を持っただけのアプリとなります)
React + Typescript + Chrome拡張の環境設定
Chrome拡張向けのReactではwebpackの設定をカスタマイズする必要があるため、Create-React-Appを使わずにReactの環境を整えていきます。今回はReact + Typescript + Chrome拡張のテンプレート環境を整えてくれるreact-typescript-chrome-extension-boilerplateというボイラープレートが公開されているので、こちらを使用させていただきます。
$ npx degit https://github.com/sivertschou/react-typescript-chrome-extension-boilerplate.git <project-name>
$ cd <project-name>
これによって次のようなディレクトリ構成が出来上がります
<project-name>
├─ dist (ビルドによって作成される)
│ ├── ...
│ ...
├─ public
│ ├── ...
│ ├── manifest.json
│ └── popup.html
├─ src
│ ├── ...
│ ├── App.tsx
│ ├── background.ts (バックグラウンド処理に利用予定)
│ ├── content.ts
│ └── popup.tsx (ポップアップ処理に利用予定)
...
├─ package.json
├─ tsconfig.json
└─ webpack.config.js
それぞれのファイルの意味合いについてはここでは割愛しますが、公式サイト等で詳しく述べられていますのでご参照ください。npm install
した後、npm start
もしくはnpm build
を行うことで、dist
ディレクトリにビルドされたファイル一式が出力されます
動作確認
現時点で動作確認をしておきます。
まずnpm build
もしくはyarn build
でビルドを行い、dist
フォルダにビルド成果物が作成されることを確認します。次に、拡張機能の管理ページに移動し、「デベロッパーモードを有効」にした上で、「パッケージ化されていない拡張機能を読み込む」を選択します。
読み込むローカルフォルダを聞かれるので、先ほどビルドしたdist
フォルダを選択します。読み込みが問題なく終了すると、次のようにアプリの詳細が表示されます
拡張機能メニューからアプリを選択すると、アプリが動作している様子が確認できます
Firebase連携
firebaseとの連携を行うために、以下の★がついたファイルを編集していきます(今回はサンプルのため最小限の実装となります)
<project-name>
...
├─ public
│ ├── ...
│ ├── manifest.json ★
│ └── popup.html
├─ src
│ ├── ...
│ ├── App.tsx ★
│ ├── background.ts ★
│ ├── firebase.ts ★(新規追加)
│ └── popup.tsx
...
firebase clientのインストール
firebaseのクライアントライブラリをインストールします。
$ npm install firebase --save
インストールが完了したらfirebase.ts
を作成し、初期化処理等を行なっていきます
import firebase from 'firebase/app';
import 'firebase/auth'
import 'firebase/firestore'
// ※firebase consoleなどを確認し、適宜必要な情報を入力する
const firebaseConfig = {
apiKey: FIREBASE_API_KEY,
authDomain: FIREBASE_AUTH_DOMAIN,
databaseURL: FIREBASE_DATABASE_URL,
projectId: FIREBASE_PROJECT_ID,
storageBucket: FIRESTORE_STORAGE_BUCKET,
appId: FIREBASE_APP_ID
}
if (!firebase.apps.length) {
firebase.initializeApp(firebaseConfig)
}
export default firebase
manifest.json
今回はfirebaseで認証を行いたいのでmanifest.jsonに対して以下の項目を追記します。
{
...
"permissions": [
"identity"
],
"content_security_policy":"script-src 'self' https://www.gstatic.com/ https://*.firebaseio.com https://www.googleapis.com https://apis.google.com https://securetoken.googleapis.com; object-src 'self'",
"oauth2": {
"client_id": "******apps.googleusercontent.com(準備で入手したOAuth Client ID)",
"scopes": [
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile"
]
},
"key": "MIIBIjANBgkqhkiG9w0...2DZA+s0JOsh8YxAua3s(準備で入手した公開鍵。改行は含まないように気をつけて)"
}
必要なcontent_security_policy
の設定についてはこちら参照しています
バックグランド側の処理
認証状態はfirebase/auth
のonAuthStateChanged
を用いてウォッチし、ポップアップ側から認証状態が取得できるようにリスナーを設定します。また、ポップアップ側でユーザがサインインの操作を行った際にはバックグラウンド側でサインイン処理を受け付けるようにメッセージリスナーを設定しておきます。
import firebase from './firebase'
type MessageType = { type: string }
function onSigninRequest(message: MessageType, _sender: chrome.runtime.MessageSender, _sendResponse: () => void) {
if (message.type == 'signin') {
// ポップアップによるGoogle認証
const provider = new firebase.auth.GoogleAuthProvider()
firebase.auth().signInWithPopup(provider).catch(() => {
console.log('サインインに失敗');
})
_sendResponse()
} else if (message.type == 'signout') {
if (firebase.auth().currentUser) {
firebase.auth().signOut();
}
_sendResponse()
}
}
function initApp() {
// ユーザーがサインイン/サインアウトした際に呼び出される
firebase.auth().onAuthStateChanged(function (user) {
// ポップアップ側からのサインイン状態取得リクエストを受け付ける
chrome.runtime.onMessage.addListener(function onReceiveAuthStateRequest(message: MessageType, _sender, _sendResponse) {
if (message.type == 'signin-state') {
if (user) {
_sendResponse({ type: 'signin-state', user });
} else {
_sendResponse({ type: 'signin-state' });
}
}
})
})
// ポップアップ側からのサインインリクエストを受け付ける
chrome.runtime.onMessage.addListener(onSigninRequest)
}
window.onload = function () {
initApp()
}
ちなみにChrome 拡張機能では HTTP リダイレクトを使用できないためサインイン処理はsignInWithPopup
またはlinkWithPopup
に限られます。また認証ポップアップはブラウザのアクションポップアップをキャンセルするため、バックグラウンドから呼び出す必要があります(Firebase公式より)
ポップアップ(UI)側の処理
ポップアップ(popup.ts
)からはApp.tsx
が呼び出されるので、こちらにUI側の認証コードを記述していきます。初回はサインインボタン、サインイン後はユーザ名とサインアウトボタンを表示するというシンプルなUIを実装します。
import * as React from "react"
import firebase from './firebase'
const App = () => {
const [user, setUser] = React.useState<firebase.User>()
// バックグラウンドにサインイン状態を問い合わせる
// 一度サインイン後はポップアップを閉じてもバックグラウンドからサインイン状態を復帰できる
React.useEffect(() => {
chrome.runtime.sendMessage({ type: 'signin-state' }, function (response) {
if (response?.type === 'signin-state') {
setUser(response.user)
}
})
}, [])
// サインイン状態をウォッチする
// ポップアップ画面でサインインやサインアウトをした時、即座に画面に反映させるために使用
React.useEffect(() => {
firebase.auth().onAuthStateChanged(function (user) {
if (user) {
setUser(before => {
if (before && before.uid === user.uid) {
return before
}
return user
})
} else {
setUser(undefined)
}
})
}, [])
// サインインボタンを押下した時に呼び出される
const signinWithPopup = React.useCallback(() => {
chrome.runtime.sendMessage({ type: 'signin' }) // バックグラウンドに移譲する
}, [])
// ブラウザでログインしているユーザのGoogle認証情報を使用してサインイン
const signinWithChromeUser = React.useCallback((interactive: boolean) => {
chrome.identity.getAuthToken({ interactive }, function (token) {
if (chrome.runtime.lastError && !interactive) {
console.log('トークンの自動取得失敗');
} else if (chrome.runtime.lastError) {
console.error(chrome.runtime.lastError);
} else if (token) {
// OAuth Access Tokenでfirebase認証
var credential = firebase.auth.GoogleAuthProvider.credential(null, token);
firebase.auth().signInWithCredential(credential).catch(function (error) {
// OAuth Access Tokenが無効なため、キャッシュをクリアして再試行
if (error.code === 'auth/invalid-credential') {
chrome.identity.removeCachedAuthToken({ token: token }, function () {
signinWithChromeUser(interactive);
});
}
});
} else {
console.error('トークンがnull');
}
});
}, [])
// サインアウトボタンを押下した時に呼び出される
const signout = React.useCallback(() => {
chrome.runtime.sendMessage({ type: 'signout' }) // バックグラウンドに転送する
}, [])
return (
<div style={{ width: 200, padding: '1rem' }}>
<h3>Firebaseで認証</h3>
{user && (<p>{user.displayName}がサインインしています</p>)}
<div>
{!user && (
<div>
<p>ブラウザでログイン中のGoogleユーザでサインインする場合はこちら</p>
<button onClick={() => { signinWithChromeUser(true) }}>サインイン(1)</button>
<p>その他のGoogleユーザでサインインする場合はこちら</p>
<button onClick={signinWithPopup}>サインイン(2)</button>
</div>
)}
{user && (<button onClick={signout}>サインアウト</button>)}
</div>
</div>
);
};
export default App;
ポイントとしては、サインインの手段として以下の二通り用意しています。
- ブラウザにログイン中のGoogle認証情報を用いてサインイン
- ここではgetAuthTokenによってmanifest.jsonに記載したclient_idからアクセストークンを取得します。client_idの連携ができていないと
OAuth2 not granted or revoked
などのエラーで失敗するので要注意
- ここではgetAuthTokenによってmanifest.jsonに記載したclient_idからアクセストークンを取得します。client_idの連携ができていないと
- Googleアカウントを選択してサインイン
- FirebaseのsigninWithPopupを使用し、任意のGoogleアカウントを選択しログインします。ポップアップ認証自体はバックグラウンドで行うため、ここでは処理の依頼を行います。
ちなみに今回はシンプルなサンプルのため全てApp.tsx
に記述しましたが、カスタムフック等を用いて分割すると良いかと思います。
これで、ポップアップ上でサインインとサインアウトができるようになりました🎉
Tips:ログの確認方法
Chrome拡張を初めて開発する場合、console.log
が表示されなくて一瞬あれ?ってなるかもしれません。ログについてはポップアップとバックグラウンドでそれぞれ見方が異なります。
- ポップアップ
アプリのポップアップを開いた状態で右クリック→[検証]からデベロッパーツール画面が表示されるので、そこからポップアップ側で出力したログなどが確認できます。
- バックグラウンド
拡張機能の管理ページからバックグラウンドページ
をクリックすることでバックグラウンド側のログを確認することができます
公開
開発が完了したらdist
フォルダをDeveloper Dashboardにアップロードします。
zip -r chrome-plugin-sample.zip dist
ストアに掲載するために説明欄やアイコンなど、必要事項を記入していきます。今回のようにpermission
にidentity
を指定している場合は、[プライバシーへの取り組み]項目の記入が必要になります。
必要事項が全て記入できたら[審査のための送信]を行います。
審査が無事終わるとアプリストアに公開されます(自分の場合、審査には3日要しました)
参考
全編を通しての流れは以下のサイトを参考に記述させていただきました。
Discussion
こちらの記事、同じようなユースケースを実現しようとする際にとても参考になりました。
ありがとうございます。
1点だけ、現行の Manifest V3 で動かそうとした場合、バックグラウンドページが Service Worker に移行された関係で以下の window が使えなくなっているので、ハマってしまう問題がありました。
FYI: https://developer.chrome.com/docs/extensions/develop/migrate/to-service-workers?hl=ja
自分はこの対応として、単純に window.onload 部分を削除して以下のように呼び出すようにしました。
以上、共有でした
コメントいただきありがとうございます!
本記事がまだManifest V3に対応できておらずすみません🙇
共有いただきました内容、是非参考にさせていただきます。