Firebase (Hosting × Functions) × Nuxt.js (universal) で ユーザ認証のベストプラクティスを探る旅 その２

# はじめに
令和の時代がやって参りました！

一昨日、[Firebase (Hosting & Functions) × Nuxt.js (universal) で ユーザ認証のベストプラクティスを探る旅](https://qiita.com/daishinkawa/items/5a0c12db05576f30a177#comment-986910a3f43558cf86e7)という記事を投稿させていただいたのですが、「Service Workerを利用した構成の方がオススメ！」という有難い助言を頂戴しましたので、早速実践してみました。
大枠は前記事の通りですが、 nuxt 標準の pwa-module を使い、 Service Worker を利用してセッションを保持する実装にしています。
初学者としてつまづいたポイントと一緒に、構成を残しておきます。

# 目標
[前回](https://qiita.com/daishinkawa/items/5a0c12db05576f30a177#comment-986910a3f43558cf86e7)と同じ

- NuxtはUniversalモードで起動する
- Firebase HostingとFirebase FunctionsでサーバレスSSRを実現する
- `/account/login` にレンダリングしたFirebaseUIからFirebase Authenticationでログインする
   - ログイン後は `index` にリダイレクトする
- ログインユーザ情報をコンテンツ中で取得できるようにする
- ユーザがログインしていない場合、どのページにアクセスしても `/account/login` にリダイレクトする
    - リダイレクト前に元ページのレンダリングはしない

# 辿り着いた答え
### ポイント
- Service Workerをnuxtに導入するにあたり `yarn create nuxt-app` で `Choose features to install` と聞かれた時に **`Progressive Web App (PWA) Support`** にチェックを入れていれば、追加パッケージ等は必要ありませんでした。
- [Firebase公式](https://firebase.google.com/docs/auth/web/service-worker-sessions?hl=ja)を参考に導入しましたが、nuxtのPWA Supportがお膳立てしてくれている部分が結構あります。firebase-auth用のservice worker を新しく作成し、`importScripts`で読み込むという方針であれば、Service Workerを追加する処理は不要でした。
- クライアント側はService Workerさえ導入してしまえば、その他変更なくただリクエスト投げるだけというびっくり簡単っぷりでした。
- ルーティング時の認証チェックは様々な実装方法があると思いますが、前回同様サーバ限定middlewareですんなり実装できました。
- 多分初学者限定ですが・・・service-workerはブラウザ上で動くので、importとかrequireができず苦労しました。
- これも初学者限定ですが・・・service-workerはFirebase Hostingでホストさせる必要があるというのがわからず苦戦しました。

### ディレクトリ構成

```console:ディレクトリ構成
.
├── firebase.json
├── functions
│   ├── index.js
│   ├── nuxt
│   └── package.json
├── public
│   ├── sw-firebase-auth.js
│   └── sw.js
└── src
    ├── assets
    ├── components
    ├── layouts
    ├── middleware
    │   └── authenticated.js
    ├── nuxt.config.js
    ├── package.json
    ├── pages
    ├── plugins
    │   ├── firebase-admin.js
    │   └── firebase.js
    ├── server
    ├── static
    │   ├── _sw-firebase-auth.js
    │   ├── sw-firebase-auth.js
    │   └── sw.js
    └── store
```

例によって無駄なのは省いてます。
新しく追加したこととして、static配下とpublicに `sw-firebas-auth.js` が追加されています。`src/static` 配下にアンスコ有無の２ファイルがある理由は後述。

### 導入の流れ
[公式ドキュメント](https://firebase.google.com/docs/auth/web/service-worker-sessions?hl=ja) に沿って、どのように導入していったか書いておきます。
#### Service Worker に対する変更
> ユーザーがログインしたときに現在の ID トークンを取得できるように、Service Worker に Auth ライブラリを組み込む必要があります。

ここで「Service Worker？え、どこにそんなファイルが？？」でだいぶ詰みました。

色々文献を漁り、どうやら**PWAサポートが追加されていればnuxtがいい感じにservice workerを追加してくれる**らしいことがわかってきました。
`yarn create nuxt-app`のダイアログで聞かれるコレ↓ですね。後から追加するやり方もあるみたいです。

```console
? Choose features to install (Press <space> to select, <a> to toggle all, <i> to invert selection)
❯◯ Progressive Web App (PWA) Support
 ◯ Linter / Formatter
 ◯ Prettier
 ◯ Axios
```
で、 `yarn run build` すると勝手に `/src/static/sw.js` が生成されてました。簡単ですね。

「じゃあ編集対象ってどのファイルなの？？」で結構悩みましたが、[pwa-module公式ドキュメント](https://pwa.nuxtjs.org/)を読んだところ、スクリプトが外出しでき、`nuxt.conf.js`でインポートする設定で行けることが分かりました。

```javascript:/src/nuxt.conf.js
  workbox: {
    importScripts: [
      'sw-firebase-auth.js'
    ]
  }
```

コレだけでService Workerを追加することができます。簡単・・・。
で、`/src/static/sw-firebase-auth.js` を追加してこいつを編集していけばOKでした。

余談ですが、[pwa-module公式ドキュメント](https://pwa.nuxtjs.org/)の記載だと、importScriptsのjsファイルは`/assets`に配置するだけでオッケー！と書いてますが、`/static`の間違いじゃないだろうか・・・。

#### Authライブラリの組み込み

次に詰まったのがここです。

```javascript:/src/static/sw-firebase-auth.js
// Initialize the Firebase app in the service worker script.
firebase.initializeApp(config);
```

ん？ `firebase`って未定義だからいきなり呼んだってダメでしょと思いながら動かしてみるとやっぱり`firebase is undefined`でChromeに怒られます。

```javascript:src/static/sw-firebase-auth.js
var firebase = require('firebase')

// Initialize the Firebase app in the service worker script.
firebase.initializeApp(config);
```
じゃあコレでいけるっしょ！と思いきや、`require`って何やねんとChromeに怒られます。

多分初学者あるある（だと思いたい）ですが、`require` でライブラリが読み込めるのはNode上だけだとよく分かっておりませんでした。（だってvueだと普通にクライアントで動いてるっぽい感じで書けるから・・・恥）
service workerはブラウザ上で動くため、`require`しようとしてもできないわけですね。
となると外部ライブラリをどうやって読み込むんじゃいと色々調べた結果、HTML側で `<script src="">` を実行する以外に方法はなく、javascriptにライブラリインポートの概念自体存在しないことがわかってきました。
jsファイル内に `<script src="">` を書いてみるという恥ずかしい悪あがきもして撃沈した後、`Browserify` を使うという解決策にたどり着きました。

私と同じく初学者向けにBrowserifyを簡潔にご説明すると、**jsだと外部ライブラリのインポートできないけど、外部ライブラリ全部ファイルの中に埋め込んじゃえば万事オッケーじゃん☆☆** という発想のシロモノです。

```console
$ yarn global add browserify
```

で導入して

```console
$ mv static/sw-firebase-auth.js static/_sw-firebase-auth.js
$ browserify static/_sw-firebase-auth.js -o static/sw-firebase-auth.js
```

で変換完了です。
当然、firebaseがまるっとファイルに含まれるので、正味78行のファイルが50,000行くらいになりました。
もっといいやり方ないだろうか・・・。
毎回やるのは面倒なので、`package.json`のscripts.buildに入れておくと良いかもしれませんね。

あとは９割コピペですが、一応Service Workerの全文を残しておきます。

```javascript:/src/static/_sw-firebase-auth.js
var firebase = require('firebase')

firebase.initializeApp({
  apiKey: '*****************',
  authDomain: '*****************',
  databaseURL: '*****************',
  projectId: '*****************',
  storageBucket: '*****************',
  messagingSenderId: '*****************'
})

const getIdToken = () => {
  return new Promise((resolve) => {
    const unsubscribe = firebase.auth().onAuthStateChanged((user) => {
      unsubscribe();
      if (user) {
        user.getIdToken().then((idToken) => {
          resolve(idToken)
        }, () => {
          resolve(null)
        });
      } else {
        resolve(null)
      }
    })
  })
}

const getOriginFromUrl = (url) => {
  const pathArray = url.split('/');
  const protocol = pathArray[0];
  const host = pathArray[2];
  return protocol + '//' + host;
};

self.addEventListener('fetch', (event) => {
  const requestProcessor = (idToken) => {
    let req = event.request;
    if (self.location.origin == getOriginFromUrl(event.request.url) &&
        (self.location.protocol == 'https:' ||
         self.location.hostname == 'localhost') &&
        idToken) {
      const headers = new Headers();
      for (let entry of req.headers.entries()) {
        headers.append(entry[0], entry[1]);
      }
      headers.append('Authorization', 'Bearer ' + idToken);
      try {
        req = new Request(req.url, {
          method: req.method,
          headers: headers,
          mode: 'same-origin',
          credentials: req.credentials,
          cache: req.cache,
          redirect: req.redirect,
          referrer: req.referrer,
          body: req.body,
          bodyUsed: req.bodyUsed,
          context: req.context
        });
      } catch (e) {
        console.log(e)
      }
    }
    return fetch(req);
  };
  event.respondWith(getIdToken().then(requestProcessor, requestProcessor));
});

self.addEventListener('activate', event => {
  event.waitUntil(clients.claim());
})
```

#### サーバー側に対する変更
あとはほとんどコピペでサーバ側の変更をmiddlewareとして実装すれば動きました。
これも９割型コピペなので、スクリプトを見ていただくのが早いと思います。

```javascript:/src/middleware/authenticated.js
function getIdToken(req) {
  const authorizationHeader = req.headers.authorization || ''
  const components = authorizationHeader.split(' ')
  return components.length > 1 ? components[1] : ''
}

export default (({ req, redirect }) => {
  if (process.server && process.env.NODE_ENV === 'production') {
    var admin = require('firebase-admin')
    const idToken = getIdToken(req)
    admin.auth().verifyIdToken(idToken).then(() => {
      redirect('/')
    }).catch((error) => {
      console.log(error)
      redirect('/account/login')
    })
  }
})
```

※ 2019/5/3追記： Service WorkerはProduction環境でないと生成されないので、`process.env`の分岐を加えました。`yarn run dev`だとmiddlewareはスルーされますのでご留意下さい。

現状だとリロードすると毎回 `/` に戻っちゃうので、routerでうまく制御しないといけないですが、それは今後。
ログインしていない状態だとエラー吐いて気持ち悪いですが、それも今後。。

この状態で `yarn run start` するとちゃんと動いてくれました。クライアント側は何も一切気にせずただリクエストを投げればいいという、実にデベロッパフレンドリー。

#### Firebaseへのデプロイ
これが最大の詰まりポイントで、令和元年初日をまるっと奪い去っていきました。

ローカルで問題なく動いたので `firebase serve` でも全然いけるっしょ、と思いきや、以下Chromeに怒られます。

```console
A bad HTTP response code (404) was received when fetching the script.
Failed to load resource: net::ERR_INVALID_RESPONSE
dcbac67bb39a765db27a.js:1 Service worker registration failed: TypeError: Failed to register a ServiceWorker: A bad HTTP response code (404) was received when fetching the script.
```

NetworkやSourceタブをみる限り、どうやら `sw.js` が見つからない御様子。
`dist` を見ても、そもそもそんなファイルがデプロイされてない。でも `yarn run start` だと普通に動いている。
Firebaseを悪者にして「service-workerのファイル名に制約があるんでしょ？」とファイル名を公式サンプルの通りに変えてみてもダメ。
「じゃあsw.jsってやつ `dist/client` に手動で置いてやれ」と思って置いてみてもダメ。
「pwa-moduleとFirebaseの相性でうまく動かないんだ！手動でサービスワーカーを配置しよう！」これもダメ。

堂々巡りに陥っていましたが、ようやく解決策と自分の勘違いに気づきました。
**「Service Workerはブラウザで実行されるので、Firebase Hostingでホストされる必要がある」** これでした。
いくらdistを弄っても、Firebase Functions側の変更なので意味がなかったんですね。。。
それと同時に、`/src/static` 配下のファイルはFirebase Hostingでホスティングされる必要があるということに今更気づきました。今回の構成では `/public` 配下に置いてやる必要があります。
そういえば `firebase serve` だとfaviconなかったよなあ。。。今更。

```console
├── public
│   ├── sw-firebase-auth.js
│   └── sw.js
```

これで `firebase serve` `firebase deploy` 共にしっかり動きました。

##### 追記
`firebase deploy` を実行した際、deployされる前に実行するスクリプトをあらかじめ定義するという超絶便利機能があるとコメント頂戴しましたので、今回の場合の設定方法を記載しておきます！
deploy前に `/public` 配下を `/static` でまるっと更新しています。
【参考】[デプロイ前フックおよびデプロイ後フック](https://firebase.google.com/docs/cli/#predeploy_and_postdeploy_hooks)

```json:firebase.json
 {
   "hosting":{
     "postdeploy":"rm -rf public/* && cp src/static/* public/",
   }
 }
```

これで `firebase deploy` するだけで `/public` 配下が `/src/static` のファイルで更新されます。便利！

## 終わりに
[前回の記事](https://qiita.com/daishinkawa/items/5a0c12db05576f30a177#comment-986910a3f43558cf86e7)に有難いコメントを頂いたおかげで、Service Workerを用いた、より簡潔でパフォーマンスも高い実装にできたと思います。感謝感激雨嵐。
Firebase、Nuxtいずれも詰まるポイントは多いですが、大体自分のそもそものjavascript経験の浅さから来るものなので、解決してしまえば数ステップでかなりすんなりデプロイできてしまいます。面倒なことは全部これに任せて、サービスの市場投入を加速するにはもってこいですね。

今後はFirestore関連を掘り下げて学んでいきたいと思います。