Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

はじめに

令和の時代がやって参りました!

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

目標

前回と同じ

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

辿り着いた答え

ポイント

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

ディレクトリ構成

ディレクトリ構成
.
├── 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 配下にアンスコ有無の2ファイルがある理由は後述。

導入の流れ

公式ドキュメント に沿って、どのように導入していったか書いておきます。

Service Worker に対する変更

ユーザーがログインしたときに現在の ID トークンを取得できるように、Service Worker に Auth ライブラリを組み込む必要があります。

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

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

? 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公式ドキュメントを読んだところ、スクリプトが外出しでき、nuxt.conf.jsでインポートする設定で行けることが分かりました。

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

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

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

Authライブラリの組み込み

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

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

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

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だと外部ライブラリのインポートできないけど、外部ライブラリ全部ファイルの中に埋め込んじゃえば万事オッケーじゃん☆☆ という発想のシロモノです。

$ yarn global add browserify

で導入して

$ 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に入れておくと良いかもしれませんね。

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

/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として実装すれば動きました。
これも9割型コピペなので、スクリプトを見ていただくのが早いと思います。

/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に怒られます。

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なかったよなあ。。。今更。

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

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

追記

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

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

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

終わりに

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

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

daishinkawa
沖縄在住のエンジニアです。フロントエンド、アプリケーション、サーバ、ネットワーク、クラウド関連を広く浅く経験しており、今は絶賛フロントエンド研鑽の毎日です。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした