ナビゲーションへスキップ コンテンツへスキップ

WebTecNote

気ままに綴る独学メモ帳

  • About
  • Contact
  • Downloads
メインナビゲーション

[Nuxt.js] Firebaseの覚書 ② ServiceWorkerによるセッション管理

2019/05/27By Tenderfeel Firebase, Javascript, Vue&Nuxt

NuxtでリファレンスにあるServiceWorkerによるセッション管理をやってみたのでここに記す…。

Firebase 認証 とかでググってみると、auth().onAuthStateChangedのイベントハンドラでstateを変更するのがよく引っかかるけど、
onAuthStateChangedは状態が反映されるまでの読み込みにかかるラグがどうしても発生するので、ログイン状態でも一瞬ログアウト状態が見えてしまう。
それを隠すためにローディングを入れるというのが合わせて出てくる手法なんだけれども、NuxtでSSRだとローディング表示はしたくないじゃないですかあ…。

リロードしてもログイン状態を即反映させるためには描画前にログインしているかどうかを判断しなければならないわけで、
それをやっつけるのがServiceWorkerによるセッション管理なのでした。
CookieではなくServiceWorkerなのは書いてある通りメリットが色々あるからですね。

Contents

  • 1 NuxtにおけるServiceWorkerの有効化
  • 2 ServerMiddlewareの用意
  • 3 Store/nuxtServerInit でユーザー情報受け取り
  • 4 Basic認証下における注意点
  • 5 verifyEmailが反映されない件
  • 6 Safariでエラーが出る件
    • 6.1 Storageへのファイルアップロードが失敗する件
    • 6.2 Cannot use wildcard in Access-Control-Allow-Origin when credentials flag is true
    • 6.3 ReadableStream uploading is not supported
    • 6.4 回避策
  • 7 自動ログアウト
    • 7.1 auth.onAuthStateChangedによるメモリリーク
  • 8 注意点
    • 8.1 シェア
    • 8.2 関連記事

NuxtにおけるServiceWorkerの有効化

自前でやってもいいけど大抵はPWAモジュール使うと思うのでPWAのWorkboxオプションでの書き方を載せておきます。

適当なディレクトリにfirebaseのserviceWorkerのソースをコピペして置いておく。

※仮に firebase/sw.js とする。

/* globals firebase, clients */
//https://firebase.google.com/docs/auth/web/service-worker-sessions
firebase.initializeApp({
 // TODO: 設定コピペする
});

/**
 * Returns a promise that resolves with an ID token if available.
 * @return {!Promise<?string>} The promise that resolves with an ID token if
 *     available. Otherwise, the promise resolves with null.
 */
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 => {
  // 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;
};

self.addEventListener('fetch', event => {
  const requestProcessor = idToken => {
    let req = event.request;
    // For same origin https requests, append idToken to header.
    if (
      self.location.origin == getOriginFromUrl(event.request.url) &&
      self.location.protocol == 'https:' && // localhost-> .hostname == 'localhost'
      idToken
    ) {
      // Clone headers as request headers are immutable.
      const headers = new Headers();
      for (let entry of req.headers.entries()) {
        headers.append(entry[0], entry[1]);
      }
      // Add ID token to header.
      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) {
        // This will fail for CORS requests. We just continue with the
        // fetch caching logic below and do not pass the ID token.
      }
    }
    return fetch(req);
  };
  // Fetch the resource after checking for the ID token.
  // This can also be integrated with existing logic to serve cached files
  // in offline mode.
  event.respondWith(getIdToken().then(requestProcessor, requestProcessor));
});

self.addEventListener('activate', event => {
  event.waitUntil(clients.claim());
});

onAuthStateChangedでgetIdToken実行してリクエストヘッダにauthorizationを追加する、という内容。

