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 firebasemanifest.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/chrometsconfig.jsonに以下を追記します。
    "types": [
      "chrome"
    ],あとは、呼び出すだけです。
chrome.identity.getProfileUserInfo((userInfo: chrome.identity.UserInfo) => {
    console.log('userInfo: ' + JSON.stringify(userInfo))
})実行結果
出力結果はこんな感じです
userInfo: {"email":"xxxx@gmail.com","id":"xxxxxxxxxxxxxxxxx"}