tag:crieit.net,2005:https://crieit.net/tags/Firebase-Auth/feed 「Firebase-Auth」の記事 - Crieit Crieitでタグ「Firebase-Auth」に投稿された最近の記事 2019-12-29T19:07:50+09:00 https://crieit.net/tags/Firebase-Auth/feed tag:crieit.net,2005:PublicArticle/15656 2019-12-29T19:07:03+09:00 2019-12-29T19:07:50+09:00 https://crieit.net/posts/Nuxt-Firebase-PWA-Service-Worker Nuxt+Firebaseでセッション管理: PWA(Service Worker)編 <p>FirebaseとSSRなNuxt.jsでアプリを作っていて、<br /> クライアント側で認証チェックするとFirebaseの初期化などでラグが...<br /> サーバ側で認証情報とかを取得してもう少しなんとかできないかなと。</p> <p>まだベータっぽい?けど、公式の以下の内容を試してみたときの備忘録。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/auth/web/service-worker-sessions?hl=ja">Service Worker によるセッション管理  |  Firebase</a></li> </ul> <h3 id="よく出てくる言葉"><a href="#%E3%82%88%E3%81%8F%E5%87%BA%E3%81%A6%E3%81%8F%E3%82%8B%E8%A8%80%E8%91%89">よく出てくる言葉</a></h3> <p>単語はよく聞くけど、ちゃんと見てなかったので、ざっくりとしたまとめ</p> <ul> <li>PWA: ネイティブアプリみたいなUXを提供するWebアプリ <ul> <li><a target="_blank" rel="nofollow noopener" href="https://developers.google.com/web/progressive-web-apps/">Progressive Web Apps  |  Google Developers</a></li> </ul></li> <li>Service Worker: PWAを実現するための基盤技術。独自のライフサイクルを持ってる <ul> <li><a target="_blank" rel="nofollow noopener" href="https://developers.google.com/web/fundamentals/primers/service-workers?hl=ja">Service Worker の紹介  |  Web Fundamentals  |  Google Developers</a></li> </ul></li> <li>Workbox: PWAでよく使うコード(ボイラープレート)やベストプラクティスを提供するライブラリ <ul> <li><a target="_blank" rel="nofollow noopener" href="https://developers.google.com/web/tools/workbox/">Workbox  |  Google Developers</a></li> </ul></li> <li>Nuxt PWA: NuxtでPWAするときに使うプラグイン。workboxを使ったService Worker(sw.js)とかを生成してくれる <ul> <li><a target="_blank" rel="nofollow noopener" href="https://pwa.nuxtjs.org/">⚡ Nuxt PWA</a></li> </ul></li> </ul> <h3 id="Nuxt PWAを使ってみる"><a href="#Nuxt+PWA%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%BF%E3%82%8B">Nuxt PWAを使ってみる</a></h3> <h4 id="インストール"><a href="#%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">インストール</a></h4> <pre><code class="shell">$ npm install @nuxtjs/pwa </code></pre> <h4 id="設定"><a href="#%E8%A8%AD%E5%AE%9A">設定</a></h4> <p>設定は簡単。modulesに<code>@nuxtjs/pwa</code>を追加するだけ。</p> <pre><code class="typescript">// nuxt.config.ts import { Configuration } from "@nuxt/types"; const config: Configuration = { // ...略 modules: [ // ... 略 "@nuxtjs/pwa", ], // ...略 }; export default config; </code></pre> <h3 id="Firebase Authと組み合わせる"><a href="#Firebase+Auth%E3%81%A8%E7%B5%84%E3%81%BF%E5%90%88%E3%82%8F%E3%81%9B%E3%82%8B">Firebase Authと組み合わせる</a></h3> <p>このあたりを見つつ、Firebase Authの情報を扱えるようにする。<br /> - <a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/auth/web/service-worker-sessions?hl=ja">Service Worker によるセッション管理  |  Firebase</a><br /> - <a target="_blank" rel="nofollow noopener" href="https://qiita.com/daishinkawa/items/915d918aba6bf7849b21#auth%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%81%AE%E7%B5%84%E3%81%BF%E8%BE%BC%E3%81%BF">Firebase (Hosting × Functions) × Nuxt.js (universal) で ユーザ認証のベストプラクティスを探る旅 その2 - Qiita</a></p> <p>流れ的には、以下の通り。</p> <ol> <li>Firebase Auth用のService Workerの作成して、リクエストにIDトークンを付与するように変更</li> <li>作成したService Workerを使うよう、nuxt.config.tsに設定を追加</li> <li></li> </ol> <h4 id="Firebase Auth用のService Workerの作成"><a href="#Firebase+Auth%E7%94%A8%E3%81%AEService+Worker%E3%81%AE%E4%BD%9C%E6%88%90">Firebase Auth用のService Workerの作成</a></h4> <p>長めだけど、ほぼ<a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/auth/web/service-worker-sessions?hl=ja">公式ドキュメント</a>のまま。</p> <pre><code class="javascript">// ~/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()); }); </code></pre> <p>やっていることは、以下のような感じ。</p> <ol> <li>リクエストするときに、</li> <li>現在のユーザからIDトークンを取得して</li> <li>IDトークンをリクエストヘッダーに追加する</li> </ol> <p>ヘッダーにIDトークンが付与されているので、<br /> サーバ側でそれを見て、認証済みかをチェックする。</p> <h3 id="nuxt.config.tsへの取り込み"><a href="#nuxt.config.ts%E3%81%B8%E3%81%AE%E5%8F%96%E3%82%8A%E8%BE%BC%E3%81%BF">nuxt.config.tsへの取り込み</a></h3> <p>作成したサービスワーカを使うように、nuxt.config.tsに設定を追加する。<br /> ドキュメントだと<a target="_blank" rel="nofollow noopener" href="https://pwa.nuxtjs.org/modules/workbox.html#adding-custom-service-worker">このあたり</a>を参照。</p> <pre><code class="typescript">// 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; </code></pre> <h4 id="nuxtServerInitなどで認証状態をチェックする"><a href="#nuxtServerInit%E3%81%AA%E3%81%A9%E3%81%A7%E8%AA%8D%E8%A8%BC%E7%8A%B6%E6%85%8B%E3%82%92%E3%83%81%E3%82%A7%E3%83%83%E3%82%AF%E3%81%99%E3%82%8B">nuxtServerInitなどで認証状態をチェックする</a></h4> <p>IDトークンもJWTデコードすると、UIDを取得できるけれど、<br /> 有効かどうかをfirebase-adminでチェックする必要がある。</p> <p>なので、まずは、firebase-adminのインスタンスを初期化するファイルを用意。</p> <pre><code class="typescript">// ~/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; </code></pre> <p>次にこれを使って、以下をしていく。</p> <ol> <li>リクエストヘッダからIDトークンを取得し、</li> <li>firebase-adminを使ってIDトークンの有効性を確認</li> </ol> <p>以下は、<a target="_blank" rel="nofollow noopener" href="https://github.com/championswimmer/vuex-module-decorators">vuex-module-decorators</a>を使ったnuxtServerInitでチェックするサンプル。</p> <pre><code class="typescript">// ~/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"; </code></pre> <p>firebase-adminを取得する部分を</p> <pre><code class="typescript">const admin = require("firebase-admin"); </code></pre> <p>としていたり、<code>if (process.server)</code>や<code>if (!admin.apps.length)</code>などのチェックをせずにいたら、<br /> クライアント側でもバンドルされていて、うまく動かいない状態に...</p> <h4 id="注意点"><a href="#%E6%B3%A8%E6%84%8F%E7%82%B9">注意点</a></h4> <p>これで認証が必要なページでもいい感じSSRできた気がする(<em>´ω`</em>)</p> <p>ただ、課題が残っていて、スーパーリロード/ハードリロードすると、<br /> Service Workderを介して、リクエストされないので、ヘッダに認証情報が付与されない...</p> <p>ログを見ていると、スーパーリロード時には、描画されたあとに、Service Workderの登録されているよう。</p> <p>そういった場合でも、利用したい場合には、従来のセッションCookieを利用する方法がよいかも?</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/auth/admin/manage-cookies?hl=ja">セッション Cookie を管理する  |  Firebase</a></li> </ul> <p>(もし良い方法があれば、教えてほしいです...)</p> <hr /> <h4 id="おまけ: Service Workerのライフサイクル"><a href="#%E3%81%8A%E3%81%BE%E3%81%91%3A+Service+Worker%E3%81%AE%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B5%E3%82%A4%E3%82%AF%E3%83%AB">おまけ: Service Workerのライフサイクル</a></h4> <p>ここに書いてあった。<br /> - <a target="_blank" rel="nofollow noopener" href="https://developers.google.com/web/fundamentals/primers/service-workers?hl=ja">Service Worker の紹介  |  Web Fundamentals  |  Google Developers</a></p> <p><img src="https://developers.google.com/web/fundamentals/primers/service-workers/images/sw-lifecycle.png?hl=ja" width="400px"/></p> <hr /> <p>以上!!</p> <h2 id="こんなのつくってます!!"><a href="#%E3%81%93%E3%82%93%E3%81%AA%E3%81%AE%E3%81%A4%E3%81%8F%E3%81%A3%E3%81%A6%E3%81%BE%E3%81%99%21%21">こんなのつくってます!!</a></h2> <p>積読用の読書管理アプリ 『積読ハウマッチ』をリリースしました!<br /> <a target="_blank" rel="nofollow noopener" href="https://tsundoku.site">積読ハウマッチ</a>は、Nuxt.js+Firebaseで開発してます!</p> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/572d4947-f40b-e4dc-1c9c-bc584cd2a66c.png" width="200"/></p> <p>もしよかったら、遊んでみてくださいヽ(=´▽`=)ノ</p> <p>要望・感想・アドバイスなどあれば、<br /> 公式アカウント(<a target="_blank" rel="nofollow noopener" href="https://twitter.com/MemoryLoverz">@MemoryLoverz</a>)や開発者(<a target="_blank" rel="nofollow noopener" href="https://twitter.com/kira_puka">@kira_puka</a>)まで♪</p> <h1 id="参考にしたサイト様"><a href="#%E5%8F%82%E8%80%83%E3%81%AB%E3%81%97%E3%81%9F%E3%82%B5%E3%82%A4%E3%83%88%E6%A7%98">参考にしたサイト様</a></h1> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/auth/web/service-worker-sessions?hl=ja">Service Worker によるセッション管理  |  Firebase</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/daishinkawa/items/915d918aba6bf7849b21#auth%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%81%AE%E7%B5%84%E3%81%BF%E8%BE%BC%E3%81%BF">Firebase (Hosting × Functions) × Nuxt.js (universal) で ユーザ認証のベストプラクティスを探る旅 その2 - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/basho/items/acd6a17bb6e2a2f7a932#section3%E6%96%B0%E8%A6%8F%E7%99%BB%E9%8C%B2google%E3%82%A2%E3%82%AB%E3%82%A6%E3%83%B3%E3%83%88%E3%81%AE%E5%AE%9F%E8%A3%85">SSRモードのNuxtでのFirebase認証 - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/y_fujieda/items/f9e765ac9d89ba241154#service-worker">Service Workerの基本とそれを使ってできること - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/horo/items/175c8fd7513138308930">ServiceWorkerとCache APIを使ってオフラインでも動くWebアプリを作る - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle?hl=ja">Service Worker のライフサイクル  |  Web Fundamentals  |  Google Developers</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/auth/admin/manage-sessions">ユーザー セッションの管理  |  Firebase</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/auth/admin/manage-cookies">Manage Session Cookies  |  Firebase</a></li> </ul> きらぷか@積読ハウマッチ/SSSAPIなど