Firebase Authentication + NuxtJSでGoogle認証を実装してみた

Table of Contents

Table of Contents

NuxtJSにFirebase認証を導入するのは簡単にできると思ったが、意外にはまりました。 今回は静的なサイトにGoogle認証の機能を実装しまして、その過程にメモした方が良いと思う部分があったので、記事にしました。 今後、同じ構成で認証機能を作成したい時に、参考になればと思います。



Contents

  • 成果物
  • 実装
    • 1.準備作業
    • 2.NuxtJSにFirebaseを設定する
    • 3.login画面
    • 4.profile画面
  • 注意点
    • 1.client-onlyの記述
  • 最後


成果物

NuxtJSで作成した静的サイトにGoogle認証の機能を実装した。 認証機能について、Firebase Authenticationを利用する。

サイトには三つの画面があります。

  1. login/top
  2. profile
  3. index (デフォルトのままでOK)

1はGoogleログインとログアウト機能を持ち、未ログインの表示とログイン完了表示も含めている画面です。 ログインは リダイレクト式 で、ログインを成功したら、profile画面へのリンクを表示します。 また、ログアウトの実行も可能です。

2はログインした状態しかアクセスできない画面です。 また、ログアウトの実行も可能です。

ソースコード: https://github.com/scobin/firebase-auth

result



実装


1. 準備作業

  • Firebaseプロジェクトの作成
  • Firebaseの設定情報(apiKey, appIdなど)
  • NuxtJSプロジェクトの作成
  • 利用するライブラリの導入

Firebaseプロジェクトの作成Firebaseの設定情報 について、公式サイトには詳細を記載しているなので、ここでは省略しておきます。

NuxtJSプロジェクトについて、このような構成になります。

firebase-auth
  |--.nuxt
  |--components
  |--helpers
    |-- cookies.js
    |-- fireinit.js
  |--layouts
  |--middleware
  |--node_modules
  |--pages
    |-- index.vue
    |-- login.vue
    |-- profile.vue
  |--plugins
    |-- fireauth.js
  |--static
  |--nuxt.config.js
  |--package.json

ライブラリについては、二部分があります。

  1. Firebase:
npm i firebase
  1. Cookie: (認証情報を保持するため)
npm i --save cookieparser
npm i --save js-cookie
npm i --save jwt-decode


2. NuxtJSにFirebaseを設定する

まず、Firebaseプロジェクトの設定ファイルを作成する。 一旦NuxtJSのプロジェクトのルートにhelpsフォルダを作成し、その中に設定ファイルを置く。 本例では、ファイル名をfireinit.jsにしている。

import * as firebase from 'firebase/app'
import 'firebase/auth'
import 'firebase/firestore'
import 'firebase/database'

var firebaseConfig = {
    apiKey: "dfjakldfljaldjldlsGmg",
    authDomain: "xxxxxx.firebaseapp.com",
    databaseURL: "https://xxxxxx.firebaseio.com",
    projectId: "xxxxx",
    appId: "fkaklsdjfklalskldflkajslkjl",
}

if (!firebase.apps.length) {
    console.log('"%cfirebase.initializeApp', 'color: red')
    firebase.initializeApp(firebaseConfig)
}
export const GoogleProvider = new firebase.auth.GoogleAuthProvider()
export const auth = firebase.auth()
export const DB = firebase.database()
export const StoreDB = firebase.firestore()
export default firebase

firebaseConfigの部分をFirebaseプロジェクトからもらったものに置き換えてください。


次に、NuxtJSプロジェクトのpluginsの配下にfireauth.jsを作成する。 認証状態の変更がある際に、相応の処理を実行するのは目的です。

import { auth } from '~/helpers/fireinit.js'
import Cookies from 'js-cookie'

export default context => {
    const { store } = context

    return new Promise((resolve, reject) => {
        auth.onAuthStateChanged(user => {
            console.log("auth's state changed!", user)
            if (user) {
                auth.currentUser.getIdToken(true)
                    .then(token => {
                        Cookies.set('access_token', token)
                    })
            } else {
                Cookies.remove('access_token')
            }
            return resolve()
        })
    })
}

ログイン認証成功した時に、Cookieに認証情報を記録して置いたため、次回にサイトにアクセスすると認証なく自動的にログイン状態になる。

ログイン認証解除(ログアウト)したら、Cookieにある認証情報を削除する。

上記のfireauth.jsを作成できたら、nuxt.config.jsのpluginに追加する。

plugins: [
  '~/plugins/fireauth.js',
  // その他...
],


3. login画面

pagesの配下にlogin.vueを作成して、下記のコードを記入する。

<template>
  <client-only>
    <div v-if="!isLogin" class="login">
      <h2 class="title">Sign In with Google</h2>
      <button @click="googleSignUp">Google Sign In</button>
    </div>
    <div v-else>
      <div class="header">
        <nuxt-link to="/profile">My profile</nuxt-link>
        <div class="logout-link" @click="signOut">Logout</div>
      </div>
      <div class="main">
        <h1>This is a private page!</h1>
      </div>
    </div>
  </client-only>
</template>

