🤩

Nuxt.js(SSR) + Firebase Authenticationまとめ

2021/02/20に公開
1

概要

初投稿は、Nuxt.js(SSR) + Firebase Authenticationで認証管理を実装する自分なりのベストプラクティスをまとめたいので書きました。この内容には、構文間違いや書き方がよくないなどあると思いますのでその時はコメントなどで教えていただけると自分の為にもなりますのでどうぞよろしくお願いします。


※画質は指摘しないでねw 動かしたかったらこのrepo

Firebaseの準備

  1. Firebaseプロジェクトを作成する(Analyticsは本番以外では設定しなくていい)
  2. Authenticationを開始して、Google認証を有効にする
  3. RealTime Databaseをユーザー登録情報用に起動(アメリカ鯖で)
  4. マイアプリにアプリを登録して、Firebase SDK snippetを構成に選択してConfigデータを取得

※あくまで必要最低限と好みで選んでいます。(Cloud FireStoreでも構いません)

Nuxt.jsの準備

  1. Nuxtプロジェクトを作成する

    yarn create nuxt-app <project-name>
    

    ※いくつかの質問(名前、Nuxt オプション、UI フレームワーク、TypeScript、linter、テストフレームワークなど)されます。すべてのオプションをみるには Create Nuxt app を参照してください。

  2. モジュールをインストールする
    今回利用するモジュールを一括インストールします。

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

    各モジュールの情報一覧

    • firebase-admin[2]
    • @nuxtjs/pwa[3]
    • @nuxtjs/firebase[4]
    • @nuxtjs/dotenv[5]
  3. nuxt.config.jsの設定

    nuxt.config.js
    require('dotenv').config()
    
    modules: [
        '@nuxtjs/dotenv',
        '@nuxtjs/pwa',
        [
          '@nuxtjs/firebase',
          {
    	config: {
    	  apiKey: process.env.apiKey,
    	  authDomain: process.env.authDomain,
    	  databaseURL: process.env.databaseURL,
    	  projectId: process.env.projectId,
    	  storageBucket: process.env.storageBucket,
    	  messagingSenderId: process.env.messagingSenderId,
    	  appId: process.env.appId,
    	  measurementId: process.env.measurementId
    	},
    	services: {
    	  auth: {
    	    persistence: 'local',
    	    initialize: {
    	      onAuthStateChangedAction: 'onAuthStateChanged',
    	    },
    	    ssr: true
    	  }
    	}
          }
        ]
      ],
    
    pwa: {
        meta: false,
        icon: false,
    
        workbox: {
          importScripts: [
    	'/firebase-auth-sw.js'
          ],
    
          dev: true
        }
      },
    
  4. 環境変数の設定

    .env
    apiKey=""
    authDomain=""
    databaseURL=""
    projectId=""
    storageBucket=""
    messagingSenderId=""
    appId=""
    measurementId=""
    

Vuex作成

store/index.js
export const state = () => ({
  authUser: null
})

export const getters = {
  isLoggedIn: state => !!state.authUser
}

export const actions = {
  async nuxtServerInit({ dispatch }, ctx) {
    if (this.$fire.auth === null) {
      throw 'nuxtServerInit Example not working - this.$fire.auth cannot be accessed.'
    }

    if (ctx.$fire.auth === null) {
      throw 'nuxtServerInit Example not working - ctx.$fire.auth cannot be accessed.'
    }

    if (ctx.app.$fire.auth === null) {
      throw 'nuxtServerInit Example not working - ctx.$fire.auth cannot be accessed.'
    }

    if (ctx.res && ctx.res.locals && ctx.res.locals.user) {
      const { allClaims: claims, ...authUser } = ctx.res.locals.user

      console.info(
        'Auth User verified on server-side. User: ',
        authUser,
        'Claims:',
        claims
      )

      await dispatch('onAuthStateChanged', {
        authUser,
        claims,
      })
    }
  },

  async onAuthStateChanged({ commit }, { authUser, claims }) {
    if (!authUser) {
      // ログアウトしたらページ遷移します
      await this.$router.push("/login")
      commit('RESET_STORE')
      return
    }

    if (authUser && claims) {
      try {
	// ログインしたらページ遷移します
        await this.$router.push("/profile")
      } catch (e) {
        console.error(e)
      }
    }

    commit('SET_USER', { authUser, claims })
  }
}