PWAモジュールいれたらnuxt.config.jsの workboxオプションでfirebase関連を追加する

  // https://pwa.nuxtjs.org/modules/workbox.html
  workbox: {
    offline: false,
    importScripts: [
      'https://www.gstatic.com/firebasejs/7.10.0/firebase-app.js',
      'https://www.gstatic.com/firebasejs/7.10.0/firebase-auth.js',
    ],
    workboxExtensions: ['~/firebase/sw.js'],
  },

バンドルするよりimportScriptsする方が楽だったのでこのように。
workboxExtensionsはworkboxが生成するsw.jsに読み込んだファイルの内容を追記するオプションです。
これでFirebaseのserviceWorkerのソースが取り込まれる。
(バージョンはpackage.json見て合わせること)

ServerMiddlewareの用意

リクエストヘッダに追加されたIDトークンを検証しなければならないが、検証するメソッドのあるAdmin SDKはnode.js(サーバーサイド)でしか実行できないのです。
のでServerMiddlewareを使う。

serverMiddleware は vue-server-renderer の 前に サーバー側で実行され、API リクエストの処理やアセットの処理などのサーバー固有のタスクとして使用できます。

ServerMiddlewareならSSR前にJWT検証したいという要求もばっちり満たしてくれる。

※ firebase/serverMiddleware.js とする
※ プロジェクト設定→サービスアカウント→Firebase Admin SDKで秘密鍵を生成する
※ 秘密鍵を含む設定ファイルは firebase/serviceAccountKey.json とする
※ 絶対にserviceAccountKey.jsonをパブリックな場所にコミットしないこと
(実際やらかした人がいるけど驚きの速さで悪用される)

ソースはリファレンスを参考に…、

const admin = require('firebase-admin');
const serviceAccount = require('../firebase/serviceAccountKey.json');

// The Firebase Admin SDK is used here to verify the ID token.
admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  databaseURL: 'https://your-database-url.firebaseio.com',
});

export default async function(req, res, next) {
  // /_nuxt/以下ファイルで実行しない
  if (req.url.indexOf('_nuxt') !== -1) return next();
  let idToken;
  if (
    req.headers.authorization &&
    req.headers.authorization.indexOf('Bearer ') !== -1
  ) {
    // Read the ID Token from the Authorization header.
    idToken = req.headers.authorization
      .split(',')
      .find(a => a.trim().startsWith('Bearer '))
      .split('Bearer ')[1];
  }

  if (idToken) {
    try {
      const decodedClaims = await admin.auth().verifyIdToken(idToken);
      req.authUser = decodedClaims;
    } catch (e) {
      delete req.authUser;
    }
  }

  next();
}

verifyIdTokenでトークンが確認されたらデコードされたユーザー情報をreq.authUserで追加するという処理してる。
nuxtが生成したファイル(_nuxt以下の)では不要なのでurlチェックして弾いている。

でこれをnuxt.config.jsに設定追加する。

serverMiddleware: ['~/firebase/serverMiddleware'],

Store/nuxtServerInit でユーザー情報受け取り

つつがなく動作すればstore/index.jsの nuxtServerInit のコンテキストでリクエストが受け取れるので、サーバーミドルウェアで追加したauthUserがあればストアにユーザーを追加する。

  async nuxtServerInit({ commit }, { app, error, route, redirect, req }) {
    try {
      const user = req.authUser || null;

      if (user) {
        commit('setUser', {
          name: user.name,
          email: user.email,
          avatar: user.picture,
          uid: user.user_id,
        });
      }

      // firestoreに用事があるならここでやる

    } catch (e) {
    }
  }

IDトークンがデコードされたオブジェクトのプロパティと、onAuthStateChangedが返すUserInfoとではキーがちょっと違うので使い回しに注意。

Basic認証下における注意点

もし開発環境などでBasic認証使ってるのであれば、authorizationはカンマ区切りの配列になる。
サーバー側がカンマ区切り未対応なら対応しておくこと。

verifyEmailが反映されない件

