Nuxt.js + Firebase Authentication を用いた認証まとめ

📆April 06, 2020

Nuxt.js で Firebase Authentication を使用した Web アプリケーションの構築を行ったので、その際の認証周りの設計をまとめておきます。

基本方針

認証状態を持つのは Firebase のみとし、Nuxt.js 側では認証状態を持たないようにしました。つまり、Store を用いたログイン状態の管理等は行わないということです。これは、認証状態の管理コストが二重になるのを避けるためです。認証状態の同期(commit)や、不整合起きた場合のハンドリングなどが煩雑にならないようにこのような設計方針としました。

ログイン状態を監視するオブザーバーの設置

firebase.auth().onAuthStateChanged(user => {}) でログインされているユーザーを取得することができます。

ログアウト状態になった場合も、このオブザーバーでハンドリングを行います。オブザーバーのラッパー関数を作成して、ログイン時に、オブザーバーの登録を行います。

/* plugins/Firebase.ts */

/*
  オブザーバーのラッパー関数
  ログインしたときと、ログアウトしたときの処理を、呼び出し元で定義する。
*/
export const registerFirebaseAuthStateObserver = (signinCallback: Function, signoutCallback: Function) => {
  auth.onAuthStateChanged(user => {
    if (user) {
      // ログインされたとき
      console.log('=== SIGNIN');
      signinCallback(user);
    } else {
      // ログアウトされたとき
      console.log('=== SIGNOUT');
      signoutCallback();
    }
  });
};
/* plugins/Firebase.ts */

/**
 * アプリケーション起動時に、Firebase オブザーバーを登録し、
 * ログイン状態を監視する
 * セッションが切れたら、自動でログイン画面に replace される
 */
export default ({ app }: any) => { // any は Context という型だけど今回は割愛
  const signinCallback = () => {
    console.log('=== Signin');
  };

  const signoutCallback = () => {
    console.log('=== Signout');
    app.router.replace('/signin');
  };

  registerFirebaseAuthStateObserver(signinCallback, signoutCallback);
};

このようにオブザーバーを登録しておくことで、Firebase のログアウトメソッド firebase.auth().signOut() を呼び出して認証状態がログアウト状態に変化したときも、onAuthStateChanged が発火し、共通的に処理を行わせることができます。

onAuthStateChange の Promise ラッパーの使用

Firebase Authentication でログインしているユーザー情報を取得する方法は2種類あります。先に書いた firebase.auth().onAuthStateChanged(user => {}) オブザーバーを使う方法と、firebase.auth().currentUser を使う方法です。後者は、タイミングによって、 Authオブジェクトが初期化されていない場合があり、公式ドキュメントでは、前者のオブザーバーを使ってユーザーを取得する方法が推奨されています。

firebase.auth().currentUserc だと、 currentUsercZ が null になってしまう場合があります。📙Firebase 公式ドキュメントahttps://firebase.google.com/docs/auth/web/manage-users?hl=ja#get_the_currently_signed-in_user

とはいうものの、 user 情報は Store では管理しない設計方針なので、使いたいタイミングで、 user オブジェクトをうまく取得してくる必要があります。 user オブジェクトはコールバック引数として渡ってくるので、少し扱いづらい。そこで登場するのが、 Promise ラッパーです。

/* plugins/Firebase.ts */

// ユーザーを取得する
// firebase.auth の currentUser を確実に取得するための Promise ラッパー
export function getCurrentUser(): Promise<any> {
  return new Promise((resolve) => {
    const unsubscribe = auth.onAuthStateChanged((user) => {
      resolve(user);
      unsubscribe();
    });
  });
}

👆のコードのように、Promise ラッパーを準備してやることで、呼び出しもとでは async/await

で呼び出すことができるようになります。また、何度も述べているように、 onAuthStateChanged はオブザーバーなので、呼び出されるたびに購読を解除します(でないとオブザーバーが多重登録されていくことになってしまいます。)

Middleware の設置

認証が必要なページにリクエストが来た場合に、認証されていない状態の場合はログインページに飛ばす、などはよくあるユースケースだと思います。このように、ページがレンダリングされる前に判定処理を加えるのには Middleware が便利です。 📙 Nuxt 公式ドキュメント

ここは好みが分かれるところですが、僕は Nuxt の Layout 層に無名 Middleware を仕込むのが好みです。Layout 層に、認証が必要でないページと、認証が必要なページで分けてファイルを作り、それぞれの Page 層でどちらのレイアウトを使うかを選択する方式を取っています。

認証が必要なページの方が多いアプリケーションの場合は、 layout/default.vue のデフォルトページの方を認証必須ページにしています(世の中の Web アプリケーションはだいたいこうなる気がする)。

/* layout/default.vue */

async middleware({ redirect }) {
    console.log('=== middleware');
    const currentUser = await getCurrentUser();

    if (!currentUser) {
      redirect('/signin');
    }
  }
  1. さきほど作成した、Promise ラッパーで認証状態を取得、
  2. 認証されていればそのままレンダリング
  3. されていなかった場合は、引数で受け取ったコンテキストを用いてリダイレクト

という処理のフローにしています。

API リクエスト時の処理の流れ

さいごに。API サーバへのリクエスト時にヘッダーに認証情報を詰める際にも、先述の Promise ラッパーを使用します。便利ですね。

async getIdToken(): Promise<string> {
    console.log('=== getIdToken');

    const currentUser = await getCurrentUser();

    if (!currentUser) {
      throw new Error('uninitialized currentUser');
    }

    const res = await currentUser.getIdToken().catch((e: any) => {
      throw e;
    });
    return res;
  }

以上の👆の流れを図にすると、以下のようになります。

同じアーキテクチャで web アプリケーションを構築する方は参考にしてみてください。



🔖 FirebaseNuxtJS