export const mutations = {
  // stateを空にします
  RESET_STORE(state) {
    state.authUser = null
  },
  // stateにpayloadをセットします
  SET_USER(state, { authUser, claims }) {
    state.authUser = {
      uid: authUser.uid,
      email: authUser.email,
      emailVerified: authUser.emailVerified,
      displayName: authUser.displayName,
      photoURL: claims.picture,
      isAdmin: claims.admin
    }
  }
}

一般ユーザー向けの実装

Pages作成

ログインページ

pages/login.vue
<template>
  <div>
    <h1>Login</h1>
    <button @click="signInWithGoogle">Sign in with Google</button>
  </div>
</template>

<script>
  export default {
    layout: "default",
    name: "login",
    methods: {
      async signInWithGoogle() {
      const provider = new this.$fireModule.default.auth.GoogleAuthProvider();
      await this.$fire.auth.signInWithPopup(provider).then(res => {
  
        res.user.getIdToken(true).then(idToken => {
          localStorage.setItem('access_token', idToken.toString())
          localStorage.setItem('refresh_token', res.user.refreshToken.toString())
        })
      })
      console.log('成功しました')
    },
    }
  }

プロフィールページ

pages/profile.vue
<template>
  <div>
    <h1>User Profile</h1>
    <p v-if="User != null">Your e-mail is {{ User.email }}</p>
    <button @click="logout">Logout</button>
  </div>
</template>

<script>
export default {
  layout: "default",
  name: "profile",
  middleware: "auth",
  data() {
    return {
      User: this.$store.state.authUser
    }
  },
  methods: {
    async logout() {
      await this.$fire.auth.signOut()
    },
  }
}
</script>

Middleware作成

認証ユーザー限定ページ用ミドルウェア

middleware/auth.js
export default function({ store, redirect }) {
  if(!store.getters['isLoggedIn']) {
    return redirect('/login')
  }
}

管理者向け実装

Pages作成

管理者ページ

pages/admin/index.vue
<template>
  <div>
    <h1>Admin</h1>
  </div>
</template>

<script>
  export default {
    name: "index",
    middleware: ["auth", "check-admin"],
  }
</script>

プロフィールページ

pages/profile
<template>
  <div>
    <h1>User Profile</h1>
    // 追加
    <p v-if="User != null">Your e-mail is {{ User.email }} <span v-if="User.isAdmin">& claims: <button @click="returnAdmin">admin</button></span></p>
    <button @click="logout">Logout</button>
  </div>
</template>

<script>
export default {
  layout: "default",
  name: "profile",
  middleware: "auth",
  data() {
    return {
      User: this.$store.state.authUser
    }
  },
  methods: {
    async logout() {
      await this.$fire.auth.signOut()
    },
    // 追加
    async returnAdmin() {
      await this.$router.push("/admin")
    }
  }
}
</script>

Middleware作成

管理者限定ページ用ミドルウェア

middleware/check-admin.js
export default function({ store, redirect }) {
  if(!store.getters['isAdminIn']) {
    return redirect("/profile")
  }
}

Vuex更新

store/index.js
export const getters = {
  // 追加
  isAdminIn: state => !!state.authUser.isAdmin
}

さらに細かい処理を実装

https://zenn.dev/watson_sei/articles/a2140e2b2dec73

参考

【Nuxt.js(SSR) × Firebase Authentication】@nuxtjs/firebaseモジュールを使ったログインセッション管理
@nuxtjs/firebase social auth

脚注
  1. 公式サイト Firebase JavaScript SDKは、Firebaseサービスを使用するアプリケーションで使用されるクライアント側ライブラリを実装します ↩︎

  2. 公式サイト Admin SDK を使用すると、特権環境から Firebase と対話します。 ↩︎

  3. 公式サイト Nuxt.js用のゼロ構成PWAソリューション ↩︎

  4. 公式サイト Firebase JavaScriptSDKをアプリケーションに簡単に統合するのに役立つモジュールです。 ↩︎

  5. 公式サイト .envファイルをコンテキストオプションにロードするNuxt.jsモジュール ↩︎

Discussion

sacckeysacckey

参考記事の「@nuxtjs/firebaseモジュールを使ったログインセッション管理」を書いた者です。
記事のURLを以下に変更しておりますので、お手隙で修正していただけますと幸いです。
https://note.com/sacckey/n/nbe3ab8553a05