メールアドレス&パスワード認証を使っているとにメールアドレス確認状況がverfyEmailとして得られるが、
メールアドレス確認ではonAuthStateChangedが発火しないのでIDトークンが更新されない。
getUserを使って最新のユーザー情報を取得しなければならない。

try {
  // JWT確認 admin.auth.DecodedIdToken
  const decodedClaims = await admin.auth().verifyIdToken(idToken);
  // 最新のUserData取得
  const userData = await admin.auth().getUser(decodedClaims.uid);

  if (userData) {
    const data = userData.toJSON();
    delete data.passwordHash;
    delete data.passwordSalt;
    delete data.tokensValidAfterTime;
    req.authUser = data;
  }
} catch (e) {
  delete req.authUser;
}

Safariでエラーが出る件

Storageへのファイルアップロードが失敗する件

Storageへ画像保存しようとしたらエラーが出て死んだ。

[Error] FetchEvent.respondWith received an error: TypeError: Request header field Pragma is not allowed by Access-Control-Allow-Headers.
[Error] XMLHttpRequest cannot load https://firebasestorage.googleapis.com/v0/b/xxxxxxx.appspot.com/o?name=xxxxx.png.
[Error] Failed to load resource: FetchEvent.respondWith received an error: TypeError: Request header field Pragma is not allowed by Access-Control-Allow-Headers. (o, line 0)

なぜかSafariでエラーになり、Chromeでは発生しない。
gsutilでCORS設定しても解決しない。で、ググったら症例が見つかったので

Firebase storage upload fails on Safari on iOS 11 #1094

fetchの先頭にstorageのドメインなら弾く条件を追加したら治った。

