Vue.jsによるGoogleの認証機能を構築する方法 【認証機能編】

Vue.jsを使用したGoogle認証機能(OAuth 2.0) を構築する方法が日本の記事に少なかったため、覚書として公開します。
今回は認証機能編ということで、認証してTokenをsessionStorageに保存するところまで実装していきます。

本章の完成イメージ

Google oAuthの準備

oAuth同意画面の設定

Google Console Platform (以後GCP) にアクセスし、oAuthの同意画面を設定します。
GCPのプロジェクト毎に一度設定すれば良いので、すでに設定済みの場合は次に進みます。

oAuth同意画面を作成

oAuth同意画面に移動し、User Typeを選択します。
プロジェクト内部のメンバーでしか使用しないページを作成する場合は内部、インターネットに公開し誰でもログインできるようにする場合は外部 を選択し、作成 を押下します。

アプリケーション名 を入力し、保存します。

このとき、アプリケーションのロゴ を選択してしまうとGoogleによる承認が必要になり、使用できなくなってしまうため選択しないでください。
間違えて選択して保存してしまった場合、後から取り消すことができず、承認するにはLanding pageの用意とプライバシーポリシーの用意が必要になります。

oAuthクライアントの作成

認証情報に移動し、認証情報を作成を押下します。

OAuthクライアントIDを選択肢、oAuthクライアントIDを作成します。

アプリケーションの種類ウェブアプリケーションにし、名前URI を入力して作成します。
URIには、WebアプリケーションのSchema + Domainを入力します。
今回はローカルで検証するため、http://localhost:8080を入力しています。

oAuthクライアントを作成すると、クライアントIDクライアントシークレットが表示されるので、メモしておきます。

Vueにログイン機能の組み込み

Vueにログイン機能を組み込んでいきます。
これから使用するベースは前章で作成したものを使用しています。

ログイン情報を保存するためのStoreを作成する

ログイン情報を保存するために、Storeを作成します。
値をセッションで保存するために、sessionStorageを指定しています。
sessionStorageを指定しない場合、画面をリロードする度に値が削除されログアウトされます。

  • src/store.js
import Vue from 'vue'
import Vuex from 'vuex'
import createPersistedState from 'vuex-persistedstate'

Vue.use(Vuex)

export default new Vuex.Store({
  plugins: [createPersistedState({
    key: 'vue-gauth',
    paths: ['userInfo'],
    storage: window.sessionStorage
  })],
  state: {
    userInfo: {}
  },
  mutations: {
    setUserInfo: function (state, userInfo) {
      state.userInfo = userInfo
    }
  },
  actions: {
  }
})

Google oAuthのパッケージを組み込む

Google oAuthをVueで使用できるようにするため、パッケージを組み込んでいきます。

まず、vue-google-oauth2をインストールします。

yarn add vue-google-oauth2

次に、認証を行うためにインスタンスを作成し、Vueに読み込ませます。
{CLIENT_ID}には、事前にメモしたoAuthクライアントのクライアントIDを入力します。

また、先ほど作成したStoreも呼び出せるように読み込みます。

  • src/main.js
import Vue from 'vue'
import App from './App.vue'
import GAuth from 'vue-google-oauth2'
import router from './router'
import store from './store'

Vue.config.productionTip = false
Vue.use(GAuth, { 
    clientId: '{CLIENT_ID}', scope: 'email profile openid', prompt: 'consent', fetch_basic_profile: false
})

new Vue({
  render: h => h(App),
  router,
  store
}).$mount('#app')

トップメニューにログイン機能を作成する

ヘッダにログインボタンとログアウトボタンを作成し、ログイン/ログアウトができるようにします。
この時、userInfo が存在しない場合のみLoginを表示し、存在する場合はLogoutを表示するようにします。
また、NeedAuthページはログイン時にのみ使用できる機能なため、userInfoが存在する場合のみ表示されるようにします。

  • src/components/PageHeader.vue
<template>
  <div>
    <nav class="navbar navbar-expand-lg navbar-light bg-light p-1">
      <div class="navbar-collapse" id="navbarSupportedContent">
        <ul class="navbar-nav mr-auto">
          <li class="nav-item d-flex align-items-center">
            <router-link class="text-dark px-2" to="/">Home</router-link>
          </li>
          <li class="nav-item d-flex align-items-center">
            <router-link v-if="Object.keys($store.state.userInfo).length" class="text-dark px-2" to="/need-auth">NeedAuth</router-link>
          </li>
        </ul>
        <ul class="navbar-nav">
          <li class="nav-item">
            <a v-if="!Object.keys($store.state.userInfo).length" class="text-dark px-3" href="javascript:void(0);" @click.prevent="handleSignIn">Login</a>
            <a v-if="Object.keys($store.state.userInfo).length" class="text-dark px-3" href="javascript:void(0);" @click.prevent="handleSignOut">Logout</a>
          </li>
        </ul>
      </div>
    </nav>
  </div>
</template>

<script>

export default {
  methods: {
    async handleSignIn () {
      try {
        const googleUser = await this.$gAuth.signIn()
        if (!googleUser) {
          return null
        }
        this.$store.commit('setUserInfo', googleUser.getAuthResponse())
      } catch (error) {
        console.error(error)
        return null
      }
    },
    async handleSignOut () {
      try {
        await this.$gAuth.signOut()
          this.$store.commit('setUserInfo', {})
        window.location.href = '/'
      } catch (error) {
        console.error(error)
      }
    }
  }
}
</script>

ログイン必須ページをログイン時にのみ表示する

ログイン必須ページとなるNeedAuthページを、ログイン時にしか表示できないようにします。
今のままでもトップメニューにはログイン時にしか表示されませんが、URLを指定するとアクセスできてしまうためURLを指定してもアクセスできないようにします。

  • src/router.js
import Vue from 'vue'
import BootstrapVue from 'bootstrap-vue'
import Router from 'vue-router'
import Home from '@/components/Home'
import needAuth from '@/components/NeedAuth'
import store from './store'

import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

Vue.use(Router)
Vue.use(BootstrapVue)

const router = new Router({
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home
    },
    {
      path: '/need-auth',
      name: 'need-auth',
      component: needAuth,
      meta: { requiresAuth: true }
    },
  ]
})

router.beforeEach((to, from, next) => {
  if (to.matched.some(record => record.meta.requiresAuth)) {
    if (Object.keys(store.state.userInfo).length) {
      next()
    } else {
      next({ path: '/', query: { redirect: to.fullPath }})
    }
  }
  next()
})

export default router

おわりに

以上で、基本となる認証機能の実装は完了です。
認証時に取得したTokenを使用し、Google機能にアクセスさせたり、API側でJWTトークンの検証を行いGoogleログインしたユーザにのみ利用できる機能の提供を行うことが可能となります。

今回のサンプルはGitHubにコードを公開しているので、参考にしてみてください。

fealone/vue-google-auth-example
Google authenticate example with vue.js. Contribute to fealone/vue-google-auth-example development by creating an account on GitHub.