【Vue 3+TypeScript+Vite】Chrome拡張機能(MV3) + FirebaseでGoogle認証する

## 経緯
Chrome拡張機能のMV2でFirebaseを使ってGoogle認証するという記事は見つかるのですが、**2021/11/27時点でMV3で作られている記事はほとんどなくハマりポイントが多かった**ので整理も兼ねて記事にしてみようと思います。

前半(Docker + Vueでビルド環境を作る)は主に開発環境構築の話となります。
**Chrome拡張機能(MV3) + FirebaseでGoogle認証するに関する部分は後半になります。[こちら](#chrome拡張機能mtv3--firebaseでgoogle認証する)から後半部に遷移できます。**

## リポジトリ

今回の記事で使ったコードは下記リポジトリにあげているので参考にしてください。
https://github.com/snyt45/chrome-extensions-mv3-google-authenticator-with-firebase

## Docker + Vueでビルド環境を作る
Chrome拡張機能はHTML、CSS、JSで書けるのですが今回は勉強も兼ねてVueを使ってChrome拡張機能に必要なファイルを生成するためのビルド環境を構築していきます。

### 今回の構成

- Docker
    - docker-composeを使います。
- [Node.js](https://nodejs.org/ja/)
    - JavaScriptの実行環境
- [npm](https://www.npmjs.com/)
    - Node Package Manager.Node.jsにデフォルトで付属しているコマンドでNode.jsのパッケージ管理をするためのツール。
- npx
  - [npm v5.2.0](https://github.com/npm/npm/releases/tag/v5.2.0)からnpmに同梱されるようになったコマンドでNode.jsのパッケージを実行するためのツール。
  - 本記事では登場しませんが、実際に動作確認する際にnpxは便利なので結構使っています。
- [Vue3](https://v3.ja.vuejs.org/) + [TypeScript](https://www.typescriptlang.org/)
    - JavaScriptフレームワークのVue3とAltJS(コンパイルすることで最終的にJavaScriptのコードを生成する)の一つのTypeScriptを使います。
- [Vite](https://vitejs.dev/)
    - Vue.jsのプロジェクト作成や実行ができるツール

### Docker上に環境構築

今回はNode.jsがインストールされているDockerイメージをベースに構築していきます。

[ViteにはNode.jsのバージョンが12以上必要](https://vitejs.dev/guide/#scaffolding-your-first-vite-project)みたいなので、今回はNode.jsのバージョン14のイメージを使います。


```docker-compose.yml
version: '3'

services:
  app:
    container_name: vue-app-container
    image: node:14
    working_dir: /app
    ports:
      - 3000:3000
    volumes:
      - .:/app
    stdin_open: true
```

`stdin_open: true`は、`stdin_open: true`がないと`docker-compose up`後にコンテナが終了してしまうのでコンテナを起動し続けるために必要でした。

docker-compose.ymlを作成したら、コンテナのビルド&起動を行います。

```bash
docker-compose up -d
```

コンテナが起動したら、コンテナ内でbashを対話シェルとして実行します。

```bash
docker-compose exec app /bin/bash
```

コンテナはNode.jsがすでにインストールされている環境なので、`node` `npm` `npx`のコマンドが使えることを確認します。

```bash
root@64985558fba6:/app# node -v
v14.18.1
root@64985558fba6:/app# npm -v
6.14.15
root@64985558fba6:/app# npx -v
6.14.15
```

### Viteでプロジェクトを生成する

Viteでプロジェクトを生成するコマンドを実行します。

```bash
npm init vite@latest
```

コマンドライン上で対話形式で入力していくと、プロジェクトが生成されます。
(3つ入力または選択するだけでプロジェクトが生成されました…ほんとに高速で驚きです！)

```bash
root@64985558fba6:/app# npm init vite@latest
npx: installed 6 in 3.059s
✔ Project name: … sample-project
✔ Select a framework: › vue
✔ Select a variant: › vue-ts

Scaffolding project in /app/sample-project...

Done. Now run:

  cd sample-project
  npm install
  npm run dev
```

app/sample-projectディレクトリに生成されたファイルをappディレクトリ配下に展開するために下記コマンドを実行します。

```bash
# sample-projectに移動
cd sample-project

# sample-project内の全てのファイルを一階層上(app/)に移動
mv * ../

# sample-projectディレクトリは不要になるので削除
cd ../
rm -rf sample-project
```

サーバーを起動して、ブラウザーで「http://localhost:3000/」にアクセスします。

```bash
# package.jsonのライブラリをインストール
npm install

# 開発用のWebサーバー起動
npm run dev
```

ですが、Vueの初期ページがされず..

下記を参考にvite.config.jsに設定を追加したところ、正常に表示されました。

[dockerにviteを入れてvueで開発しようとしたときlocalhostに繋がらなかった話 \| SEとDIY](https://choice-site.com/2021/06/24/docker%E3%81%ABvite%E3%82%92%E5%85%A5%E3%82%8C%E3%81%A6vue%E3%81%A7%E9%96%8B%E7%99%BA%E3%81%97%E3%82%88%E3%81%86%E3%81%A8%E3%81%97%E3%81%9F%E3%81%A8%E3%81%8Dlocalhost%E3%81%AB%E7%B9%8B%E3%81%8C/)

```diff:vite.config.js
export default defineConfig({
  plugins: [vue()],
+  server: {
+    host: true
+  }
})
```

正常に表示されている画像。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163887/452e1790-f9de-fe97-cbca-aa7cbab72d7d.png)

動作確認できたので、今回不要なファイルを削除しておきます。

```bash
# index.html削除
rm -rf index.html

# public削除
rm -rf public

# src配下のファイルを削除
cd src
rm -rf *
```

### Chrome拡張機能用のファイルを生成できるようにする

今回Vueを使いますが最終的にはHTMLやJSなどをextension/フォルダにビルドし、そのファイルをChrome拡張用のファイルとして使います。

#### Chrome拡張機能用のビルド後のディレクトリ構成

ビルドを実行した際に下記のようなディレクトリ構成が作成されるようにします。

```
extension
|- dist/
|  |- assets/         # index.htmlから読み込むjsなどが配置される
|  |- popup/          # popup用のUIファイル
|      |- index.html  
|- manifest.json      # Chrome拡張機能のエントリポイント
```

#### manifest.jsonを生成するためのビルド環境を作る

Chrome拡張機能のエントリポイントであるmanifest.jsonをextension/にビルドできるようにしていきます。

extensionディレクトリを作成しておきます。

```bash
mkdir extension
```

先に必要になるライブラリをインストールしておきます。

```bash
npm install --save-dev @types/node # pathモジュールの型情報を取得するため
npm install --save-dev @types/fs-extra # fs-extraモジュールの型情報を取得するため
npm install --save-dev fs-extra # fs-extraモジュールを使えるようにするため
npm install --save-dev esno # manifest.tsをバッチのように実行するビルドツール
```

manifest.jsonを作成するためのmanifets.tsを作成します。
(よく使うメソッドはscripts/utils.tsに作成しておきます。)

```scripts/utils.ts
import { resolve } from 'path'

export const r = (...args: string[]) => resolve(__dirname, '..', ...args)
```

```src/manifest.ts
import fs from 'fs-extra'
import { r } from '../scripts/utils'

const getManifest = () => {
  return {
    manifest_version: 3,
    name: 'Getting Started Example',
    description: 'Build an Extension!',
    version: '1.0',
    action: {
      default_popup: './dist/popup/index.html'
    }
  }
}

function writeManifest() {
  fs.writeJSON(r('extension/manifest.json'), getManifest(), { spaces: 2 })
}

writeManifest()
```

package.jsonにmanifest.jsonを生成するnpmスクリプトを追加します。

```package.json
  "scripts": {
    "build:manifest": "esno src/manifest.ts"
  },
```

`npm run build:manifest`を実行してextensionディレクトリにmanifest.jsonが作られれば成功です。

#### popup用のUIファイルを生成するためのビルド環境を作る

src配下にpopupフォルダを作成します。

```bash
mkdir src/popup
```

[Viteではindex.htmlがエントリポイント](https://ja.vitejs.dev/guide/#index-html-%E3%81%A8%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%83%AB%E3%83%BC%E3%83%88)になります。
エントリポイントとなるファイルを作成します。
ビルド時にエントリポイントで読み込まれているmain.tsもバンドルされます。

```src/popup/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Popup</title>
</head>
<body style="min-width: 100px">
  <div id="app"></div>
  <script type="module" src="./main.ts"></script>
</body>
</html>
```

エントリポイントから読み込むファイルも作成します。

```src/popup/main.ts
import { createApp } from 'vue'
import App from './Popup.vue'

const app = createApp(App)
app.mount('#app')
```

```src/popup/Popup.vue
<template>
  <div>
    Hello World!!
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
  name: 'Popup',
  setup() {
    return {}
  },
})
</script>
```

このままだとvueファイルを認識できていなかったため、env.d.tsを追加しました。
(src配下を一度削除しましたが、そのときに必要なenv.d.tsも削除してしまっていたようです…)

```src/env.d.ts
/// <reference types="vite/client" />

declare module '*.vue' {
  import { DefineComponent } from 'vue'
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
  const component: DefineComponent<{}, {}, any>
  export default component
}
```

popup用のUIファイルをビルドするためにvite.config.tsに設定を追加します。

```vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { r } from './scripts/utils'

// https://vitejs.dev/config/
export default defineConfig({
  root: r('src'),
  build: {
    outDir: r('extension/dist'),
    emptyOutDir: false,
    rollupOptions: {
      input: {
        popup: r('src/popup/index.html'),
      },
    },
  },
  plugins: [
    vue(),
    // rewrite assets to use relative path
    {
      name: 'assets-rewrite',
      enforce: 'post',
      apply: 'build',
      transformIndexHtml(html) {
        return html.replace(/"\/assets\//g, '"../assets/')
      },
    },
  ],
  server: {
    host: true
  }
})
```

package.jsonにextensionディレクトリにdistディレクトリを生成するnpmスクリプトを追加します。

```diff:package.json
  "scripts": {
    "build:manifest": "esno src/manifest.ts",
+    "build:popup": "vite build"
  },
```

`npm run build:popup`を実行して、extensionディレクトリにdistディレクトリが生成されて中にassetsやpopupディレクトリが生成されていれば成功です。

#### ビルドスクリプトを微調整

まずは必要になるライブラリをインストールします。

```bash
npm install --save-dev rimraf # ファイル・ディレクトリ削除用のライブラリ
npm install --save-dev npm-run-all # npmスクリプトをシーケンシャル(ひとつずつ順番に)実行できるライブラリ
```

npmスクリプトを修正します。
buildコマンドとclearコマンドが新たにできたのでビルドするとき便利になりました。

```diff:package.json
  "scripts": {
+    "build": "run-s clear build:manifest build:popup",
    "build:manifest": "esno src/manifest.ts",
    "build:popup": "vite build",
+    "clear": "rimraf extension/dist extension/manifest.json"
  },
```

#### ビルドがうまくいったか動作確認

ここまででChrome拡張機能に最低限必要なファイルがビルドできるようになったので、実際に確認します。

```bash
npm run build
```

chrome://extensions/ にアクセスします。

左上の「パッケージ化されていない拡張機能を読み込む」から生成したextensionディレクトリを指定します。

![Snipaste_2021-11-27_23-09-05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163887/c8df7af2-e093-d82f-d468-90b880233fb0.png)

追加に成功すると次のように表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163887/f214f520-173f-33ac-9c98-8f498fb08bc9.png)

**ブラウザ右上のChrome拡張機能をクリックしてHello Worldが表示されれば成功です！**

![Snipaste_2021-11-27_23-12-42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163887/58157b0f-8ea4-f81a-95f6-2c5791f08bf6.png)


## Chrome拡張機能(MtV3) + FirebaseでGoogle認証する

ビルド環境ができたので、やっと本題になります。

最終的には**`chrome.identity.getAuthToken`+`signInWithCredential`を使う方法でChrome拡張機能(MV3) + FirebaseでGoogle認証することができました。**

### chrome.identity.getAuthTokenとは

chromeで使えるAPI。OAuth2アクセストークンを取得します。
https://developer.chrome.com/docs/extensions/reference/identity/

### signInWithCredentialとは

Firebaseで使えるAPI。指定された資格情報を使用して非同期にサインインします。
https://firebase.google.com/docs/reference/js/auth.md#signinwithcredential

### Chrome拡張機能(MtV3) + FirebaseでGoogle認証するまでの流れ

#### 1. Firebaseのプロジェクトを作成する
Firebaseのログインを済ませ、FirebaseコンソールのFirebaseプロジェクトから「プロジェクトを追加」します。

![Snipaste_2021-11-28_19-43-14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163887/26426513-44d7-addd-0f79-c882c6969cf5.png)

プロジェクト名を入力します。ここでは、「sample-project」にしています。

![Snipaste_2021-11-28_19-43-43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163887/99cc7355-0637-2146-23c8-d6b4a0a46f0e.png)

Googleアナリティクスは今回必要ないので無効にします。

![Snipaste_2021-11-28_19-44-14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163887/23360606-d152-1843-f088-3abe180c9ca1.png)

少し待つとプロジェクトが作成されます。

![Snipaste_2021-11-28_19-44-54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163887/32d4f8f3-a20e-3cb2-a1da-42f3f0af0693.png)

続行を押して、FIrebaseのプロジェクトに移動して次のような画面が表示されればプロジェクトが作成できています。

![Snipaste_2021-11-28_19-48-16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163887/362236f7-bd8f-7477-226e-20edcd88ce2d.png)

#### 2. Firebaseのマイアプリを登録する。

Chrome拡張機能からGoogle認証するうえでマイアプリが必要になるので、「プロジェクトの設定」から登録します。

![Snipaste_2021-11-28_20-02-40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163887/bfbf2dfc-d83b-cd48-296a-dbcd02686258.png)

マイアプリの赤枠アイコンをクリックして登録していきます。

![Snipaste_2021-11-28_20-05-55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163887/6fa32416-911c-2093-1691-11a336f5b2b6.png)

アプリのニックネームを入力します。

![Snipaste_2021-11-28_20-07-09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163887/56d988a5-04fe-9e2f-0449-253ae38dd545.png)

FirebaseのSDKの追加を行います。
今回は、「npmを使用する」にチェックします。

この手順に従って、SDKをインストールしておきます。
(**SDKをインストールしておくことで、Chrome拡張機能プロジェクト内にFirebaseスクリプトを含むことになるので、エラーにならずに使用することができます。**)

```bash
npm install --save-dev firebase # Firebase SDKのインストール
```

「コンソールに進む」ことでマイアプリが登録されます。

![Snipaste_2021-11-28_20-09-53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163887/6164ba07-0329-4373-424e-145c1d0bcb1d.png)

ひとまず、ここでFirebaseの初期化までエラーなく動くか試しておきます。

```diff:src/popup/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Popup</title>
</head>
<body style="min-width: 100px">
  <div id="app"></div>
  <script type="module" src="./main.ts"></script>
+  <script type="module" src="./firebase.ts"></script>
</body>
</html>
```

```src/popup/firebase.ts
import { initializeApp } from 'firebase/app'

const firebaseConfig = {
  apiKey: import.meta.env.VITE_FIREBASE_APIKEY,
  authDomain: import.meta.env.VITE_FIREBASE_AUTHDOMAIN,
  projectId: import.meta.env.VITE_FIREBASE_PROJECTID,
  storageBucket: import.meta.env.VITE_FIREBASE_STORAGEBUCKET,
  messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGINGSENDERID,
  appId: import.meta.env.VITE_FIREBASE_APPID
}
const app = initializeApp(firebaseConfig)
console.log(app)
```

環境変数は.env.localを作ってそちらから読み込むようにしました。
(.gitignoreでgit管理対象外にしておきましょう)

```.env.local
# Firebase configuration
VITE_FIREBASE_APIKEY=XXXXXXXXXXX
VITE_FIREBASE_AUTHDOMAIN=XXXXXXXXXXX
VITE_FIREBASE_PROJECTID=XXXXXXXXXXX
VITE_FIREBASE_STORAGEBUCKET=XXXXXXXXXXX
VITE_FIREBASE_MESSAGINGSENDERID=XXXXXXXXXXX
VITE_FIREBASE_APPID=XXXXXXXXXXX
```

`npm run build`して、Chrome拡張機能を再読み込みします。

ブラウザ右上の拡張機能のアイコンを右クリックして「ポップアップ検証」からコンソールを開きます。

![Snipaste_2021-11-28_20-33-31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163887/4e1e031b-662f-552b-6518-2414cc605a23.png)

エラーが出ていなければここまでの動作確認は成功です。

![Snipaste_2021-11-28_20-33-54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163887/f4d06353-6f17-ca97-e4f7-726634ed03c3.png)

#### 3. Google Cloud Platform(GCP)のプロジェクトの確認

**ここが重要なのですが、Firebaseでプロジェクトを作成した時点でGoogle Cloud Platform(GCP)側で同じプロジェクト名でプロジェクトが作成されています。**

ここでは、sample-projectをクリックしてGCPのプロジェクトのダッシュボードに移動します。

![Snipaste_2021-11-28_19-47-56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163887/1935d70a-c250-d480-3678-3d6dc219d7f3.png)

プロジェクトのダッシュボードの左メニューにある「APIとサービス」の認証情報をクリックします。

![Snipaste_2021-11-28_19-52-37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163887/aef0d1df-1345-0f0f-9193-54dac4d11630.png)

「APIキー」、「OAuth 2.0 クライアント ID」がFirebaseによって自動で作成されていることが分かります。

![Snipaste_2021-11-28_19-57-45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163887/6f3c09f6-826b-2d20-e77e-87ad5429beac.png)

**後述のハマりポイント③にも書いていますが、自動で作成されている「OAuth 2.0 クライアント ID」を削除するとFIrebase認証でGoogleを追加する際にエラーが発生するので残しておきます。**

#### 4. `chrome.identity.getAuthToken`を使うための準備

こちらを参考にして準備していきます。
https://developer.chrome.com/docs/apps/app_identity/

##### 4.1 マニフェストに権限を追加します。

```diff:src/manifest.ts
略
const getManifest = () => {
  return {
    manifest_version: 3,
    name: 'Getting Started Example',
    description: 'Build an Extension!',
    version: '1.0',
    action: {
      default_popup: './dist/popup/index.html'
    },
+    permissions: [
+      'identity'
+    ],
  }
}

略
```

##### 4.2 OAuth2クライアントIDを取得する

**すでにFirebaseが自動でOAuth 2.0 クライアント IDを作成していますが、種類が「ウェブ アプリケーション」になってしまっているため新たに作成していきます。**

その前に、OAuth同意画面の設定を行います。

User Typeを「外部」にチェックして、作成します。

![Snipaste_2021-11-28_21-27-53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163887/b865d81f-c208-1185-ea1c-f1cfced02113.png)

アプリ名、ユーザーサポートメール、デベロッパーの連絡先情報を入力して保存して次へ。


![Snipaste_2021-11-28_21-28-57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163887/30104d7f-0fa7-8858-44d2-7a9bfed312ca.png)


![Snipaste_2021-11-28_21-29-38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163887/3702bc50-1645-831c-a1af-ca04122092ed.png)

スコープは「Googleアカウントのメールアドレスの参照」にチェックを入れて保存して次へ。

![Snipaste_2021-11-28_21-30-03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163887/19499c05-2f2d-5707-cc35-b1b6162128fa.png)

省略可能な情報は、何もせず保存して次へ。

再度、OAuth 同意画面に戻りテストユーザーに自分のGoogleアカウントのメールアドレスを登録しておきます。

![Snipaste_2021-11-28_21-33-55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163887/ca9102f5-b143-64fe-1946-6050f60dac41.png)

続いて、OAuthクライアントIDを作成していきます。

![Snipaste_2021-11-28_21-35-04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163887/e0b8a721-062f-0c8b-122e-d94a69841e0b.png)

アプリケーションの種類を「Chromeアプリ」、名前を「sample-project」、アプリケーションIDはChrome拡張機能の画面から取得したIDを入力して作成します。

![Snipaste_2021-11-28_21-36-06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163887/04bd05d1-178e-a3ad-dabd-26fb6c40dc9a.png)

アプリケーションIDは下記から取得します。

![Snipaste_2021-11-28_21-35-54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163887/e79396a1-fd73-5087-d00d-cf7074e3ef15.png)

これでOAuth2クライアントIDを取得できるようになりました。

![Snipaste_2021-11-28_21-38-48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163887/cc9ebb2c-57ac-361d-00fb-d8a4bd9e6e7e.png)

##### 4.3 `chrome.identity.getAuthToken`でアクセストークンを取得

アクセストークンを取得できる準備ができたので実装をしていきます。

まずは、chromeのAPIを使う際に必要なライブラリをインストールしておきます。

```bash
npm install --save-dev @types/chrome # chromeの型情報を取得するため
```

マニフェストに取得したOAuth2クライアントIDとスコープを追加します。

```diff:src/manifest.ts
const getManifest = () => {
  return {
    manifest_version: 3,
    name: 'Getting Started Example',
    description: 'Build an Extension!',
    version: '1.0',
    action: {
      default_popup: './dist/popup/index.html'
    },
    permissions: [
      'identity'
    ],
+    oauth2: {
+      client_id: 'XXXXXXXXXXXX.apps.googleusercontent.com',
+      scopes: ['https://www.googleapis.com/auth/userinfo.email']
    }
  }
}
```

アクセストークンを取得するための処理を追加します。

```src/manifest.ts
import { initializeApp } from 'firebase/app'

const firebaseConfig = {
  apiKey: import.meta.env.VITE_FIREBASE_APIKEY,
  authDomain: import.meta.env.VITE_FIREBASE_AUTHDOMAIN,
  projectId: import.meta.env.VITE_FIREBASE_PROJECTID,
  storageBucket: import.meta.env.VITE_FIREBASE_STORAGEBUCKET,
  messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGINGSENDERID,
  appId: import.meta.env.VITE_FIREBASE_APPID
}
initializeApp(firebaseConfig)

// ChromeアプリからGoogleログインしてトークン取得
chrome.identity.getAuthToken(
  {interactive: true},
  (token: string) => {
    console.log('token', token)
  }
)
```

`npm run build`を行い、Chrome拡張機能を再読み込みしてChrome拡張機能のアイコンをクリックすると、Googleのサインインのポップアップが表示されるので、ログインするユーザーを選択します。

![Snipaste_2021-11-28_21-40-12 (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163887/dab4a945-154e-d76e-2a3b-8cc666d3632c.png)

「Continue」を選択します。

![Snipaste_2021-11-28_21-40-30 (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163887/9483009a-4708-b363-d7c6-6b684c57df1d.png)

「Allow」を選択します。

![Snipaste_2021-11-28_21-40-48 (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163887/418d14eb-fc11-c460-eeb3-84749323ed55.png)

コンソールを開いて、ログインユーザーのtokenが表示されていれば成功です。

![Snipaste_2021-11-28_21-45-28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163887/4a1275e2-8a1d-8d8b-cf46-128058793c9f.png)

#### 5. FirebaseでGoogle認証するための準備
##### 5.1 AuthenticationでGoogle認証を追加する

Authenticationで「始める」をクリックします。

![Snipaste_2021-11-28_22-09-43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163887/8c5ff2bb-7015-53f3-be86-33822cb0db82.png)

Sign-in methodの中から「Google」を選択します。

![Snipaste_2021-11-28_22-10-56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163887/c32ecc49-b4cd-876b-33a8-7eefd5913904.png)

「有効にする」を選択し、プロジェクトの公開名、プロジェクトのサポートメールを入力・選択して保存します。

![Snipaste_2021-11-28_22-13-42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163887/4eb9a8fb-790c-4f52-85dd-a650bcb14e49.png)

Googleが有効になっていればOKです。

![Snipaste_2021-11-28_22-57-47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163887/4d372141-6d3e-393e-66be-b9b51bc67c83.png)

##### 5.2 `signInWithCredential`を使ってFirebaseでGoogle認証する

firebase.tsを下記のように修正します。

```src/popup/firebase.ts
import { initializeApp } from 'firebase/app'
import { getAuth, GoogleAuthProvider, signInWithCredential } from 'firebase/auth'

const firebaseConfig = {
  apiKey: import.meta.env.VITE_FIREBASE_APIKEY,
  authDomain: import.meta.env.VITE_FIREBASE_AUTHDOMAIN,
  projectId: import.meta.env.VITE_FIREBASE_PROJECTID,
  storageBucket: import.meta.env.VITE_FIREBASE_STORAGEBUCKET,
  messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGINGSENDERID,
  appId: import.meta.env.VITE_FIREBASE_APPID
}
initializeApp(firebaseConfig)
const auth = getAuth()

// ChromeアプリからGoogleログインしてトークン取得
chrome.identity.getAuthToken(
  {interactive: true},
  (token: string) => {
    console.log('token', token)
    // Googleログイン成功時に受け取るトークンを使ってGoogleのクレデンシャル作成
    const credential = GoogleAuthProvider.credential(null, token)
    console.log('credential:', credential)
    console.log('auth:', auth)

    // Googleユーザーのクレデンシャルを使ってサインイン
    signInWithCredential(auth, credential).then((result) => {
      console.log("Sign In Success", result)
    }).catch((error) => {
      console.log("Sign In Error", error)
    })
  }
)
```

いつも通り`npm run build`を行って、Chrome拡張機能を再読み込みしてアイコンをクリックしてコンソールを開きます。

**下記のように`Sign In Success`となっていればFirebaseでGoogle認証できています。**

![Snipaste_2021-11-28_23-00-26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163887/c981a5c6-c563-2710-4e99-3965a48d00da.png)

ここまで長かったですね！！お疲れ様でした！
最後にChrome拡張機能(MV3)とFirebaseでGoogle認証のハマりポイントを紹介していきます。

## Chrome拡張機能(MV3) + FirebaseでGoogle認証する際のハマりポイント

### ハマりポイント① MV3からはリモートでホストされるコードは実行できなくなります

**これがMV3で作成する際の一番のハマりポイントだと思います。**

Chrome拡張機能の開発について検索する際にまだまだMV2で作成したときの記事が多く、APIを叩くような拡張機能を作る際には記事通りに作っているので動かないということが多かったです。

下記はGoogle公式サイトからの引用です。

> リモートでホストされるコード
**MV3 では、拡張機能が JavaScript や Wasm ファイルなどのリモート コードを読み込めないようになったことが、セキュリティ上の重要な改善点です。**これにより、Chrome ウェブストアに送信される拡張機能の安全な動作を、より確実かつ効率的に審査できるようになりました。**具体的には、すべてのロジックが拡張機能のパッケージに含まれている必要があります。**
https://developer.chrome.com/docs/extensions/mv3/intro/mv3-overview/#remotely-hosted-code

リモートでホストされるコードとは具体的には下記を指すようです。

> リモートでホストされるコードとは、拡張機能のパッケージにロード可能なリソースとして含まれていないコードを指します。たとえば、次の両方がリモートでホストされているコードと見なされます。
- **リモートサーバーからプルされたJavaScriptファイル**
- **実行時にevalに渡されるコード文字列**
https://developer.chrome.com/docs/extensions/mv3/intro/mv3-migration/#remotely-hosted-code

こちらについては、**Chrome拡張機能に必要なスクリプトは拡張機能に含めることで回避できます。**

MV2のサポートタイムラインは[こちら](https://developer.chrome.com/docs/extensions/mv3/mv2-sunset/)にあり、段階的にMV3に移行していくようですが、2023年6月には全てのMV2のChrome拡張機能が使えなくなるようです。

### ハマりポイント② firebaseの例でよく紹介されているsignInWithPopup自体はローカルからメソッド呼び出しできるがさらにそこからGoogleのリモートコードを実行するため使えない

Firebaseでの認証にGoogle認証を使う方法があるのですが、その中で[signInWithPopupを使う方法](https://firebase.google.com/docs/auth/web/google-signin)が紹介されています。

私の環境で動作するコードに書き換えていますが、下記のようなイメージです。

```ts
import { initializeApp } from 'firebase/app'
import { getAuth, GoogleAuthProvider, signInWithPopup } from 'firebase/auth'

const firebaseConfig = {
  apiKey: import.meta.env.VITE_FIREBASE_APIKEY,
  authDomain: import.meta.env.VITE_FIREBASE_AUTHDOMAIN,
  projectId: import.meta.env.VITE_FIREBASE_PROJECTID,
  storageBucket: import.meta.env.VITE_FIREBASE_STORAGEBUCKET,
  messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGINGSENDERID,
  appId: import.meta.env.VITE_FIREBASE_APPID
}
initializeApp(firebaseConfig)
const auth = getAuth()
const provider = new GoogleAuthProvider()

signInWithPopup(auth, provider)
  .then((result) => {
    console.log(result)
  }).catch((error) => {
    console.log(error)
  })
```

こちらを実行するとコンソールに下記エラーが表示されます。
Content Security Policy違反しているよ。外部のスクリプトを実行する場合はCSPヘッダに追加してねという感じです。

> Refused to load the script 'https://apis.google.com/js/api.js?onload=__iframefcb717050' because it violates the following Content Security Policy directive: "script-src 'self'". Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback.

manifest.jsonに下記のように`https://apis.google.com/js/api.js`を許可リストに含めてみます。

```manifest.json
# NG
  "content_security_policy": {
    "extension_pages": "script-src 'self' https://apis.google.com/js/api.js; object-src 'self'"
  },
```

すると、下記のように読み込み失敗するようになってしまいました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163887/13185aab-b20b-9219-fb07-5f8426e4f990.png)

manifest.jsonの書き方が間違っているのか確かめるために、`https://apis.google.com/js/api.js`を削除した状態だと読み込むことができました。

```manifest.json
# OK
  "content_security_policy": {
    "extension_pages": "script-src 'self'; object-src 'self'"
  },
```

CSPの読み込みが失敗する原因まではわかりませんでしたが、基本的にはリモートスクリプトはダメという方針なので、そもそもCSPで許可してあげても実行できないので意味がないです。

これらのことから分かるように、**signInWithPopupは内部で`https://apis.google.com/js/api.js`のスクリプトを呼び出しているため、MV3では使うことができません。**

**最終的には下記の記事を参考に`chrome.identity.getAuthToken`+`signInWithCredential`を使う方法で回避しました。**

[Chrome拡張＋FirebaseでGoogle認証するいくつかの方法 – rinoguchiブログ](https://rinoguchi.net/2020/05/chrome-extension-firebase-google-authentication.html)


### ハマりポイント③ FirebaseのAuthenticationでGoogleを有効にし用とした際に「Google の更新中にエラーが発生しました」と表示されて保存できない

**この問題は、Firebaseによってデフォルトで自動生成されたOAuthクライアントを削除することで発生するようです。**

私は諦めて、再度Firebaseのプロジェクトを作成するところからやり直すことで解決しました…

https://stackoverflow.com/questions/67204755/firebase-authentication-unable-to-enable-google-auth-method-error-updating-go


## 参考記事紹介

- [kawamataryo/sync\-raise\-hand: Your real raised hand will be synchronized with the action of the raise button in Google Meet\.](https://github.com/kawamataryo/sync-raise-hand)
    - **Chrome拡張機能をVue.js+TypeScriptで作成されていて、ビルド環境を作る際などとても参考にさせて頂きました。人のコードを見るのは本当に学びが多く大変勉強させていただきました。**
- [Getting started \- Chrome Developers](https://developer.chrome.com/docs/extensions/mv3/getstarted/)
    - Google公式のChrome拡張機能を開発するための入門記事です。**一通り触ることでmanifest.json、background.js、popup.htmlを使ってブラウザ右上の拡張機能をクリックしたときに表示されるUIを作ることができるようになります。**
- [Content scripts \- Chrome Developers](https://developer.chrome.com/docs/extensions/mv3/content_scripts/#programmatic)
    - 上記の入門記事ではコンテンツスクリプトについて触れられていないので、**画面のDOMを操作したChrome拡張機能を作りたいという方はこちらも見ておきましょう。**
- [Overview of Manifest V3 \- Chrome Developers](https://developer.chrome.com/docs/extensions/mv3/intro/mv3-overview/)
    - MV2で作られた記事が多いため、私のようにハマる前に公式を見てMV3に学びましょう。**MV2からMV3に変わったことで、セキュリティが厳しくなったため、例えばChrome拡張機能からFirebaseなどのリモートサーバーのスクリプトは実行できなくなっているので注意です(実行するためのコードは全てChrome拡張機能側にダウンロードしておく必要があります)。**
- [2017\-07\-11のJS: npm 5\.2\.0\(npx\)、lerna 2\.0\.0、WebRTC \- JSer\.info](https://jser.info/2017/07/11/npm-5.2.0npx-lerna-2.0.0-webrtc/)
    - npxのコマンドについて詳しいです。
- [話題の爆速CLIツール「Vite」をVue\.jsの定番ツール「Vue CLI」と徹底比較！ \(1/3\)：CodeZine（コードジン）](https://codezine.jp/article/detail/14939)
    - Viteを使って作成した場合とVue CLIを使って作成した場合の比較など参考になりました。

## まとめ
今回はビルド環境にViteを使ってみたり、初めてのChrome拡張機能開発で調査がしんどいうえにFirebaseのGoogle認証までやったのでハマりどころが多すぎてやりたいことができずに辛かったです…

今後、MV3でChrome拡張機能される方の少しでも参考になれば記事を頑張って書いてよかったなと思えるのでLGTM頂けると嬉しいですmm

現場からは以上です。