<script>
import { auth, GoogleProvider } from "~/helpers/fireinit.js";
import { getUserFromCookie } from '@/helpers/cookies.js' 
export default {
  asyncData({ req, redirect }) {
    console.log('asyncData')
    let isLogin = false
    if (process.server) {
      console.log("login", "process.server")
      const user = getUserFromCookie(req)
      if (user) {
        isLogin = true
      }
    } else {
      console.log("login", "process.client")
      var user = auth.currentUser
      if (user) {
        isLogin = true
      }
    }
    return {isLogin: isLogin}
  },
  mounted() {
    let that = this
    auth.getRedirectResult().then(function (result) {
        console.log('Redirect result', result)
        if (result.credential) {
            // This gives you a Google Access Token. You can use it to access the Google API.
            var token = result.credential.accessToken;
            
            that.isLogin = true
        }
        // The signed-in user info.
        // var user = result.user;
    }).catch(function (error) {
        // Handle Errors here.
        console.log(error.message)
    });
  },
  methods: {
    googleSignUp: function() {
      console.log("sign up")
      auth.signInWithRedirect(GoogleProvider)
    },
    signOut: function() {
      console.log('sign out')
      auth.signOut().then(() => {
        this.isLogin = null
      }).catch(error => console.log(error))
    }
  }
}
</script>


<style>
.login {
  margin-top: 50px;
  display: flex;
  width: 100%;
  justify-content: center;
  align-items: center;
  flex-direction: column;
}
.logout-link {
  cursor: pointer;
  text-decoration: underline;
  color: #551a8b;
}
.header {
  display: flex;
  justify-content: space-around;
  margin: 10px 20px;
  width: 30%;
}
.main {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
}
</style>

以下、実装の要点を説明します。

  • 未ログインとログインの表示を制御するため、isLoginという変数を使います。

  • ログインとログアウトを実行するため、googleSignUpsignOutの関数を作成します。

  • リダイレクト式の認証なので、mounted関数にリダイレクトの結果を検知する。

上記の実装ができれば、認証はできるけど、画面を一旦離れて戻ったり、リロードしたりすると未ログインの画面が表示されてしまいます。 これは保持している認証の状態を使っていないからだと思います。

  • この問題を解決するため、asyncDataにはCookieから認証情報やFirebaseライブラリから認証情報を確認します。

その中身には、process.serverでサーバ側とクライエント側を分けて処理します。

例えば、サーバー側の処理:Httpリクエストをして、サイトにアクセスする時、リクエストのCookieで保持している認証情報を取得します。 クライエント側の処理:ブラウザのBack/forward機能で画面遷移する場合、Firebaseライブラリ経由で認証情報を確認します。

  • Cookieに関連する処理を共通化にするため、下記のようにもう一つのファイルcookies.jsを作成して、helpers配下に追加する。
import jwtDecode from 'jwt-decode'
var cookieparser = require('cookieparser')

export function getUserFromCookie(req) {
    if (process.server && process.static) return
    if (!req.headers.cookie) return

    if (req.headers.cookie) {
        const parsed = cookieparser.parse(req.headers.cookie)
        const accessTokenCookie = parsed.access_token
        if (!accessTokenCookie) return

        const decodedToken = jwtDecode(accessTokenCookie)
        if (!decodedToken) return

        return decodedToken
    }
}

export function getUserFromSession(req) {
    console.log('[CHECK-AUTH] - checking if user is stored in session')
    return req.session ? req.session.userId : null
}


4. profile画面

ログインの状態でアクセスできるが、未ログイン状態でアクセスするとlogin画面にリダイレクトする画面を作成します。

<template>
  <div>
    <div class="header">
      <div class="logout-link" @click="signOut">Logout</div>
    </div>
    <h1>My Profile</h1>
  </div>
</template>

<script>
import { getUserFromCookie } from '@/helpers/cookies.js' 
import { auth } from "~/helpers/fireinit.js";

export default {
  asyncData({redirect, req}) {
    if (process.server) {
      console.log("login", "process.server")
      const user = getUserFromCookie(req)
      if (!user) {
        redirect('/login')
      }
    } else {
      console.log("login", "process.client")
      var user = auth.currentUser
      if (!user) {
        redirect('/login')
      }
    }
  },
  methods: {
    signOut: function() {
      console.log('sign out')
      let that = this
      auth.signOut().then(() => {
        that.$router.replace('/login')
      }).catch(error => console.log(error))
    }
  }
}
</script>

<style>
.header {
  display: flex;
  justify-content: space-around;
  margin: 10px 20px;
  width: 30%;
}

.logout-link {
  cursor: pointer;
  text-decoration: underline;
  color: #551a8b;
}
</style>

画面の実装を説明します。

ログアウトできるため、signOut関数を作成する。ログアウトしたら、login画面に遷移する。

ログイン状態を判断するため、asyncData関数にlogin画面と似てる処理を実装する。



注意点


1. client-onlyの記述

リダイレクト式の認証を行う際に、認証を完了してlogin画面に戻ってすぐにログイン成功画面の表示ができていないことがあります。 しばらく経ったら、ログインできた画面に切り替えます。 これはリダイレクト画面から戻る時に、認証情報を確認する時間が必要だと思います。

解決策としては、<client-only></client-only>で表示部分を囲むようにします。そうすると認証情報を確認できた後に画面をユーザーに表示することになります。



最後

難しい実装をしていないですが、asyncDataの部分はかなり気になります。 認証が必要なページが増えると、各ページにasyncDataの実装は必要になるのではないかと思っています。 共通化できればそのようにしたいです。 middlewareを使って、認証を確認する処理を統一できるようですが、isLogin変数の処理をstoreで処理する必要があるかもしれません。 他に良い方法があるかもしれませんが、またわかるようになったら追記します。