【Nuxt×Firebase】ServiceWorkerでセッション管理し、サーバー側で認証状態を確認する方法

どーも、ぐるたか@guru_takaです。

今までNuxtとFirebaseのログイン情報をonAuthStateChangedで監視していました。

しかし、リロードすると、ログイン情報を取得するまでにラグが発生。そこでSSRする前に、サーバー側でログイン情報を取得して、ラグなしでレンダリングする方法を探しました。

すると、公式でService Worker によるセッション管理がする方法があったので、実際に試しました。

すると、動画のように、ログイン情報に関わる描画のラグがなくなって超ハッピー!

今回はその方法を紹介していきます。

大まかな仕組み

Service Workerを利用して、サーバーへリクエストする時、ユーザーのIDトークンをリクエストヘッダーに追加します。

そして、Nuxtが最初に起動するタイミングで、サーバー側にて発火するnuxtServerInitで、認証状態をチェック!

具体的には、リクエストにヘッダーに追加されたIDトークンを取得し、firebase-adminで有効性を確認します。

後はIDトークンをデコードして、認証中のユーザー情報を復元するイメージです!

大まかな手順

STEP.1
Nuxt PWAのモジュールをインストール
STEP.2
Firebase Auth用のService Workerを作成
STEP.3
Service Worker周りの設定をnuxt.config.jsに追加
STEP.4
firebase-adminのインスタンス初期化用のファイルを準備
STEP.5
vuex内のnuxtServerInitで、認証状態をチェック

STEP.1:Nuxt PWAのモジュールをインストール

まずは、PWAサポートのモジュールをインストール!

コマンドライン
$ npm i @nuxtjs/pwa

その後、nuxt.config.jsに追記しましょう。

nuxt.config.js
{
    modules: [
        '@nuxtjs/pwa',
    ],
}
参考 ⚡ Nuxt PWA
MEMO
npm create nuxt-appでPWAサポートをすでにインストールしていれば、STEP1をスキップしてOKです!

STEP2:Service Workerの作成

続いて、ervice Workerを作成していきます。

ここは公式からコピペすればOK!

Nuxt+Firebaseでセッション管理: PWA(Service Worker)編に、日本語にてわかりやすいコメントが記載されているので、引用させて頂きます。

本当にありがたい、感謝!!!

以下のコードでやっていることは

STEP.1
リクエスト時にユーザーからトークンIDを取得
STEP.2
IDトークンをリクエストヘッダーに追加

です。

~/static/sw-firebase-auth.js
// firebaseを初期化
firebase.initializeApp({
  apiKey: /* API_KEY */,
  authDomain: /* AUTH_DOMAIN */,
  databaseURL: /* DATABASE_URL */,
  projectId: /* PROJECT_ID */,
  storageBucket: /* STORAGE_BUCKET */,
  messagingSenderId: /* MESSAGING_SENDER_ID */,
  appId: /* APP_ID */,
  measurementId: /* AUTH_DOMAIN */
});

// onAuthStateChanged()で現在のuserからidTokenを取得
const getIdToken = () => {
  return new Promise((resolve, reject) => {
    const unsubscribe = firebase.auth().onAuthStateChanged(user => {
      unsubscribe();
      if (user) {
        user.getIdToken().then(
          idToken => resolve(idToken),
          error => resolve(null)
        );
      } else {
        resolve(null);
      }
    });
  });
};

// URLからルートのURLを取得する処理
const getOriginFromUrl = url => {
  // https://stackoverflow.com/questions/1420881/how-to-extract-base-url-from-a-string-in-javascript
  const pathArray = url.split("/");
  const protocol = pathArray[0];
  const host = pathArray[2];
  return protocol + "//" + host;
};

/**
 *  Service Workderのライフサイクルでfetchしたときの処理
 */
self.addEventListener("fetch", event => {

  // リクエストをラップして、ヘッダにFirebase AuthのIdTokenを追加する処理
  const requestProcessor = idToken => {
    let req = event.request;
    // URLを取得して、httpsもしくはlocalhostかなどをチェック
    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]);
      }
      // クローンしたヘッダにFirebase AuthのIdTokenを追加
      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.error(e);
      }
    }
    return fetch(req);
  };

  // 上の関数を使って、全リクエストでIdTokenの取得し、Firebase AuthのIdTokenを追加ようにする
  event.respondWith(getIdToken().then(requestProcessor, requestProcessor));
});

