Vue.js の主要な機能をざっくりとつかってみたときのメモ(Firebase認証・認可編)

 「[Vue.js の主要な機能をざっくりとつかってみたときのメモ](https://qiita.com/masatomix/items/24142b084b745ce816a9)」や「[Vue.js の主要な機能をざっくりとつかってみたときのメモ(Firebase編)](https://qiita.com/masatomix/items/7be14b8eb433edcfbfc7)」のつづきです。。
前回までで、FirebaseのFirestoreにTodoリストをCRUDしました。今回はFirebaseのOAuthの認証・認可機能を使ってみます。
OAuthってのは、いわゆる「Googleアカウントでログイン」などのリンクを押すと、Googleのサイトのポップアップが出てきて、このアカウントをつかってログインしていい？って聞かれるヤツです。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/73777/84292777-bbc7-31fb-5ed6-974fec8b8a73.png)
本来は、**認可サーバであるGoogleが「Firebaseをつかった(WEB)アプリに、あなたのGoogleアカウント情報を読ませてもいいかな？」って、Googleアカウントのオーナ(ユーザ)に許可つまり認可させる**という「認可のための」仕組みなのですが、その際にポップアップでユーザを認証するため、結果として「Firebaseをつかった(WEB)アプリにGoogleアカウントでログインする(ユーザ情報が連携される)という処理シーケンスとなります。
参考:[Authleteを使った認可サーバの構築と、OAuthクライアント(Webアプリケーション)からの疎通](https://qiita.com/masatomix/items/91c486f40ce3120a4686)
## そのまえに bootstrap-vue いれさせて
そのまえに、前回から、今回の記事作成までの最中に[bootstrap-vue](https://bootstrap-vue.js.org/) を導入しているので、そのインストールについて。
### インストールとコードへの反映

```bash
$ npm install bootstrap-vue --save
```

インストールはこれだけであとは main.js に以下を追加します。

```javascript:main.js
import BootstrapVue from "bootstrap-vue"
import "bootstrap/dist/css/bootstrap.min.css"
import "bootstrap-vue/dist/bootstrap-vue.css"
Vue.use(BootstrapVue)
```
これでvue.jsでbootstrap-vueが利用可能になりました。。Header.vueなどにログインリンクなどをつけたり適度に装飾してますが、コードはあとで出てくるので、ココでは割愛します :-)
## Firebaseの認証をやってみる
### Google認証を有効にする
[Firebaseのコンソール](https://console.firebase.google.com/u/0/?hl=ja)から、自分が作成したプロジェクトを選択し、左のメニュー部の「Authentication」を選択。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/73777/e62a5031-0754-e15d-d35a-bc211cbb207e.png)
「ログイン方法」を選択すると、下記のようにログイン方法を選択する画面になります。今回はGoogleをつかうので、プロバイダのGoogleを「有効」にしておきましょう。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/73777/11a24764-8ba3-b29d-fefb-d6994bf2de7b.png)
OAuthの認可サーバを使用するときって、認可サーバ側からWEBアプリに対して「client_id/client_secret」を払い出してもらい、それらをパラメタに載せることでWEBアプリ自体の認証を行うのですが、Google もFirebaseもGoogle だからですかね、この辺はあらかじめ設定済みになってるようです。
### 承認済みドメインを追加する
この認証機能を利用出来るドメインを指定します。いまはlocalhostで起動しているので設定不要ですが、外部に公開するサーバのURLが http://client.example.com:8080 などだった場合、 client.example.com を設定追加しておきましょう。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/73777/a6da523a-fe5e-b86c-93ca-d5dba4629ed2.png)

コレを行わないと、あとで出てくる ``firebase.auth().signInWithPopup``関数が``exception``となるみたいですね。


### ライブラリのインストール
Firebaseのライブラリは[前回](https://qiita.com/masatomix/items/7be14b8eb433edcfbfc7#%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)インストール済みなのですが、ログインしているユーザ情報やログイン状態をjsやvue間で共有するために、オブジェクトの状態を管理するフレームワーク[Vuex](https://vuex.vuejs.org/ja/guide/)を導入します。
Vuexは、 Vuex.Store というオブジェクトに対して、

```javascript:store.js
import Vue from 'vue'
import Vuex from 'vuex'
import createPersistedState from 'vuex-persistedstate'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    user: {},
    loginStatus: false
  },
  mutations: {
    user (state, user) { state.user = user },
    loginStatus (state, loginStatus) { state.loginStatus = loginStatus }
  },
  plugins: [
    createPersistedState({storage: window.sessionStorage, key: 'vuex-todo-examples'})
  ]
})
```

というインスタンスを保持する領域 ``user``,``loginStatus``を作成しておき、jsやvueからは

```vuejs:Header.vue(のscript部)
<script>
import firebase from 'firebase'
export default {
  name: 'Header',
  computed: {
    loginStatus () {
      return this.$store.state.loginStatus // vuexのインスタンスを参照
    },
    user () {
      return this.$store.state.user // vuexのインスタンスを参照
    }
  },
  ...
}
</script>
```
とやることで参照ができます。
保存しているインスタンスの更新については、直接プロパティを操作するのではなく

```vuejs:Login.vue(のscript部。あとで出てますが新規で作ったログイン画面)
<script>
import firebase from 'firebase'
export default {
  name: 'Login',
  ... 割愛
  methods: {
    loginWithGoogle () {
      const provider = new firebase.auth.GoogleAuthProvider()
      firebase
        .auth()
        .signInWithPopup(provider)
        .then(result => {
          this.$store.commit('user', result.user) // インスタンスの更新
          this.$store.commit('loginStatus', true) // インスタンスの更新
          this.$router.push(
            this.$route.query.redirect ? this.$route.query.redirect : '/'
          )
        })
        .catch(function (error) {
          const errorCode = error.code
          const errorMessage = error.message
          alert(errorMessage)
        })
    }
  }
}
</script>
```
このように``$store.commit('メソッド名','値')`` という仕様で``Vuex.Store`` にmutationsで定義していたメソッドを呼びだすことで、データを更新します。

- [Vue.js + Vuexでデータが循環する全体像を図解してみた](https://qiita.com/m_mitsuhide/items/f16d988ec491b7800ace)
- [Vuexのはじめかた - Vuex入門者向けのはじめの一歩ガイド](https://masayuki14.hatenablog.com/entry/2017/12/14/000000)

このVuexをつかえばアプリケーション全体で状態が管理出来るので、Googleログインが成功した際やユーザ情報が更新されたとき、そのインスタンスを保存しておき、各画面ではそのインスタンスのあるなしで、ログイン済みかどうかを判定することができそうです。

### ということでVuexのインストール

```bash
npm install --save vuex vuex-persistedstate
```

vuexはライブラリそのもの、vuex-persistedstate がそのデータをLocalStorageやSessionStorageに保存するためのライブラリです。
### ソース追加・修正内容
さてソースについて。多いので一覧をつけました。

- [src/main.js で、Firebaseのユーザ情報の更新を検知](#srcmainjs-でfirebaseのユーザ情報の更新を検知)
- [src/store.js: Vuexの追加](#srcstorejs-vuexの追加)
- [components/Header.vue: ヘッダにログアウトリンクを追加](#componentsheadervue-ヘッダにログアウトリンクを追加)
- [router/index.js にログイン画面のルーティング追加](#routerindexjs-にログイン画面のルーティング追加)
- [components/Login.vue: /loginで呼ばれる画面を追加](#componentsloginvue-loginで呼ばれる画面を追加)

順番に見ていきましょう。

#### src/main.js で、Firebaseのユーザ情報の更新を検知
ユーザがログイン中かの判定は、Googleアカウントログイン後や、そのユーザ情報が更新された際、そのユーザ情報をvuexに保存しておいて、そのあるなしをチェックすればよいという話でした。
なのでmain.jsで先ほどのstore.jsをimportし「Firebaseの認証機能を経由してユーザ情報が更新されたときに呼ばれるコールバック」で、vuexのユーザ情報とログイン状態を更新することにしました。

```javascript:src/main.js
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
import store from '@/store' // 追加
import firebase from 'firebase'
import firebaseConfig from '@/firebaseConfig'
import BootstrapVue from 'bootstrap-vue'
import 'bootstrap/dist/css/bootstrap.min.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'
Vue.use(BootstrapVue)
firebase.initializeApp(firebaseConfig)
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.SESSION) // 追加
Vue.config.productionTip = false
// 追加
firebase.auth().onAuthStateChanged(function (user) {
  // ユーザ情報が変更されたら呼ばれる
  if (user) {
    // User is signed in.
    store.commit('user', user)
    store.commit('loginStatus', true)
  } else {
    store.commit('user', {})
    store.commit('loginStatus', false)
  }
})
// ここまで
// router.beforeEach((to, from, next) => {
// あとで追加する
/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  store,
  components: { App },
  template: '<App/>'
})

```

#### src/store.js: Vuexの追加
ユーザ情報とログイン状態を保持するためのvuexを作成しました。先のソースとおなじものです。

```javascript:src/store.js(新規)
// さっき載せたのとおなじ
import Vue from 'vue'
import Vuex from 'vuex'
import createPersistedState from 'vuex-persistedstate'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    user: {},
    loginStatus: false
  },
  mutations: {
    user (state, user) { state.user = user },
    loginStatus (state, loginStatus) { state.loginStatus = loginStatus }
  },
  plugins: [
    createPersistedState({storage: window.sessionStorage, key: 'vuex-todo-examples'})
  ]
})
```

#### components/Header.vue: ヘッダにログアウトリンクを追加
つづいてHeader.vueです。共通ヘッダにしていた箇所に(まだログインも実装してないんですがorz)ログアウトのリンクを追加します。
``<template>`` に色々書いてますが、bootstrapのGUIの記述がほとんどです。ログアウトリンクの表示可否をvuexの``loginStatus`` で制御しています。
また、ログアウトリンクをクリックしたときに呼ばれる ``logout()`` メソッドも追加しています。こちらも``firebase.auth().signOut()`` を呼び出している程度で、とてもシンプルです。

```vuejs:components/Header.vue
<template>
  <b-navbar toggleable="md" type="dark" variant="info">
    <b-navbar-toggle target="nav_collapse"></b-navbar-toggle>
    <b-navbar-brand href="#">ToDo管理</b-navbar-brand>
    <b-collapse is-nav id="nav_collapse">
      <!-- Right aligned nav items -->
      <b-navbar-nav class="ml-auto" v-if="loginStatus">
        <b-nav-item-dropdown right>
          <!-- Using button-content slot -->
          <template slot="button-content">
            <em>
              <span>{{user.displayName}}</span>
            </em>
          </template>
          <b-dropdown-item @click="logout()">{{user.displayName}}さんを Signout</b-dropdown-item>
        </b-nav-item-dropdown>
      </b-navbar-nav>
    </b-collapse>
  </b-navbar>
</template>

<script>
import firebase from 'firebase'
export default {
  name: 'Header',
  computed: {
    loginStatus () {
      return this.$store.state.loginStatus
    },
    user () {
      return this.$store.state.user
    }
  },
  methods: {
    logout () {
      firebase.auth().signOut()
      // のちに画面遷移処理を追加する。
    }
  }
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
```

#### router/index.js にログイン画面のルーティング追加
Routerについては、/login でログイン画面を表示させるためのルーティングを追加しています。

```javascript:router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
import Login from '@/components/Login'
Vue.use(Router)
export default new Router({
  routes: [
    {
      path: '/',
      name: 'HelloWorld',
      component: HelloWorld
    },
    // ↓追加
    {
      path: '/login',
      component: Login
    }
  ]
})

```

#### components/Login.vue: /loginで呼ばれる画面を追加
さて /login で呼び出される実際のログイン画面です。
http://localhost:8080/#/login で表示されるログイン画面となります。

```vuejs:components/Login.vue(新規)
<template>
  <b-container>
    <div class="form-signin">
      <button type="button" class="google-button" @click="loginWithGoogle">
        <span class="google-button__icon">
          <svg viewBox="0 0 366 372" xmlns="http://www.w3.org/2000/svg"><path d="M125.9 10.2c40.2-13.9 85.3-13.6 125.3 1.1 22.2 8.2 42.5 21 59.9 37.1-5.8 6.3-12.1 12.2-18.1 18.3l-34.2 34.2c-11.3-10.8-25.1-19-40.1-23.6-17.6-5.3-36.6-6.1-54.6-2.2-21 4.5-40.5 15.5-55.6 30.9-12.2 12.3-21.4 27.5-27 43.9-20.3-15.8-40.6-31.5-61-47.3 21.5-43 60.1-76.9 105.4-92.4z" id="Shape" fill="#EA4335"/><path d="M20.6 102.4c20.3 15.8 40.6 31.5 61 47.3-8 23.3-8 49.2 0 72.4-20.3 15.8-40.6 31.6-60.9 47.3C1.9 232.7-3.8 189.6 4.4 149.2c3.3-16.2 8.7-32 16.2-46.8z" id="Shape" fill="#FBBC05"/><path d="M361.7 151.1c5.8 32.7 4.5 66.8-4.7 98.8-8.5 29.3-24.6 56.5-47.1 77.2l-59.1-45.9c19.5-13.1 33.3-34.3 37.2-57.5H186.6c.1-24.2.1-48.4.1-72.6h175z" id="Shape" fill="#4285F4"/><path d="M81.4 222.2c7.8 22.9 22.8 43.2 42.6 57.1 12.4 8.7 26.6 14.9 41.4 17.9 14.6 3 29.7 2.6 44.4.1 14.6-2.6 28.7-7.9 41-16.2l59.1 45.9c-21.3 19.7-48 33.1-76.2 39.6-31.2 7.1-64.2 7.3-95.2-1-24.6-6.5-47.7-18.2-67.6-34.1-20.9-16.6-38.3-38-50.4-62 20.3-15.7 40.6-31.5 60.9-47.3z" fill="#34A853"/></svg>
        </span>
        <span class="google-button__text">Sign in with Google</span>
      </button>
    </div>
  </b-container>
</template>
<script>
import firebase from "firebase";
// import 'firebaseui/dist/firebaseui.css'
export default {
  name: "Login",
  methods: {
    loginWithGoogle() {
      const provider = new firebase.auth.GoogleAuthProvider();
      firebase
        .auth()
        .signInWithPopup(provider)
        .then(result => {
          this.$store.commit("user", result.user);
          this.$store.commit("loginStatus", true);
          this.$router.push(
            this.$route.query.redirect ? this.$route.query.redirect : "/"
          );
        })
        .catch(function(error) {
          const errorCode = error.code;
          const errorMessage = error.message;
          alert(errorMessage);
        });
    }
  }
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
html,
body {
  height: 100%;
}
body {
  display: -ms-flexbox;
  display: flex;
  -ms-flex-align: center;
  align-items: center;
  padding-top: 40px;
  padding-bottom: 40px;
  background-color: #f5f5f5;
}
.form-signin {
  width: 100%;
  max-width: 330px;
  padding: 15px;
  margin: auto;
}
.form-signin .checkbox {
  font-weight: 400;
}
.form-signin .form-control {
  position: relative;
  box-sizing: border-box;
  height: auto;
  padding: 10px;
  font-size: 16px;
}
.form-signin .form-control:focus {
  z-index: 2;
}
.form-signin input[type="email"] {
  margin-bottom: -1px;
  border-bottom-right-radius: 0;
  border-bottom-left-radius: 0;
}
.form-signin input[type="password"] {
  margin-bottom: 10px;
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}
/* Shared */
.loginBtn {
  box-sizing: border-box;
  position: relative;
  /* width: 13em; - apply for fixed size */
  margin: 0.2em;
  padding: 0 15px 0 46px;
  border: none;
  text-align: left;
  line-height: 34px;
  white-space: nowrap;
  border-radius: 0.2em;
  font-size: 16px;
  color: #fff;
  cursor: pointer;
}
.loginBtn:before {
  content: "";
  box-sizing: border-box;
  position: absolute;
  top: 0;
  left: 0;
  width: 34px;
  height: 100%;
}
.loginBtn:focus {
  outline: none;
}
.loginBtn:active {
  box-shadow: inset 0 0 0 32px rgba(0, 0, 0, 0.1);
}
.google-button {
  height: 40px;
  border-width: 0;
  background: white;
  color: #737373;
  border-radius: 5px;
  white-space: nowrap;
  box-shadow: 1px 1px 0px 1px rgba(0, 0, 0, 0.05);
  transition-property: background-color, box-shadow;
  transition-duration: 150ms;
  transition-timing-function: ease-in-out;
  padding: 0;
  box-shadow: 1px 4px 5px 1px rgba(0, 0, 0, 0.1);
}
.google-button:hover {
  cursor: pointer;
}
.google-button:active {
  box-shadow: 3px 6px 7px 3px rgba(0, 0, 0, 0.1);
  transition-duration: 10ms;
}
.google-button__icon {
  display: inline-block;
  vertical-align: middle;
  margin: 8px 0 8px 8px;
  width: 18px;
  height: 18px;
  box-sizing: border-box;
}
.google-button__icon--plus {
  width: 27px;
}
.google-button__text {
  display: inline-block;
  vertical-align: middle;
  padding: 0 24px;
  font-size: 14px;
  font-weight: bold;
  font-family: "Roboto", arial, sans-serif;
}
</style>
```

``npm run dev`` して表示してみると、こんな感じ。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/73777/2f23f2ca-2082-3d64-cc9b-3d1f4b62b129.png)
コードが長くてちょっとアレですが、``<template>``と``<style>``、``<script>`` それぞれみてみましょう。
まずtemplate部とstyle部いわゆる画面は、Googleログインのリンクボタンを追加しているだけです。styleはまあ長いですがボタンに適用しているcssですね。
つぎにボタンのクリックで呼ばれるメソッド ``loginWithGoogle`` (script部)ですが、

```javascript
methods: {
  loginWithGoogle(){
    const provider = new firebase.auth.GoogleAuthProvider();
    firebase.auth().signInWithPopup(provider).then(......)
    ...
  }
}
```

まずココまででポップアップが表示され、そのWindowsにGoogleがログイン画面を表示してくれます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/73777/84292777-bbc7-31fb-5ed6-974fec8b8a73.png)
ポープアップ画面でGoogleへのログインとユーザの認可オペが完了すると、ポップアップは自動で閉じられ、``then``に記述した下記のコールバックが呼び出されます。

```javascript:then内のコールバック
result => {
  this.$store.commit('user', result.user)
  this.$store.commit('loginStatus', true)
  this.$router.push(
  this.$route.query.redirect ? this.$route.query.redirect : '/'
  // あとで詳細説明
  )
}
```

このコールバック処理ではvuexにあるuser/loginStatusを

- コールバックに渡ってくるユーザ情報(``result.user``)
- ログインステータスを``true``

で更新し、'/' へ画面遷移します。 '/'はルーティングでいままでのToDo画面が定義されているので、結果としてGoogle認証が完了するとToDo画面が表示されることになります。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/73777/80211159-c7f2-ab51-9336-7044f83c6745.png)
ちなみにToDo画面であるHelloWorld.vue については、全体を

```vuejs
<main v-if="$store.state.loginStatus" class="container">
```
で囲うことで、ログインステータスが ``true``のときのみコンテンツを表示するようにしています。

```vuejs:HelloWorld.vue(のtemplate部)
<template>
  <main v-if="$store.state.loginStatus" class="container">
    <h1>
      My Todo Task
      <span class="info">({{remainingTask.length}}/{{todos.length}})</span>
      <span class="info" style="cursor:pointer" @click="checkAll()" v-if="!isAllChecked()">すべてチェック/はずす</span>
      <span class="info" style="cursor:pointer" @click="unCheckAll()" v-if="isAllChecked()">すべてチェック/はずす</span>
      <b-button size="sm" variant="secondary" @click="deleteEndTask">完了タスクの削除</b-button>
    </h1>
    <ul>
      <li v-for="todo in todos" :key="todo.id">
        <input type="checkbox" v-model="todo.isDone" @click="toggle(todo.id)">
        <span v-bind:class="{done: todo.isDone}">{{todo.name}}</span>
        <span @click="deleteTask(todo.id)" class="xButton">[x]</span>
      </li>
    </ul>
    <form @submit.prevent="addTask">
      <input type="text" v-model="newTask" placeholder="タスクを入力">
      <b-button type="submit" variant="primary" style="margin:4px">追加</b-button>
    </form>
  </main>
</template>
```
### いま時点のソース
ここまでのソースは
https://github.com/masatomix/todo-examples/tree/for_qiita_auth_001
へコミット済みです。
一応このタグから構築する手順を示しておきます。

```bash
$ git clone --branch for_qiita_auth_001 https://github.com/masatomix/todo-examples.git
$ cd todo-examples/
$ npm install

src/firebaseConfig.js を自分の設定に書き換え

$ npm run dev
```

これで、http://localhost:8080/#/login にブラウザでアクセスできるとおもいます。

### いったん整理
さてここまでで、Googleアカウントでログインして、ToDoアプリケーションを使用するという基本的なところができあがりました。しかしながらまだ

- [ヘッダ右上のドロップダウンの「ログアウト」を選んだら、ログイン画面に遷移させたい(今はログアウトさせるだけ)](#ヘッダ右上のドロップダウンのログアウトを選んだらログイン画面に遷移させたい)
- [画面に応じて、ログインが必要・必要でない、を制御したい](#画面に応じてログインが必要必要でないを制御したい)
- [画面にアクセスしたときに、ログイン済みでない場合はログイン画面を表示し、認証したら該当画面へ遷移するようにしたい](#画面にアクセスしたときにログイン済みでない場合はログイン画面を表示し認証したら該当画面へ遷移する)
- [Firestore へのデータアクセスを、認証されたユーザのみに制限したい](#firestore-へのデータアクセスを認証されたユーザのみに制限したい)

などを対応したいので、つづけてやっていきます。
### ヘッダ右上のドロップダウンの「ログアウト」を選んだら、ログイン画面に遷移させたい
ヘッダのログアウトを選択すると、``logout()``が呼ばれますが、

```javascript:Header.vue(のscript部)
methods: {
  logout () {
    firebase.auth().signOut()
    // のちに画面遷移処理を追加する。
  }
}
```
にログアウト後のコールバックを追加します。

```javascript:Header.vue(のscript部)
methods: {
  logout () {
    firebase.auth().signOut()
    .then(() => {
    this.$router.push('/')
    window.location.reload() // 保持してた不要な情報を一度クリア(vuexはきえない)
    })
    .catch(function (error) {
      const errorCode = error.code
      const errorMessage = error.message
      alert(errorMessage)
    })
  }
}
```

``this.$router.push('/')`` だとログイン画面ではなくてToDo画面に行こうとするんですが、このあとの機能追加で **「ログイン済み状態じゃなかったらログイン画面に飛ばす」**処理を入れるので、結果ログイン画面に遷移します。
ちなみに ``firebase.auth().signOut()`` のコールバックに入った時点でユーザ情報は変更され、その結果main.jsに追加した

```javascript:src/main.js
  firebase.auth().onAuthStateChanged(function (user) {// ユーザ情報が変更されたら呼ばれる
    if (user) {
      // User is signed in.
      store.commit('user', user)
      store.commit('loginStatus', true)
    } else {
      store.commit('user', {})
      store.commit('loginStatus', false)
    }
  })
```
がうごきだして、vuex上の ``user``,``loginStatus``はそれぞれ``{}``と``false``で更新されます。

### 画面に応じて、ログインが必要・必要でない、を制御したい
さて、認証されていない場合はログイン画面に飛ばしたいのですが、ログイン画面が認証が必要だと無限ループするので、画面によってログインの必要可否を制御したいですね。
ということで、画面ごとの定義を router/index.js に追加します。

```javascript:router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
import Login from '@/components/Login'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'HelloWorld',
      component: HelloWorld
    },
    {
      path: '/login',
      component: Login,
      meta: {
        isPublic: true // このプロパティ追加
      }
    }
  ]
})

```
isPublicがtrueの場合は、認証状態をチェックしないで画面を表示します。デフォルトはfalseなので、ログイン画面以外は認証が必要という設定になりました。
この次の項の画面遷移の制御で、上記の定義を参照する機能を追加します。

### 画面にアクセスしたときに、ログイン済みでない場合はログイン画面を表示し、認証したら該当画面へ遷移する
さあもうすこしです。
#### src/main.js にナビゲーションガードを追加する
[公式のナビゲーションガード](https://router.vuejs.org/ja/guide/advanced/navigation-guards.html) に説明がありますが、ナビゲーションガード機構を使うことで、画面の遷移をフックして、状態をチェックするなどの処理を入れ込むことが出来ます。
こんな感じに、main.jsでナビゲーションガードを追加します。

```javascript:src/main.js
router.beforeEach((to, from, next) => {
  const currentUser = store.state.user
  if (currentUser.uid) {
    if (to.path === '/login') {
      firebase.auth().signOut().then(() => next())
    }
  }
  if (to.matched.some(record => record.meta.isPublic)) {
    // alert('isPublic = true '+ to.path)
    next()
  } else {
    // alert('isPublic = false '+ to.path)
    if (currentUser.uid) {
      next()
    } else {
      next({
        path: '/login',
        query: {
          redirect: to.path
        }
      })
    }
  }
})
```
つまり、URLが変更されるときに変更前のURL情報(from)と変更後のそれ(to)が渡ってくるので、

- vuex からユーザ情報が取れたときは、toがログイン画面(/login)へのアクセスだったら、いちどログアウトさせてユーザ情報をクリアしてから、ログイン画面へ遷移(next())させる
- 遷移先(to)のURLについて、isPublicがtrueの場合は、そのまま遷移(next())させる
- isPublicがfalseの場合は、
    - vuex からユーザ情報(``store.state.user.uid``)が取れたときはそのまま遷移(next())
    - vuex からユーザ情報(``store.state.user.uid``)が取れない場合は、ログイン画面へ遷移させる(※))

最後の処理(※)は

```javascript
next({
  path: '/login',
  query: {
    redirect: to.path
  }
})
```
の処理のことです。
/login に遷移する際 redirect というクエリパラメタに to.pathという変数をセットしていますが、たとえば /ui001 に遷移しようとしてナビゲーションガードによってログイン画面に遷移させられたとき、遷移後のログイン画面(/login)のURLが
http://localhost:8080/#/login?redirect=%2Fui001
とクエリパラメタが後ろにつくようにしています。このように redirect にto.pathを渡しているのは、ログインが成功した際に本来行きたかった画面のURL(/ui001ですね) の情報が必要だからです。
#### Login.vue でのログイン成功後の遷移先
さて、渡されたredirectパラメタですが、Login.vue でログイン成功後の画面遷移 はこのようになっていました。

```javascript:Login.vue(抜粋)
this.$router.push(
  this.$route.query.redirect ? this.$route.query.redirect : '/'
)
```
コレはつまり、redirectパラメタがある場合はそこのURLへ遷移、パラメタがない場合は '/' へ遷移(デフォルト値) するってことですね。
以上で、認証が必要な画面に行こうとした場合、ナビゲーションガードによって認証されているかがチェックされ、必要に応じてログイン画面を出した後、本来行きたい画面に遷移する流れを実装することが出来ました。
うーん、疲れましたね。。。

### Firestore へのデータアクセスを、認証されたユーザのみに制限したい
さあFirebaseでの認証ができたので、せっかくなのでFirestoreへのアクセスを、認証されたユーザのみに出来たらよりセキュアですね。
そのための制御はFirebase側の画面にあります。
https://console.firebase.google.com から自分のプロジェクトに移動し、Database >> ルール に遷移します
![image.png](https://qiita-image-store.s3.amazonaws.com/0/73777/40c3581f-d0fe-a0e9-e21f-2e69208dc8d2.png)
ココですが、ワタクシまえに権限エラーに対応するため、下記の通りだれでもOK!ってしていたんですが、

```javascript:だれでもOKの設定
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write;
      // allow read, write: if request.auth.uid != null;
    }
  }
}
```
これを

```javascript:認証されてるヒトだけOK
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      //allow read, write;
      allow read, write: if request.auth.uid != null;
    }
  }
}
```
にして「公開」ボタンを押せばOKです。ほどなくして反映されるようで、認証状態でないFirestoreへのアクセスにはエラーが返るようになりました。
## ナビゲーションガードなどを入れたソース
最終形のソースは
https://github.com/masatomix/todo-examples/tree/for_qiita_auth_002
へコミット済みです。
下記手順でビルドできます。

```bash
$ git clone --branch for_qiita_auth_002 https://github.com/masatomix/todo-examples.git
$ cd todo-examples/
$ npm install

src/firebaseConfig.js を自分の設定に書き換え

$ npm run dev
```
これで、http://localhost:8080/#/ にブラウザでアクセスできるとおもいます。
ログイン画面 '/login' とToDo画面 '/' の他に、'/ui001','/ui002'という画面を追加でコミットしています。/ui001だけ認証が必要な画面にしてあるので、
http://localhost:8080/#/ui001 などに直接アクセスしてみて、ログイン画面が表示され、ログインするとちゃんと行きたかった画面に遷移できること、などを確認してみてください。



## まとめ
Firebase認証をつかえば、OAuthをつかったGoogle認証機能を簡単に実装することが出来ましたね。

ザックリまとめると、、ログイン状態はVuexで管理し、ログイン時やユーザ情報の変更に従ってVuexの情報を更新します。また、画面遷移時にログイン状態をチェックするナビゲーションガードを導入することで、必要な時だけログイン画面を挟むことができるようになりました。最後にFirestoreへのアクセスを、Firebaseログインしているユーザに制限することで、データを保護することも出来ました。


以上です。おつかれさまでした。
## 関連リンク

- [Vue vuexでfirebaseのログイン保持](https://qiita.com/ErgoFriend/items/bd1bb445e185bf45e272)
- [面倒なログイン機能の実装はFirebase Authenticationに丸投げしよう](https://www.apps-gcp.com/firebase-authentication/) 概要が整理されています
- [Firebase Authentication を使って得られた知見まとめ - トークンの仕様や注意点など](https://slideship.com/users/@iktakahiro/docs/2018/01/JnhLGehLsRw4dcZ42EY3Jf/) 戻り電文のJWTの仕様が丁寧に説明されています。
- [Firebase Auth のユーザ認証機能を自前のデータベースと連携する](https://qiita.com/geerpm/items/165c31302edce1e52146) Firebaseを認証だけにつかう場合。
- [ブラウザでログインしたFirebaseのユーザー情報をサーバー側で取得する](https://uyamazak.hatenablog.com/entry/2018/05/17/180521) その2
- [Authenticating a Vue JS Application With Firebase UI](https://medium.com/dailyjs/authenticating-a-vue-js-application-with-firebase-ui-8870a3a5cff8) Firebase UI っていう、画面まで作ってくれちゃうライブラリをつかう場合。
- [Vue.jsにおけるFirebaseの主要な機能の取扱い](https://www.h-sakano.net/entry/2018/12/04/000000)
- [Vue Routerのナビゲーションガードによるアクセス制限を試した&コードを読み解いた](https://ky-yk-d.hatenablog.com/entry/2018/07/07/203000)
- [【備忘録】【Vue.js】routeを使ってログインしてないと見れないように設定する方法](https://www.katonobo.com/entry/vue-routeguard)
- [Firebase / Firestore を使って簡単な Chat を作ってみる。(JS, Vue)](https://qiita.com/Satachito/items/33e9006a96b19b18040d)
- [Vue.js 2.0 でログイン (vue-router で認証が必要な URL を定義)](https://qiita.com/takatama/items/05e9fbc7199cde4caf60)
- [Vue.js の主要な機能をざっくりとつかってみたときのメモ](https://qiita.com/masatomix/items/24142b084b745ce816a9)
- [Vue.js の主要な機能をざっくりとつかってみたときのメモ(Firebase編)](https://qiita.com/masatomix/items/7be14b8eb433edcfbfc7)
- [Vue.js の主要な機能をざっくりとつかってみたときのメモ(Firebase認証・認可編)](https://qiita.com/masatomix/items/5316acdb5980289e5cc2) 本記事