self.addEventListener('fetch', event => {
  if (event.request.url.indexOf('firebasestorage.googleapis.com') !== -1) {
    return;
  }
//....

Cannot use wildcard in Access-Control-Allow-Origin when credentials flag is true

FetchEvent.respondWith received an error: TypeError: Cannot use wildcard in Access-Control-Allow-Origin when credentials flag is true.

authorizationヘッダのカンマ区切りによるエラーである。
上記、Basic認証管理下でauthorizationヘッダ追加していると発生する。
Basic認証はauthorization
firebaseはfirebase-authorizationみたいな名前変更をするか、
authorizationのカンマ区切りに対応する。

ReadableStream uploading is not supported

POSTリクエストした時に発生するエラー。

NotSupportedError: ReadableStream uploading is not supported

https://github.com/GoogleChrome/workbox/issues/1732

GETだけにヘッダー追加をすればいい。

回避策

これらのエラーまとめて解決するには、以下を追加する。

self.addEventListener('fetch', event => {
  if (self.location.origin === getOriginFromUrl(event.request.url) &&
    event.request.method === 'GET') {
   // ...
  }
});

自動ログアウト

2つタブ開いてどちらも同じアカウントでログインした状態にしてから、タブAをログアウトさせる。そうするとタブBも自動でログアウトする。
みたいな芸当はServiceWorkerだけではできないので、onAuthStateChangedの実装が必要。

//plugin/firebase.js
if (process.client) {
  auth.onAuthStateChanged(user => {
    if (user) {
      store.commit('setUser', user);
    } else {
      store.dispatch('logout');
    }
  });
}

認証ページで使ってるレイアウトに追加

//layout/auth.vue
computed: {
  ...mapState(['user']),
},
watch: {
  user(val) {
    if (!val || (val && !val.uid)) {
      this.$router.push('/');
    }
  }
}

※pluginやnuxtServerInitでonAuthStateChangedするとメモリリーク発生したので調査中。

auth.onAuthStateChangedによるメモリリーク

/plugin/firebase.jsをSSRでも実行するようにnuxt.config.jsで { ssr: true } 設定している場合、 auth.onAuthStateChanged はserverとclientで2回実行される。そしてメモリリークを起こす。

if (process.client) または if (!process.server) でクライアントだけ実行するように分岐をしておくこと。

注意点

  • PWAはプロダクションモードでしかsw.js生成しない
  • ハードリロードした時はSeviceWorkerが動かない

シェア

  • シェア
  • クリックして Twitter で共有 (新しいウィンドウで開きます)
  • Facebook で共有するにはクリックしてください (新しいウィンドウで開きます)
  • クリックして Skype で共有 (新しいウィンドウで開きます)
  • クリックして Tumblr で共有 (新しいウィンドウで開きます)
  • クリックして Pocket でシェア (新しいウィンドウで開きます)
  • クリックして LinkedIn で共有 (新しいウィンドウで開きます)
  • クリックして Pinterest で共有 (新しいウィンドウで開きます)
  • クリックして WhatsApp で共有 (新しいウィンドウで開きます)
  • クリックして Telegram で共有 (新しいウィンドウで開きます)
  • クリックして友達へメールで送信 (新しいウィンドウで開きます)

関連記事

Firebase、FirebaseAuthentication、login-system、Nuxt、Vue

投稿ナビゲーション

[JS] Firebaseの覚書 ① メールアドレス認証
[Vue.js] ファイルアップロード&バリデーション ( use bootstrap-vue )

コメントを残す コメントをキャンセル

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください。

フォローする
  • Twitter
カテゴリー
  • CSS (63)
    • Compass (3)
    • Sass (1)
  • Custom (8)
  • Dojo (1)
  • GoogleMap (23)
  • HTML&XHTML (14)
    • HTML5 (3)
  • Information (19)
  • Javascript (90)
    • Firebase (8)
    • Vue&Nuxt (15)
  • jQuery (10)
  • Material (9)
  • Memo (84)
  • MooTools (62)
    • Tutorial (6)
  • Perl (1)
  • PHP (37)
    • CakePHP (2)
    • OOPでBBS (7)
    • Zend Framework (4)
  • Template (29)
    • 4BOX (8)
    • 5BOX (5)
    • 6BOX (4)
    • Form (5)
    • Menu (1)
    • Web Site (6)
  • wordpress (69)
    • plugin (9)
    • Reference (7)
    • Themes Making (3)
最近の投稿
  • [Vue] vue-test-utils の Deprecation warning への対応
  • Vercel+Nuxt.jsで爆速Webアプリ作成
  • [JS] Firebaseの覚書 ⑧ 匿名認証の利用
  • Nuxt.jsでTestCafeを使用してみたメモ
  • [Nuxt.js] core-jsエラーへの対処
最近のコメント
  • [HTML] Aタグにおけるrel属性の意味と効果について に aタグでリンクを繋げる際に気をつけたい事 | デザインスタジオドアーズ名古屋 より
  • レスポンシブWebデザインを実装するためのTips に 1行で、画像をレスポンシブにする – たつブロ より
  • [WordPress] カスタムヘッダーをカルーセル表示に変更する に Tenderfeel より
  • [WordPress] カスタムヘッダーをカルーセル表示に変更する に nirdosh_yaqin より
  • IKEAの葉っぱをオフィスの席に設置したメモ に UNIX的工作 – chao情報 より
Tags
1Column 2column Ajax Android Aptana Class CodeSandbox CSS CSS3 Demo Download Dreamweaver Elastic Firebase FirebaseAuthentication Fixed Form free script Fx.Tween Google GoogleMap HTML5 HTML5API IE infowindow iphone JavaScript jQuery MooTools Nuxt OOP PHP Regular Expression rollover Sass Template Vue Web Browser wordpress wp-custom wp-function wp-plugin wp-themes XHTML zeromail
  • RSS - 投稿
  • RSS - コメント
© WebTecNote 2021 • ThemeCountry Powered by WordPress
loading キャンセル
投稿を送信できませんでした。メールアドレスを確認してください。
メール送信チェックに失敗しました。もう一度お試しください。
このブログではメールでの投稿共有はできません。