Nuxt + Firebase Authentication

最近、個人でFirebaseをバックエンドとしたNuxtによるWebアプリケーションの開発をしています。その中で、Firebaseの機能の1つであるFirebase Authenticationを使用してユーザーの認証機能を実装してみました。この記事では、NuxtアプリでFirebase Authenticationによるユーザー認証機能を実装する方法について書いてみたいと思います。

はじめに

ここで検討するアプリケーションは、次の仕様を前提としています。

  1. Googleアカウントで利用可能である。
  2. 次のページを持つ。
  • ルートページ (/)
  • ログインページ (/login)
  1. 認証されていないユーザーがルートページ(/)にアクセスした場合、ログインページ(/login)にリダイレクトする。
  2. 認証済みのユーザーがログインページ(/login)にアクセスした場合、ルートページ(/)にリダイレクトする。

Firebaseプロジェクトの準備

ここでは詳細は割愛しますが、Firebaseにアカウントを作成し、プロジェクトを作成する必要があります。

プロジェクトを作成すると、

  • apiKey
  • authDomain
  • databaseURL
  • projectId
  • storageBucket
  • messagingSenderId

が払い出されます。 これらの値はNuxtアプリケーションからFirebaseへ接続する際に使用します。

Nuxtアプリケーションの生成

Nuxtアプリケーションのベースは、vue-cliを使用して作成します。

vue-cliがインストールされていない場合は、インストールします。

次のコマンドで、アプリケーションのベースを作成し、依存するnpmパッケージ群をインストールします。

$ vue init nuxt-community/starter-template <project-name>
$ cd <project-name>
$ npm install

そして、

$ npm run dev

でアプリケーションを起動し、正常に動作することを確認します。

Firebaseとの接続

firebaseパッケージのインストール

アプリケーションからFirebaseを利用するために、firebaseパッケージをインストールします。

$ npm install firebase --save

firebaseプラグイン

NuxtアプリケーションからFirebaseに接続するためのモジュールを作成します。

pluginsディレクトリにfirebase.jsというファイルを作成し、次のような実装とします。

import firebase from 'firebase'

if (!firebase.apps.length) {
  firebase.initializeApp({
    apiKey: <APIKEY>,
    authDomain: <AUTHDOMAIN>,
    databaseURL: <DATABASEURL>,
    projectId: <PROJECTID>,
    storageBucket: <STORAGEBUCKET>,
    messagingSenderId: <MESSAGINGSENDERID>
  })
}

export default firebase

ここで、Firebaseの各種情報を管理するのに、direnvを利用すると便利です。 direnvの導入方法は割愛しますが、direnvを使用する場合は、プロジェクトに対して、

  • .envrcを作成する。
  • nuxt.config.jsで環境変数を読み込むよう設定する。
  • firebaseモジュールの初期化を環境変数を用いて行う。

という変更を行います。

.envrcの作成

次の内容を.envrcに書きます。

export APIKEY=<APIKEY>
export AUTHDOMAIN=<AUTHDOMAIN>
export DATABASEURL=<DATABASEURL>
export PROJECTID=<PROJECTID>
export STORAGEBUCKET=<STORAGEBUCKET>
export MESSAGINGSENDERID=<MESSAGINGSENDERID>

<>の中身は、Firebaseプロジェクトで払い出された値を指定します。

nuxt.config.jsの変更

const webpack = require('webpack')

module.exports = {
  // ...
  build: {
    extend (config, { isDev, isClient }) {
      // ...
      config.plugins.push(
        new webpack.EnvironmentPlugin([
          'APIKEY',
          'AUTHDOMAIN',
          'DATABASEURL',
          'PROJECTID',
          'STORAGEBUCKET',
          'MESSAGINGSENDERID'
        ])
      )
    }
  }
}

firebase.jsの変更

import firebase from 'firebase'

if (!firebase.apps.length) {
  firebase.initializeApp({
    apiKey: process.env.APIKEY,
    authDomain: process.env.AUTHDOMAIN,
    databaseURL: process.env.DATABASEURL,
    projectId: process.env.PROJECTID,
    storageBucket: process.env.STORAGEBUCKET,
    messagingSenderId: process.env.MESSAGINGSENDERID
  })
}

export default firebase

Storeの実装

アプリケーション全体で、ユーザーの認証状態を管理するために、Vuexによる状態管理を実装します。

state

