くらげになりたい。

くらげのようにふわふわ生きたい日曜プログラマなブログ。趣味の備忘録です。

Nuxt+Firebaseでセッション管理: PWA(Service Worker)編

FirebaseとSSRなNuxt.jsでアプリを作っていて、
クライアント側で認証チェックするとFirebaseの初期化などでラグが...
サーバ側で認証情報とかを取得してもう少しなんとかできないかなと。

まだベータっぽい?けど、公式の以下の内容を試してみたときの備忘録。

よく出てくる言葉

単語はよく聞くけど、ちゃんと見てなかったので、ざっくりとしたまとめ

Nuxt PWAを使ってみる

インストール

$ npm install @nuxtjs/pwa

設定

設定は簡単。modulesに@nuxtjs/pwaを追加するだけ。

// nuxt.config.ts
import { Configuration } from "@nuxt/types";

const config: Configuration = {
  // ...略
  modules: [
    // ... 略
    "@nuxtjs/pwa",
  ],
  // ...略
};

export default config;

Firebase Authと組み合わせる

このあたりを見つつ、Firebase Authの情報を扱えるようにする。 - Service Worker によるセッション管理  |  Firebase - Firebase (Hosting × Functions) × Nuxt.js (universal) で ユーザ認証のベストプラクティスを探る旅 その2 - Qiita

流れ的には、以下の通り。

  1. Firebase Auth用のService Workerの作成して、リクエストにIDトークンを付与するように変更
  2. 作成したService Workerを使うよう、nuxt.config.tsに設定を追加

Firebase Auth用のService Workerの作成

長めだけど、ほぼ公式ドキュメントのまま。

// ~/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());
});

やっていることは、以下のような感じ。

  1. リクエストするときに、
  2. 現在のユーザからIDトークンを取得して
  3. IDトークンをリクエストヘッダーに追加する

ヘッダーにIDトークンが付与されているので、
サーバ側でそれを見て、認証済みかをチェックする。

nuxt.config.tsへの取り込み

作成したサービスワーカを使うように、nuxt.config.tsに設定を追加する。
ドキュメントだとこのあたりを参照。

// nuxt.config.ts
import { Configuration } from "@nuxt/types";

const config: Configuration = {
  // ...略
  modules: [
    // ... 略
    "@nuxtjs/pwa",
  ],
  workbox: {
    // 追加するスクリプトを指定。
    // バンドルされないので、CDNのfirebase-appを追加しておく。
    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"
    ],
    // 開発中でもsw.jsが生成されるように設定。
    dev: process.env.MODE != "production",
  },
  // ...略
};

export default config;

nuxtServerInitなどで認証状態をチェックする

IDトークンもJWTデコードすると、UIDを取得できるけれど、
有効かどうかをfirebase-adminでチェックする必要がある。

なので、まずは、firebase-adminのインスタンスを初期化するファイルを用意。

// ~/utils/firebaseAdmin.ts
let admin;

if (process.server) {
  admin = require("firebase-admin");
  if (!admin.apps.length) {
    const serviceAccount = require("./path/to/your/key.json");
    admin.initializeApp({
      credential: admin.credential.cert(serviceAccount),
      databaseURL: "https://your-database-url.firebaseio.com"
    });
  }
}

export default admin;

次にこれを使って、以下をしていく。

  1. リクエストヘッダからIDトークンを取得し、
  2. firebase-adminを使ってIDトークンの有効性を確認

以下は、vuex-module-decoratorsを使ったnuxtServerInitでチェックするサンプル。

// ~/store/index.ts

import { Context } from "@nuxt/types";
import { ActionContext } from "vuex/types";
import { ActionTree, Store } from "vuex";
import { initialiseStores } from "~/utils/store-accessor";

export const state = () => ({});
export type RootState = ReturnType<typeof state>;

const initializer = (store: Store<any>) => initialiseStores(store);
export const plugins = [initializer];

export const actions: ActionTree<any, any> = {
  async nuxtServerInit(
    context: ActionContext<RootState, RootState>,
    server: Context
  ) {
    // requestのAuthorizationからIDトークンを取得
    const authorizationHeader = req.headers.authorization || "";
    const components = authorizationHeader.split(" ");
    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);
    
    // 検証結果からUIDを取得
    const uid = decodedClaims.uid;
    
    // TODO: 認証状態に応じてなにかする
  }
};

export * from "~/utils/store-accessor";

firebase-adminを取得する部分を

const admin = require("firebase-admin");

としていたり、if (process.server)if (!admin.apps.length)などのチェックをせずにいたら、
クライアント側でもバンドルされていて、うまく動かいない状態に...

注意点

これで認証が必要なページでもいい感じSSRできた気がする(´ω`)

ただ、課題が残っていて、スーパーリロード/ハードリロードすると、
Service Workderを介して、リクエストされないので、ヘッダに認証情報が付与されない...

ログを見ていると、スーパーリロード時には、描画されたあとに、Service Workderの登録されているよう。

そういった場合でも、利用したい場合には、従来のセッションCookieを利用する方法がよいかも?

(もし良い方法があれば、教えてほしいです...)


おまけ: Service Workerのライフサイクル

ここに書いてあった。 - Service Worker の紹介  |  Web Fundamentals  |  Google Developers


以上!!

参考にしたサイト様