/**
 *  Service Workderのライフサイクルでactivateしたときの処理
 */
self.addEventListener("activate", event => {
  event.waitUntil(clients.claim());
});
参考 Service Workerの基本とそれを使ってできること - Qiita

STEP3:nuxt.config.jsの設定を追加

nuxt.config.jsにworkboxの設定を追加します。workboxはPWAを良い感じにしてくれるライブラリです。

参考 Workbox | Google Developers
nuxt.config.js
  workbox: {
    //firebaseのauth周りが使えうように、CDNでfirebase-appとfirebase-authを追加
    importScripts: [
      "https://www.gstatic.com/firebasejs/7.6.1/firebase-app.js",
      "https://www.gstatic.com/firebasejs/7.6.1/firebase-auth.js",
      "sw-firebase-auth.js"
    ],
    dev: process.env.MODE != "production",
  },

dev: process.env.MODE != "production",を追加することで、dev開発でもsw.jsが生成されます!

上記を生成しないと、sw.jsはプロダクションモードのでしか生成されないので、注意してください。

STEP4:firebase-adminのインスタンス初期化用のファイルを準備

リクエストにヘッダーに追加されたIDトークンの有効性をサーバー側で確認するためにFirebase Admin SDKを準備します。

また、ユーザー認証後にサーバー側でfirestoreなども使えるようになります!

コマンドライン
$ npm install firebase-admin --save
~/utils/firebaseAdmin.js
let admin

if (process.server) {
  admin = require('firebase-admin')
  if (!admin.apps.length) {
    const serviceAccount = require('./path/serviceAccountKey.json')
    admin.initializeApp({
      credential: admin.credential.cert(serviceAccount),
      databaseURL: '自分のdatabaseURL'
    })
  }
}

export default admin

秘密キーのダウンロード方法は、こちらの記事にスクショ付きでまとめていますので、参考にしてみて下さい!
【Nuxt】NuxtServerInit内でfirestoreからデータを取得する方法

STEP5:nuxtServerInitで、認証状態をチェック

後は、nuxtServerInitでリクエストヘッダーにあるトークンIDをデコードしていきます。

コメント付きのソースを以下に記します!

~/store/index.js
async nuxtServerInit(
    { state, commit, dispatch },
    { app, error, route, redirect, req }
  ) {
    // requestのAuthorizationからIDトークンを取得
    const authorizationHeader = req.headers.authorization || ''

    //配列化
    const components = authorizationHeader.split(' ')

    //配列の2番目の要素がトークンID
    const token = components.length > 1 ? components[1] : ''
    if (!token) return

    // firebase-adminの初期化
    const admin = require('~/utils/firebaseAdmin').default
    if (!admin) return

    // IDトークンの検証&デコード: 有効期限などをFirebaseでチェック
    const decodedClaims = await admin.auth().verifyIdToken(token)

    // 検証結果からuser情報を取得
    //decodedClaims:デコードした属性情報
    const user = decodedClaims

  
    // TODO:user.user_idがuid
    // uid使った処理を以下に書く
  },

これで、サーバー側で認証チェックできるので、認証周りのレンダリングやリダイレクト処理などもSSRできてUXがより良くなります!

認証後にfirestoreを使いたい方は以下の記事を参考にしてみて下さい!
【Nuxt】NuxtServerInit内でfirestoreからデータを取得する方法

注意:ハードリロード時は、Sevice Workerは動かない

ハードリロードした時はSeviceWorkerが動かないので、サーバー側でユーザー認証できないので注意しましょう。

参考リンク

参考 Firebase (Hosting × Functions) × Nuxt.js (universal) で ユーザ認証のベストプラクティスを探る旅 その2 - Qiita 参考記事のタイトルとURLを入力してください Firebase Authentication の覚書② ServiceWorkerによるセッション管理 – WebTecNote” site=”” target=”_blank”] 参考 Nuxt+Firebaseでセッション管理: PWA(Service Worker)編 - くらげになりたい。

コメントを残す