stateとして持っておきたいのが、認証されたユーザー情報です。

したがって、次のように実装します。

export const state = () => ({
  user: null
})

getters, actions, mutations

gettersactionsmutationsは、以降必要応じて随時足していきます。

Pageの実装

具体的な認証機能は後で実装することにして、まずはページコンポーネントを定義してみます。

ファイルは、

  • index.vue (ルートページコンポーネント)
  • login.vue (ログインページコンポーネント)

として、pagesディレクトリに格納します。

今回は、テンプレートエンジンにpugを、cssメタ言語にscssを使用することにして、 次のパッケージ群をインストールします。

$ npm install --save-dev pug pug-loader node-sass sass-loader

この段階では、それぞれ、次の実装としておきます。

index.vue

<template lang="pug">
.container
  h1 index
</template>

<script>
export default {
}
</script>

<style lang="scss" scoped>
</style>

login.vue

<template lang="pug">
.container
  h1 login
</template>

<script>
export default {
}
</script>

<style lang="scss" scoped>
</style>

Middlewareの実装

次に、冒頭で書いた、

  • 認証されていないユーザーがルートページ(/)にアクセスした場合、ログインページ(/login)にリダイレクトする。
  • 認証済みのユーザーがログインページ(/login)にアクセスした場合、ルートページ(/)にリダイレクトする。

を実現する方法を考えてみます。これらは、ページ遷移の際にチェックするのが良さそうです。幸い、Nuxt.jsは、 Middlewareという機構を提供しており、

ミドルウェアを使って、あるページまたはあるページのグループがレンダリングされる前に実行される関数を定義することができます。

とあるので、今回の仕様を実現するにはうってつけの機能です。

今回実装するMiddlewareでは、

  • Storeにユーザー情報が格納されている場合は、認証済みと判定する。
  • Storeにユーザー情報が格納されていない場合は、認証されていないと判定する。

というロジックが必要になるため、Storeに次のgetterを追加しておきます。

export const getters = {
  isAuthenticated (state) {
    return !!state.user
  }
}

上記のgetterを使用して、次のMiddlewareを定義します。 (authenticated.jsというファイル名でmiddlewareディレクトリに格納します。)

export default function ({ store, route, redirect }) {
  if (!store.getters.isAuthenticated && route.name !== 'login') {
    redirect('/login')
  }
  if (store.getters.isAuthenticated && route.name === 'login') {
    redirect('/')
  }
}

そして、nuxt.config.jsを編集し、作成したmiddlewareを登録します。

module.exports = {
  // ...
  router: {
    middleware: 'authenticated'
  }
}

この実装によって、ルートページにアクセスすると、(ログイン済みのユーザー情報がないため、)必ずログインページにリダイレクトされるようになります。

ここからは、いよいよFirebase Authenticationを使用して、ログイン機能を作っていきたいと思います。

ログイン機能

Googleアカウントでの認証機能を実装するにあたり、次のAPIを使用します。

ログイン機能のフローは、次のようになります。

  1. ユーザーはログインページでログインボタンをクリックする。
  2. アプリケーションはsignInWithRedirectを実行する。
  3. Googleの認証画面に遷移する。
  4. ユーザーはGoogleの認証画面で認証情報を入力する。
  5. アプリケーションのページにリダイレクトされる。
  6. onAuthStateChangedで認証情報を取得し、ルートページに遷移する。

loginページコンポーネントにログインボタンを配置し、クリックイベントハンドラーを実装していきます。 クリックイベントハンドラーでは、Vuexのactionを実行するようにします。ということで、storeモジュールに次のactionを追加します。

import firebase from '@/plugins/firebase'

const googleProvider = new firebase.auth.GoogleAuthProvider()

export const actions = {
  login () {
    return new Promise((resolve, reject) => {
      firebase.auth().signInWithRedirect(googleProvider)
        .then(() => resolve())
        .catch((err) => reject(err))
    })
  }
}

loginページコンポーネントにログインボタンを追加し、クリック時に上記のactionを実行するよう修正します。

<template lang="pug">
.container
  h1 login
  input(type='button'
        value='Login in with Google'
        @click='doLogin')
</template>

<script>
import { mapActions } from 'vuex'

export default {
  methods: {
    ...mapActions([
      'login'
    ]),

    doLogin () {
      this.login()
        .then(() => console.log('resloved'))
        .catch((err) => console.log(err))
    }
  }
}
</script>

