Chrome拡張アプリを作っていて、ユーザのデータをユーザアカウント(email)単位にCloud FireStoreに保存したいと思っています。ユーザがアクセスできる情報をセキュリティルールを設定して制限したいので、Firebase AuthenticationでGoogle認証を行うことにしました。紆余曲折あり、いくつかの方法を試したので記録しておきます。
なお、単にGoogle認証したいだけなら google-api-javascript-client を使うのが一般的かもしれません。
【不採用】Firebase Authentication in ポップアップページ
公式ページで紹介されている方法を試してみました。参考にしたのは以下の二つのページです。
Firebase側の設定
- Firebase プロジェクト作成
- こちらから作成します。とりあえず、Google AnalyticsはOFFにしました。
- アプリを登録
- Firebaseコンソールの
プロジェクトの概要
ページのWEBアイコンからアプリを登録します。Hostingは今回利用しないのでOFFにしました。
- Firebaseコンソールの
- Google ログインを有効化
- Firebaseコンソールの Authentication -> Sign-in method で Google を有効にします
- Chrome拡張を承認済みドメインに追加
-
こちらに書いてある通り、Authentication -> Sign-in method -> 承認済みドメイン に
chrome-extension://CHROME_EXTENSION_ID
の形式でドメインを追加します -
CHROME_EXTENSION_ID
はChrome拡張の管理ページで確認できます
-
こちらに書いてある通り、Authentication -> Sign-in method -> 承認済みドメイン に
FirebaseのSDKをインストール
npm install firebase
manifest.jsonの設定
さらに、 https://apis.google.com/ 上のスクリプトを利用できるようにするため、manifest.json
のcontent_security_policy
に以下のように追加します。
"content_security_policy": "script-src 'self' https://apis.google.com/; object-src 'self'",
Firebaseの初期化および認証処理
最後に、ポップアップページのスクリプトで、Firebaseを初期化して認証処理を記載します。firebaseConfigの内容は、Firebaseコンソール > プロジェクトの概要 -> 対象のアプリ のSetting画面からコピペしてきます。ソースコード上にapiKeyが露出していることに不安を感じますが、こちらを参照すると大丈夫なようです。
import * as firebase from "firebase/app";
import "firebase/auth";
// Firebaseの初期化
const firebaseConfig = {
apiKey: "xxxxx",
authDomain: "xxxxx.firebaseapp.com",
databaseURL: "https://xxxxx.firebaseio.com",
projectId: "xxxxx",
storageBucket: "xxxxx.appspot.com",
messagingSenderId: "xxxxx",
appId: "xxxxx"
};
firebase.initializeApp(firebaseConfig);
// 認証処理
const provider = new firebase.auth.GoogleAuthProvider()
provider.addScope('https://www.googleapis.com/auth/userinfo.email');
firebase.auth().useDeviceLanguage()
firebase.auth().signInWithPopup(provider).then(function(result) {
console.log('userInfo: ' + JSON.stringify(result.user))
}).catch(function(error) {
console.log(error)
});
typescriptの場合も手順は特に変わりません。型を書いたり、アロー関数にしたりするだけです。
実行結果
ここまで実装したらChrome拡張のポップアップページを開くと、Google認証画面がさらにポップアップされ、user情報を取得することができるようになります。出力結果は以下のような感じです。
userInfo: {"uid":"xxxxx","displayName":"xxxxx","photoURL":"xxxxx","email":"xxxxx@gmail.com","emailVerified":true,"phoneNumber":xxxxx,...省略...}
一見良さそうなのですが、Google認証画面がWindowの裏側にポップアップされて気づかなかったり、なんどもChrome拡張のアイコンをクリックすると、いくつもGoogle認証画面が開かれたり、裏側に画面が残ってしまったりと、UXが最低です。。
【不採用】Firebase Authentication in オプションページ
世の中のChrome拡張を色々試していると、オプションページでGoogle認証を行なっているアプリを発見しました。そんなにUXも悪くないので、やってみました。
Firebase側の設定、SDKインストール、manifest.jsonの設定
Firebase Authentication in ポップアップページ
と全く同一です。
Firebaseの初期化および認証処理
オプションページのスクリプトにオプションページと全く同じコードを組み込んでみました。
すると、新しいblankのタブが立ちあがり、アドレスバーにabout:blank#blocked
と表示されます。何かよく分からないけどポップアップブロックされたようです。
manifest.jsonにopen_in_tabを追加
色々試したところ、manifest.json
の"open_in_tab": true
を指定して、オプションページ自体を新しいタブで表示するようにすると回避できました。
"options_ui": {
"page": "options/options.html",
"open_in_tab": true, // これを追加
"chrome_style": true
},
実行結果
オプションページを開くと、認証画面がポップアップ表示され、認証を行うことができした。別タブで開いていたせいか、ポップアップ画面が裏に隠れるようなこともあまりないので、UXもそれほど悪くないと感じます。
ただ、、、一つ大きな問題があります。
open_in_tab
というオプションは非推奨で、いずれ削除されてしまう予定なのです。後々苦労しそうなので、別の方法を探すことにしました。
【採用】chrome.identity API と Firebase Authentication のハイブリッド
さらに探していると、
Firebase Auth w/ Google Sign-In in Chrome Extensions
という素晴らしいページを発見しました。
ここでは、Chrome拡張をインストールしたGoogleアカウントのaccess tokenをchrome.identity APIを使って取得して、access tokenを元にGoogle OAuth2認証を行うという順番で処理をしています。
Firebase側の設定、SDKのインストール
まず、Firebase Authentication in ポップアップページ
に書いてある内容と同様に、Firebase側の設定とFirebase SDKのインストールを行います。
OAuth クライアントIDの作成
次に、こちらから、認証情報を作成 -> OAuth クライアントID -> Chrome アプリ -> アプリケーションIDにCHROME_EXTENSION_IDを指定 を行い、OAuth クライアントIDを作成します。
manifest.jsonの設定
さらに、manifest.json
に以下のように記述します。client_id
には先ほど作成したOAuth クライアントIDを指定しscopes
にuserinfo.email
を指定します。その他のスコープはこちらを参照ください。
"permissions": [
"identity"
],
"oauth2": {
"client_id": "xxxxxxxxxx.apps.googleusercontent.com",
"scopes": ["https://www.googleapis.com/auth/userinfo.email"]
}
Firebaseの初期化および認証処理
あとは、認証処理を実装します。ポップアップページのスクリプトにFirebaseの初期化および認証処理を追加します。実装は、こちらをほぼそのまま使っています。
import * as firebase from "firebase/app";
import "firebase/auth";
// Firebaseの初期化
const firebaseConfig = {
apiKey: "xxxxx",
authDomain: "xxxxx.firebaseapp.com",
databaseURL: "https://xxxxx.firebaseio.com",
projectId: "xxxxx",
storageBucket: "xxxxx.appspot.com",
messagingSenderId: "xxxxx",
appId: "xxxxx"
};
firebase.initializeApp(firebaseConfig);
// 認証処理
function startAuth(interactive: any) {
chrome.identity.getAuthToken({interactive: !!interactive}, (token: string) => {
if (chrome.runtime.lastError && !interactive) {
console.log('It was not possible to get a token programmatically.');
} else if(chrome.runtime.lastError) {
console.error(chrome.runtime.lastError);
} else if (token) {
const credential = firebase.auth.GoogleAuthProvider.credential(null, token);
firebase.auth().signInWithCredential(credential)
.then(function(result) {
console.log('userinfo: ' + JSON.stringify(result.user))
})
.catch((error: any) => {
if (error.code === 'auth/invalid-credential') {
chrome.identity.removeCachedAuthToken({token: token}, function() {
startAuth(interactive);
});
}
});
} else {
console.error('The OAuth Token was null');
}
});
}
startAuth(true);
普通はFirebaseの認証状態が変更されたことを検知して再度認証したい場合は、以下を追加します。user
オブジェクトを取得できなかったら、再度startAuth()
を呼び出しています。
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
console.log('userinfo: ' + JSON.stringify(user));
} else {
startAuth(true);
}
});
typescriptでも記法は違えど同様に実装すれば大丈夫です。
実行結果
実際の動きは以下のようになります。
Chrome拡張のアイコンをクリックした際に、ログインしてない状態だとchrome.identity.getAuthToken()
でinteractive: true
を指定しているため、別タブでGoogleの認証画面が表示されます。
ログイン後、もう一度Chrome拡張のアイコンをクリックすると、今度はauth_tokenを取得できて、FirebaseのAuthenticationのSignInもうまくいって、user情報を取得できます。
userInfo: {"uid":"xxxxx","displayName":"xxxxx","photoURL":"xxxxx","email":"xxxxx@gmail.com","emailVerified":true,"phoneNumber":xxxxx,...省略...}
ちなみに、認証済みの場合にuser情報を取得するまでのリードタイムも、firebase.auth().signInWithPopup
に比べるとかなり早く体感で0.5秒ぐらいという感じなので、user情報をどこかにキープしておくような工夫がいらないのも、とても助かります。
【参考】chrome.identity API
Firebaseとか関係なく、Chrome拡張をインストールしたGoogleアカウントに紐づくemailが欲しいだけなら、chrome.identity APIを使う方法がもっともシンプルだと思います。このAPIは、OAuth2のaccess tokenを取得するのが主目的のようなのですが、access tokenを取得する以外に、getProfileUserInfo()
を使ってemail
とid
を取得することができます。
manifest.jsonの設定
manifest.json
に以下のpermissionを追加します。
"permissions": [
"identity", // chrome.identity APIを使う権限
"identity.email" // emaiを取得する権限
],
なお、指定できるpermissionの種類はこちらを参照いただければと思います。
必要な権限が追加されたことは、Chrome拡張の詳細ページで確認できます。
ユーザ情報取得処理
あとはオプションページやポップアップページでAPIを呼び出します。
javascriptだとこれだけです。
chrome.identity.getProfileUserInfo(function(userInfo) {
console.log('userInfo: ' + JSON.stringify(userInfo));
});
typescriptだと、型を解決するために少し記述が増えます。
型定義を追加して、
npm install @types/chrome
tsconfig.json
に以下を追記します。
"types": [
"chrome"
],
あとは、呼び出すだけです。
chrome.identity.getProfileUserInfo((userInfo: chrome.identity.UserInfo) => {
console.log('userInfo: ' + JSON.stringify(userInfo))
})
実行結果
出力結果はこんな感じです
userInfo: {"email":"xxxx@gmail.com","id":"xxxxxxxxxxxxxxxxx"}