【Nuxt.js】SSRモードでFirebase Authを使用する

# この記事について
Nuxt.jsのSSR使用時に、Firebase Authを使用してログイン制御を行ってみましたので、実装方法をメモとして残したいと思います。
Nuxt.jsのMiddleware（SSR中の処理）で、Firebaseでログイン済みである/未ログインであるの判定を行う方法を記載したいと思います。
主に [Firebase Auth with SSR - Nuxt Firebase](https://firebase.nuxtjs.org/tutorials/ssr/) を参考に実装しております。
また、Firebase AuthについてはGoogle認証で実装しております。

本記事内でstoreやFirebase Authの基本的な実装を記載しておりますが、細かな説明までは行っておりません。
storeやFirebase Authの基本的な実装に関しては、以下を参照ください。
[Vuex ストア - Nuxt.js](https://develop365.gitlab.io/nuxtjs-2.8.X-doc/ja/guide/vuex-store/)
[JavaScript で Google を使用して認証する - Firebase](https://firebase.google.com/docs/auth/web/google-signin?hl=ja)

# 対象読者
* Nuxt.jsのSSRモードでFirebase Authの実装を行いたい方
* Vue.jsのstoreの実装経験のある方

# 環境
|内容|バージョン|
|---|---|
|node|16.14.2|
|yarn|1.22.15|
|nuxt|2.15.8|
|nuxt-typed-vuex|0.3.0|
|@nuxtjs/firebase|8.2.2|
|@nuxtjs/pwa|3.3.5|
|firebase-admin|10.1.0|

最終的に作成されるディレクトリ構成

```
sampleProject
├middleware
| └router.ts
├pages
| ├index.vue
| ├login.vue
| └logout.vue
├static
| └firebase-auth-sw.js
├store
| ├firebaseAuth.ts
| └index.ts
├types
| └index.d.ts
└nuxt.config.js
```

# 結論

```nuxt.config.js
export default {
  modules: [
    '@nuxtjs/pwa',
    '@nuxtjs/firebase',
  ],
  firebase: {
    config: {
      apiKey: 'yourApiKey',
      authDomain: 'yourAuthDomain',
      projectId: 'yourProjectId',
      storageBucket: 'yourStorageBucket',
      messagingSenderId: 'yourMessagingSenderId',
      appId: 'yourAppId',
      measurementId: 'yourMeasurementId',
    },
    services: {
      auth: {
        ssr: true,
        initialize: {
          onAuthStateChangedAction:
            'firebaseAuth/onAuthStateChangedAction',
        },
      },
    },
  },
  pwa: {
    meta: false,
    icon: false,
    workbox: {
      importScripts: ['./firebase-auth-sw.js'],
      dev: process.env.NODE_ENV === 'development',
    },
  },
};
```

```store/index.ts
import {
  getAccessorType,
  getterTree,
  mutationTree,
  actionTree,
} from 'typed-vuex';
import * as firebaseAuth from './firebaseAuth';

export const state = () => ({});
export const getters = getterTree(state, {});
export const mutations = mutationTree(state, {});
export const actions = actionTree(
  { state, getters, mutations },
  {
    async nuxtServerInit({ dispatch }, { res }) {
      if (res && res.locals && res.locals.user) {
        const authUser = res.locals.user;
        await dispatch('firebaseAuth/onAuthStateChangedAction', {
          authUser,
        });
      }
    },
  },
);
export const accessorType = getAccessorType({
  state,
  getters,
  mutations,
  actions,
  modules: {
    firebaseAuth,
  },
});
```

# Nuxt.jsでFirebase Authを使用して単純なログイン・ログアウト処理を実装してみる
まずは、シンプルにFirebase AuthのGoogle認証を使用して、ログイン・ログアウトを実装します。

まずは必要なライブラリのインストール

```
yarn add @nuxtjs/firebase
```

Firebase Authを使用できるように `nuxt.config.js` に以下を追加
「Firebaseプロジェクトは作成済み」かつ「Google認証のAuthenticationは有効化済み」とします。
Firebaseプロジェクトが未作成の場合は、[こちら](https://jeannie-blog.online/create-firebase-project/)を参考に作成して下さい。
`apiKey` ～ `measurementId` には作成したプロジェクトから取得したものをそれぞれ指定してください。

```nuxt.config.js
export default {
  modules: [
    '@nuxtjs/firebase',
  ],
  firebase: {
    config: {
      apiKey: 'yourApiKey',
      authDomain: 'yourAuthDomain',
      projectId: 'yourProjectId',
      storageBucket: 'yourStorageBucket',
      messagingSenderId: 'yourMessagingSenderId',
      appId: 'yourAppId',
      measurementId: 'yourMeasurementId',
    },
    services: {
      auth: true,
    },
  },
};
```

```pages/index.vue
<template>
  <div>
    <button @click="onClickedLoginButton">ログイン</button>
    <button @click="onClickedLogoutButton">ログアウト</button>
  </div>
</template>

<script lang="ts">
import Vue from 'vue';

export default Vue.extend({
  methods: {
    async onClickedLoginButton() {
      const provider = new this.$fireModule.auth.GoogleAuthProvider();
      await this.$fire.auth
        .signInWithPopup(provider)
        .then((userResult) => console.log(userResult))
        .catch((error) => console.error(error));
    },
    async onClickedLogoutButton() {
      await this.$fire.auth.signOut();
    },
  },
});
</script>
```

これで画面上でGoogleアカウントを使用してログイン・ログアウトが可能となります。
しかし、通常は複数ページにまたがってログイン状態を共有したいと思いますので、ログインユーザのユーザIDをstoreで管理して「storeにログインユーザのユーザIDが存在する場合はログイン済み」と判定できるように実装したいと思います。

# storeでログインユーザのユーザIDを管理する
Firebase AuthでログインしたユーザのユーザIDを管理するstoreを作成します。
Firebase Auth用の個別storeを管理する `firebaseAuth.ts` と 他のstoreも含めて管理する `index.ts` に分けております。
また、[onAuthStateChangedAction](https://firebase.nuxtjs.org/service-options/auth#onauthstatechangedaction)を使用して画面初期表示時やログイン状態変更時のイベントキャッチが可能であるため、 `onAuthStateChangedAction` をstore内に実装しております。

```store/firebaseAuth.ts
import { getterTree, mutationTree, actionTree } from 'typed-vuex';

export const state = (): firebaseUserInfo => ({
  userId: null as string | null,
});
export type RootState = ReturnType<typeof state>;

export const getters = getterTree(state, {
  getUserId(state): string {
    return state.userId;
  },
});

export const mutations = mutationTree(state, {
  setUserId(state, userId: string) {
    state.userId = userId;
  },
});

export const actions = actionTree(
  { state, mutations },
  {
    async onAuthStateChangedAction({ commit }, { authUser }) {
      if (authUser === null) {
        commit('setUserId', null);
        return;
      }

      commit('setUserId', authUser.uid);
    },
  },
);
```

```store/index.ts
import {
  getAccessorType,
  getterTree,
  mutationTree,
  actionTree,
} from 'typed-vuex';
// その他のstoreがある場合はここで他のstoreをimportする
import * as firebaseAuth from './firebaseAuth';

export const state = () => ({});
export const getters = getterTree(state, {});
export const mutations = mutationTree(state, {});
export const actions = actionTree({ state, getters, mutations }, {});
export const accessorType = getAccessorType({
  state,
  getters,
  mutations,
  actions,
  modules: {
    // その他のstoreがある場合はmodulesに他のstoreを追加する
    firebaseAuth,
  },
});
```

`onAuthStateChangedAction` でログイン状態変更時のイベントキャッチを可能とするために、 `nuxt.config.js` に記述を追加します。
`onAuthStateChangedAction` 起動時に `firebaseAuth` storeの `onAuthStateChangedAction` アクションを呼び出すための記述となります。

```nuxt.config.js
export default {
  modules: [
    '@nuxtjs/firebase',
  ],
  firebase: {
    config: {
      apiKey: 'yourApiKey',
      authDomain: 'yourAuthDomain',
      projectId: 'yourProjectId',
      storageBucket: 'yourStorageBucket',
      messagingSenderId: 'yourMessagingSenderId',
      appId: 'yourAppId',
      measurementId: 'yourMeasurementId',
    },
    services: {
      auth: {
        // 以下追加
        initialize: {
          onAuthStateChangedAction:
            'firebaseAuth/onAuthStateChangedAction',
        },
      },
    },
  },
};
```

これで、 `pages/index.ts` の画面からログインするとstoreにユーザIDがセットされ、ログアウトするとstoreのユーザIDにnullがセットされるようになります。

# Firebase Auth & store のみの場合の課題
ここで、以下のような要件があった場合、 `middleware/router.ts` を作成して実装を試みてみます。
* ログイン時...`pages/login.vue` を表示
* 未ログイン時...`pages/logout.vue` を表示

```middleware/router.ts
import { Context, Middleware } from '@nuxt/types';

const middlware: Middleware = ({ redirect, $accessor, $fire }: Context) => {
  // 動作確認用で、storeと$fireの両方のユーザIDをlogに出力してみる
  const firebaseLoginUserId = $accessor.firebaseAuth.getUserId;
  console.log(firebaseLoginUserId);
  console.log($fire.auth.currentUser?.uid);
  if (firebaseLoginUserId === null) {
    // ログインしていない場合はpages/logout.vueへ遷移
    redirect('/logout');
  } else {
    // ログインしている場合はpages/login.vueへ遷移
    redirect('/login');
  }
};

export default middlware;
```

middlewareの処理内でstoreにアクセスできるように、 `$accessor` を型追加しておきます。

```types/index.d.ts
import { accessorType } from '~~/store';

declare module '@nuxt/types' {
  interface NuxtAppOptions {
    $accessor: typeof accessorType;
  }
  interface Context {
    $accessor: typeof accessorType;
  }
}
```

ここで、Firebaseでログイン後にブラウザの画面リロードしてみます。
すると、ログインしているにもかかわらず `console.log(firebaseLoginUserId);` と `console.log($fire.auth.currentUser?.uid);` の両方で `null` となってしまいます。
これは以下が原因によるものです。
* storeに入れたデータは、画面リフレッシュ時にはリセットされてしまう
* FirebaseはデフォルトではSSR側の処理内でログイン状態を取得できない
* `onAuthStateChangedAction` が実行されるのは画面表示後であるため、SSR時のmiddlewareではまだ実行されず、storeのユーザIDはnullとなる

# Firebase Auth, PWA, nuxtServerInit & store（課題の解決）
そこで、[こちら](https://firebase.nuxtjs.org/tutorials/ssr/)のリファレンスを見ると、SSRでFirebaseでログインしたユーザのログイン状態を取得できるようなので、これに従って実装していきたいと思います。

必要なライブラリのインストール
内部的にサーバサイドでのFirebase Authを使用するため、`firebase-admin` をインストールします。

```
yarn add firebase-admin @nuxtjs/pwa
```

また、Firebase AuthのSSRを有効化して、インストールした `@nuxtjs/pwa` を適用するため `nuxt.config.js` を以下のように書き換えます。
`importScripts` に記載した `firebase-auth-sw.js` のファイルはサーバ起動時に自動で作成されます。

```nuxt.config.js
export default {
  modules: [
    '@nuxtjs/pwa', // 追加
    '@nuxtjs/firebase',
  ],
  firebase: {
    config: {
      apiKey: 'yourApiKey',
      authDomain: 'yourAuthDomain',
      projectId: 'yourProjectId',
      storageBucket: 'yourStorageBucket',
      messagingSenderId: 'yourMessagingSenderId',
      appId: 'yourAppId',
      measurementId: 'yourMeasurementId',
    },
    services: {
      auth: {
        ssr: true, // 追加
        initialize: {
          onAuthStateChangedAction:
            'firebaseAuth/onAuthStateChangedAction',
        },
      },
    },
  },
  // 以下追加
  pwa: {
    meta: false,
    icon: false,
    workbox: {
      importScripts: ['./firebase-auth-sw.js'],
      dev: process.env.NODE_ENV === 'development',
    },
  },
};
```

そして、[Nuxt.js使用時に一番最初に実行される `nuxtServerInit` ](https://nuxtjs.org/docs/concepts/nuxt-lifecycle/)でFirebaseのログインユーザの情報を取得することができます。
nuxtServerInitの使い方については[こちら](https://nuxtjs.org/docs/directory-structure/store/#the-nuxtserverinit-action)を参照してください。
nuxtServerInitは `store/index.ts` にのみ記載可能です。
（画面ロード時に1度だけ呼び出される処理です。）

```store/index.ts
import {
  getAccessorType,
  getterTree,
  mutationTree,
  actionTree,
} from 'typed-vuex';
import * as firebaseAuth from './firebaseAuth';

export const state = () => ({});
export const getters = getterTree(state, {});
export const mutations = mutationTree(state, {});
export const actions = actionTree(
  { state, getters, mutations },
  // nuxtServerInitを追加
  {
    async nuxtServerInit({ dispatch }, { res }) {
      if (res && res.locals && res.locals.user) {
        const authUser = res.locals.user;
        await dispatch('firebaseAuth/onAuthStateChangedAction', {
          authUser,
        });
      }
    },
  },
);
export const accessorType = getAccessorType({
  state,
  getters,
  mutations,
  actions,
  modules: {
    firebaseAuth,
  },
});
```

こうすることで、以下の流れで処理が実行されてmiddlewareでログインユーザのユーザIDを取得することができます。
1. `nuxtServerInit` が実行される。
1. ログイン状態であれば、`nuxtServerInit` 内の以下の記述によってログインユーザの情報を取得する。

    ```ts
    const authUser = res.locals.user;
    await dispatch('firebaseAuth/onAuthStateChangedAction', {
      authUser,
    });
    ```
1. `firebaseAuth` storeの `onAuthStateChangedAction` が呼び出されて `useId` stateにログインユーザのユーザIDがセットされる。
1. middlwareの処理が実行されてmiddleware内で `$accessor.firebaseAuth.getUserId` によってstoreからユーザIDを取得することが可能（ログイン後に画面リロードを行うと `pages/login.vue` が表示される）。

また、今回はmiddleware内でのユーザID取得を試みましたが、asyncDataやfetchなどSSRにかかわるすべての処理の中でも同様にログインユーザの情報が取得できるようになります。
（Firebase AuthのSSRの設定などを行わない場合は、asyncDataやfetchでもmiddlware同様にログインユーザの乗法が取得できなくなります。）
（また今回、Nginxを使用して `test.localhost` のようなサブドメインを使用した際にもSSR時にFirebaseのログインユーザの情報が取得できなくなりました。こちらの対処法などご存じの方いましたらご教示お願いします。）

# 参考
[Firebase Auth with SSR - Nuxt Firebase](https://firebase.nuxtjs.org/tutorials/ssr/)