<style lang="scss" scoped>
</style>

この実装により、ログインボタンをクリックすると、Googleが提供する認証ページに遷移するようになり、 Googleアカウントの認証情報を入力すると、再びアプリケーション側にリダイレクトされます。

ここで、アプリケーション側にリダイレクトされた際に、上述のonAuthStateChangedメソッドを実行することで、 認証済みのユーザー情報が取得できるようになり、取得したユーザー情報をStoreに格納することで、 認証済みのユーザーをアプリケーションで管理できます。

認証済みのユーザーをStoreに格納するために、次のmutationとactionを追加しておきます。

export const mutations = {
  setUser (state, payload) {
    state.user = payload
  }
}

export const actions = {
  // ...
  setUser ({ commit }, payload) {
    commit('setUser', payload)
  }
}

onAuthStateChangedを呼び出すにあたり、ちょっとした工夫が必要です。

Googleの認証ページからリダイレクトされるページは、アプリケーションのルートページ(/)になります。 ですが、リダイレクトされた時点では、まだ認証済みユーザー情報がStoreに格納できていないため、 先ほど追加したMiddleware(authenticated.js)により、ログインページ(/login)にリダイレクトします。 そのため、onAuthStateChangedを実行するのはloginページコンポーネントとする必要があります。

また、loginページコンポーネントでonAuthStateChangedを実行し、コールバックが実行されるのを待つには、 コンポーネントのmountedフックで非同期処理として実行すれば良さそうです。 (この方法は、https://github.com/potato4d/nuxt-firebase-sns-example)を参考にさせて頂きました。)

async mounted () {
  let user = await new Promise((resolve, reject) => {
    firebase.auth().onAuthStateChanged((user) => resolve(user))
  })
  this.setUser(user) // setUser is mapped action from vuex
  if (user) {
    this.$router.push('/') // if non-null user given, go to root page.
  }
}

上記のようにすることで、loginページコンポーネントがマウントされた時点で、

  • onAuthStateChangedの完了を確実に待って、
    • 認証済みユーザー情報があれば、ルートページに遷移する。
    • 認証済みユーザー情報がなければ、ログインページをそのまま表示する。

とすることができました。ですが、まだ少しだけ気持ち悪いことがおきます。

ユーザーが認証済みの状態でルートページにアクセスすると、

  • ルートページにアクセスする。
  • Storeに認証済みユーザー情報がないため、ログインページにリダイレクトする。
  • ログインページのmountedフックが動き、onAuthStateChangedが実行される。
  • 認証済みユーザーが取得でき、ルートページに遷移する。

となり、一瞬ログインページが表示されてしまいます。

これを対処するための方法としては、mountedフックが完了するまでの間、 ローディング画面を表示する、という方法が考えられます。

ログアウト機能

ログアウト機能を実装するにあたり、次のAPIを使用します。

ルートページコンポーネントにログアウトボタンを配置し、クリックイベントハンドラーを実装していきます。 クリックイベントハンドラーでは、Vuexのactionを実行するようにします。次のactionを追加します。

export const actions = {
  // ...
  logout ({ commit }) {
    return new Promise((resolve, reject) => {
      firebase.auth().signOut()
        .then(() => {
          commit('setUser', null)
          resolve()
        })
    })
  }
  // ...
}

ルートページコンポーネントにログアウトボタンを追加し、クリック時に上記のactionを実行するよう修正します。

<template lang="pug">
.container
  h1 index
  input(type='button'
        value='Logout'
        @click='doLogout')
</template>

<script>
import { mapActions } from 'vuex'

export default {
  methods: {
    ...mapActions([
      'logout'
    ]),

    doLogout () {
      this.logout()
        .then(() => {
          this.$router.push('/login')
        })
        .catch((err) => console.log(err))
    }
  }
}
</script>

<style lang="scss" scoped>
</style>

これで、ログアウトボタンをクリックすることで、FirebaseのsignOut処理を経由して、 再びログインページに遷移するようになります。

おわりに

NuxtアプリでFirebase Authenticationによるユーザー認証機能を実装する方法について纏めてみました。

誰かの参考になれば何よりです...

コードは、shimar/nuxt-firebase-auth-exampleで公開しています。

2020-12-29追記

Next.jsでも似たようなことをやってみました。 Next.js + Firebase Authentication

参考サイト