FirebaseとSSRなNuxt.jsでアプリを作っていて、
クライアント側で認証チェックするとFirebaseの初期化などでラグが...
サーバ側で認証情報とかを取得してもう少しなんとかできないかなと。
まだベータっぽい?けど、公式の以下の内容を試してみたときの備忘録。
よく出てくる言葉
単語はよく聞くけど、ちゃんと見てなかったので、ざっくりとしたまとめ
- PWA: ネイティブアプリみたいなUXを提供するWebアプリ
- Service Worker: PWAを実現するための基盤技術。独自のライフサイクルを持ってる
- Workbox: PWAでよく使うコード(ボイラープレート)やベストプラクティスを提供するライブラリ
- Nuxt PWA: NuxtでPWAするときに使うプラグイン。workboxを使ったService Worker(sw.js)とかを生成してくれる
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
流れ的には、以下の通り。
- Firebase Auth用のService Workerの作成して、リクエストにIDトークンを付与するように変更
- 作成した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()); });
やっていることは、以下のような感じ。
ヘッダーに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;
次にこれを使って、以下をしていく。
以下は、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
以上!!
参考にしたサイト様
- Service Worker によるセッション管理 | Firebase
- Firebase (Hosting × Functions) × Nuxt.js (universal) で ユーザ認証のベストプラクティスを探る旅 その2 - Qiita
- SSRモードのNuxtでのFirebase認証 - Qiita
- Service Workerの基本とそれを使ってできること - Qiita
- ServiceWorkerとCache APIを使ってオフラインでも動くWebアプリを作る - Qiita
- Service Worker のライフサイクル | Web Fundamentals | Google Developers
- ユーザー セッションの管理 | Firebase
- Manage Session Cookies | Firebase