Vue.js + Firebase を使って爆速でユーザ認証を実装する

# 最初に

この記事はVue.js アドベントカレンダー#4 25日目の記事です。

この一ヶ月でVue.jsについてのナレッジが100記事増えたことになります。やったね！！

---
**2018/8/2 追記**

現在では `vue-cli` の3.x.xがリリースされており、スキャフォールドからVueプロジェクト作成の工程が若干異なりますのでご留意ください。

---


# この記事でやること

バックエンドをFirebaseに丸投げしたユーザ登録 → サインイン → サインアウト までのチュートリアルです。

![1513916508.gif](https://qiita-image-store.s3.amazonaws.com/0/67790/3c04318a-4589-f0cd-e7c4-6f8cd68a8e44.gif)


# 認証の実装は面倒

フロントエンドの技術を使ってちょっとしたアプリケーションを作った時、認証やユーザ管理を実装するのはそれなりに面倒かと思います。フレームワークの選定、DBは何使う?など決めなければならないこと、覚えなくてはならないことがたくさんありますね。

そこでBaaSを使います。

## BaaSって？

Backend as a Service のことを指します。
サービスそのものがバックエンドとしての機能を備えたサービスのことを指します。流行りの「○○ as a Service」のリテラルですね。

なんのこっちゃ分からなくても言葉に惑わされることなく、とりあえず使ってみれば理解が早いと思います。

今回は「 **Firebase** 」を使ってユーザ認証を実装していきたいと思います。

## Firebaseとは

Googleが買収したmBaaS（mobile Backend as a Service）です。

Firebaseについては色々の方がすでに言及されているので、そちらを参照のこと。

[Firebaseの始め方](https://qiita.com/kohashi/items/43ea22f61ade45972881)

[Firebaseをやめた4つの理由](https://qiita.com/ko2ic/items/3d25d41971636a28cc1c)

[それでもFirebaseを使うべき5つのメリット](https://qiita.com/hinom77/items/5e75e9646f4f815ca074#_reference-d3a43bfb8a995755b451)

成熟を待たずしてサービスや技術が批評されるのはフロントエンドあるあるっぽいです。技術を信じるお前を信じる強い心が必要です。バックエンド屋でよかった。


# 環境

せっかくなのでパッケージ管理にはyarnを使います。

```bash 
# Nodejs
$ node -v
v8.7.0
# vue-cli
$ vue -V
2.9.2
# yarn
$ yarn -v
1.3.2
```

# フロント部を実装する

まずはログイン画面など作っていきます。

## vue-cliでプロジェクトを作成

vue-cliというスキャフォールドでプロジェクトを作成して行きます。特に指定ありませんがvue-routerを使う設定だけはYesにしましょう。

ここではプロジェクト名を vue_firebase_auth_test にしています。

```bash
$ vue init webpack vue_firebase_auth_test

? Project name vue_firebase_auth_test
? Project description A Vue.js project
? Author sin_tanaka
? Vue build standalone
? Install vue-router? Yes
? Use ESLint to lint your code? Yes
? Pick an ESLint preset Standard
? Set up unit tests No
? Setup e2e tests with Nightwatch? No
? Should we run `npm install` for you after the project has been created? (recommended) ya
rn

   vue-cli · Generated "vue_firebase_auth_test".


# Installing project dependencies ...
# ========================

yarn install v1.3.2
info No lockfile found.
[1/5] 🔍  Validating package.json...
[2/5] 🔍  Resolving packages...
[3/5] 🚚  Fetching packages...
[4/5] 🔗  Linking dependencies...
[5/5] 📃  Building fresh packages...
success Saved lockfile.
✨  Done in 92.66s.


Running eslint --fix to comply with chosen preset rules...
# ========================

yarn run v1.3.2
$ eslint --ext .js,.vue src --fix
✨  Done in 12.88s.

# Project initialization finished!
# ========================

To get started:

  cd vue_firebase_auth_test
  npm run dev

Documentation can be found at https://vuejs-templates.github.io/webpack
```

いつのまにかパッケージのインストールまで終わらせくれるようになっていました。
ローカルサーバを立ち上げます。

```bash
$ cd vue_firebase_auth_test
$ yarn dev
```

![スクリーンショット 0029-12-21 午後4.57.06.png](https://qiita-image-store.s3.amazonaws.com/0/67790/0164f2ef-c4df-6ee6-e6ee-6afb8f734309.png)

この画面が表示されればOKです。

ついでにプロジェクトの構造を見ておきます。

```
$ tree -I node_modules
.
├── README.md
├── build
│   ├── build.js
│   ├── check-versions.js
│   ├── logo.png
│   ├── utils.js
│   ├── vue-loader.conf.js
│   ├── webpack.base.conf.js
│   ├── webpack.dev.conf.js
│   └── webpack.prod.conf.js
├── config
│   ├── dev.env.js
│   ├── index.js
│   └── prod.env.js
├── index.html
├── package.json
├── src
│   ├── App.vue
│   ├── assets
│   │   └── logo.png
│   ├── components
│   │   └── HelloWorld.vue
│   ├── main.js
│   └── router
│       └── index.js
├── static
└── yarn.lock
```

## vue-routerでルーティングを決める

次にルーティングを決めておきます。

認証してない状態で / にアクセスすると /signin にリダイレクト、どの画面でもsignoutしたら /signin にリダイレクトされるようなアプリとします。


| path | 説明 |
|:---|:---:|
| /  | 表示に認証が必要なコンテンツ  |
| /signup  | ユーザ登録  |
| /signin  | サインイン  |

では vue-routerでpathに対して、出力するコンポーネントを定義します。

```js:src/router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
import Signup from '@/components/Signup'
import Signin from '@/components/Signin'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'HelloWorld',
      component: HelloWorld
    },
    {
      path: '/signup',
      name: 'Signup',
      component: Signup
    },
    {
      path: '/signin',
      name: 'Signin',
      component: Signin
    }
  ]
})
```

Signin, Signupに対応するコンポーネントを作成して行きます。

## 登録画面をつくる

さくっと登録画面を作ります。

![スクリーンショット 0029-12-22 午前9.58.44.png](https://qiita-image-store.s3.amazonaws.com/0/67790/ed288dab-a505-ca94-916a-a9aa0c1cf637.png)

```html:src/components/Signup.vue
<template>
  <div class="signup">
    <h2>Sign up</h2>
    <input type="text" placeholder="Username" v-model="username">
    <input type="password" placeholder="Password" v-model="password">
    <button>Register</button>
    <p>Do you have an account? 
      <router-link to="/signin">sign in now!!</router-link>
    </p>
  </div>
</template>

<script>
export default {
  name: 'Signup',
  data () {
    return {
      username: '',
      password: ''
    }
  },
  methods: {}
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1, h2 {
  font-weight: normal;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}
.signup {
  margin-top: 20px;
  
  display: flex;
  flex-flow: column nowrap;
  justify-content: center;
  align-items: center
}
input {
  margin: 10px 0;
  padding: 10px;
}
</style>
```

## 認証画面をつくる

登録画面のコピペで認証画面もつくります。

![スクリーンショット 0029-12-22 午前9.54.20.png](https://qiita-image-store.s3.amazonaws.com/0/67790/250a0502-712b-f58b-70a3-41b95cbe155b.png)

```html:src/components/Signin.vue
<template>
  <div class="signin">
    <h2>Sign in</h2>
    <input type="text" placeholder="Username" v-model="username">
    <input type="password" placeholder="Password" v-model="password">
    <button>Signin</button>
    <p>You don't have an account? 
      <router-link to="/signup">create account now!!</router-link>
    </p>
  </div>
</template>

<script>
export default {
  name: 'Signin',
  data () {
    return {
      username: '',
      password: ''
    }
  },
  methods: {}
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1, h2 {
  font-weight: normal;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}
.signin {
  margin-top: 20px;
  
  display: flex;
  flex-flow: column nowrap;
  justify-content: center;
  align-items: center
}
input {
  margin: 10px 0;
  padding: 10px;
}
</style>
```

http://localhost:8080/#/signup
http://localhost:8080/#/signin

にアクセスして確認しておきます。

# バックエンド部を実装する

画面ができたのでバックエンドをFirebaseで設定します。

## Firebaseでプロジェクトを作成

コンソールにログインしてプロジェクトを作成します。

![スクリーンショット 0029-12-22 午前10.05.34.png](https://qiita-image-store.s3.amazonaws.com/0/67790/f3fdfbc5-83f1-4acb-14e6-8d6f9de34984.png)

こんな画面になればOKです。

![スクリーンショット 0029-12-22 午前10.03.50.png](https://qiita-image-store.s3.amazonaws.com/0/67790/ba5c3a4e-bf04-8c72-9272-31ac6c03d46c.png)

## Firebaseの設定をプロジェクトに追加する

**WebアプリにFirebaseを追加する** をクリックします。

ドキュメント: https://firebase.google.com/docs/auth/web/password-auth?authuser=1

```html
<script src="https://www.gstatic.com/firebasejs/4.8.1/firebase.js"></script>
<script>
  // Initialize Firebase
  var config = {
    apiKey: "YOUR_KEY",
    authDomain: "YOUR_DOMAIN.firebaseapp.com",
    databaseURL: "YOUR_DOMAIN.firebaseio.com",
    projectId: "YOUR_ID",
    storageBucket: "YOUR_BUCKET_ID.appspot.com",
    messagingSenderId: "YOUR_SENDER_ID"
  };
  firebase.initializeApp(config);
</script>
```

プロジェクト固有のconfigが表示されるのでjsに組み込みます。

ここでfirebaseのモジュールを組み込んでいない人はインストールしておきます。
https://firebase.google.com/docs/web/setup?authuser=0

```bash
$ yarn add firebase --save
```

```js: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 firebase from 'firebase'

Vue.config.productionTip = false

const config = {
    apiKey: 'YOUR_KEY',
    authDomain: 'YOUR_DOMAIN.firebaseapp.com',
    databaseURL: 'YOUR_DOMAIN.firebaseio.com',
    projectId: 'YOUR_ID',
    storageBucket: 'YOUR_BUCKET_ID.appspot.com',
    messagingSenderId: 'YOUR_SENDER_ID'
}
firebase.initializeApp(config);

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  template: '<App/>',
  components: { App }
})
```

## 認証の設定

またFirebase プロジェクト画面に戻って認証を有効にします。
ここではメール認証を有効にしておきます。

![スクリーンショット 0029-12-22 午前10.34.53.png](https://qiita-image-store.s3.amazonaws.com/0/67790/49a9fdc1-0c08-d43f-399f-47e7610be81a.png)

## ユーザ登録のコードを追加する

firebase.auth().createUserWithEmailAndPassword() を使い、ユーザ登録のメソッドを追加します。

```html:src/components/Signup.vue
<template>
  <div class="signup">
    <h2>Sign up</h2>
    <input type="text" placeholder="Username" v-model="username">
    <input type="password" placeholder="Password" v-model="password">
    <button @click="signUp">Register</button>
    <p>Do you have an account? 
      <router-link to="/signin">sign in now!!</router-link>
    </p>
  </div>
</template>

<script>
import firebase from 'firebase'

export default {
  name: 'Signup',
  data () {
    return {
      username: '',
      password: ''
    }
  },
  methods: {
    signUp: function () {
      firebase.auth().createUserWithEmailAndPassword(this.username, this.password)
        .then(user => {
          alert('Create account: ', user.email)
        })
        .catch(error => {
          alert(error.message)
        })
    }
  }
}
</script>

<style scoped>
 /* 省略 */
</style>
```

ユーザ登録を試してみます。

![1513916508.gif](https://qiita-image-store.s3.amazonaws.com/0/67790/3c04318a-4589-f0cd-e7c4-6f8cd68a8e44.gif)

できました！

## ユーザ認証のコードを組み込む

firebase.auth().signInWithEmailAndPassword()を使い、認証のメソッドを追加します。

```html:src/components/Signin.vue
<template>
  <div class="signin">
    <h2>Sign in</h2>
    <input type="text" placeholder="Username" v-model="username">
    <input type="password" placeholder="Password" v-model="password">
    <button @click="signIn">Signin</button>
    <p>You don't have an account? 
      <router-link to="/signup">create account now!!</router-link>
    </p>
  </div>
</template>

<script>
import firebase from 'firebase'

export default {
  name: 'Signin',
  data: function () {
    return {
      username: '',
      password: ''
    }
  },
  methods: {
    signIn: function () {
      firebase.auth().signInWithEmailAndPassword(this.username, this.password).then(
        user => {
          alert('Success!')
          this.$router.push('/')
        },
        err => {
          alert(err.message)
        }
      )
    }
  }
}
</script>

<style scoped>
 /* 省略 */
</style>
```
認証 => ルートへリダイレクトまでやってみます。

![1513909477.gif](https://qiita-image-store.s3.amazonaws.com/0/67790/de618b4f-3427-cc4f-14e6-01e9d14e1fd0.gif)

できましたね！！

## vue-routerでリダイレクトの設定をする

メタフィールドを設定します。
> ルートの定義をする際に meta フィールドを含めることができます。
引用: [ルートメタフィールド](https://router.vuejs.org/ja/advanced/meta.html)


サンプルの通りですが、requiresAuthをメタフィールドに定義しておきます。

また、Firebase側から、現在のユーザを取得し、 `currentUser && requiresAuth` ならアクセス可、それ以外は /signinへリダイレクトされるようにしましょう。

> currentUser プロパティを使用しても、現在ログインしているユーザーを取得できます。ユーザーがログインしていない場合、currentUser は null です。

firebase.auth().currentUser から現在のユーザを取得できます。

```js:src/router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
import Signup from '@/components/Signup'
import Signin from '@/components/Signin'
import firebase from 'firebase'

Vue.use(Router)

let router = new Router({
  routes: [
    {
      path: '*',
      redirect: 'signin'
    },
    {
      path: '/',
      name: 'HelloWorld',
      component: HelloWorld,
      meta: { requiresAuth: true }
    },
    {
      path: '/signup',
      name: 'Signup',
      component: Signup
    },
    {
      path: '/signin',
      name: 'Signin',
      component: Signin
    }
  ]
})

router.beforeEach((to, from, next) => {
  let requiresAuth = to.matched.some(record => record.meta.requiresAuth)
  let currentUser = firebase.auth().currentUser
  if (requiresAuth) {
    // このルートはログインされているかどうか認証が必要です。
    // もしされていないならば、ログインページにリダイレクトします。
    if (!currentUser) {
      next({
        path: '/signin',
        query: { redirect: to.fullPath }
      })
    } else {
      next()
    }
  } else {
    next() // next() を常に呼び出すようにしてください!
  }
})

export default router
```

なお、推奨されているのは以下の方法のようです。
この辺力及ばずよく分からず。。なんとなく変更を監視するのはわかるのですが。。

> 現在ログインしているユーザーを取得するには、Auth オブジェクトでオブザーバーを設定することをおすすめします。
引用: [Firebase でユーザーを管理する](https://firebase.google.com/docs/auth/web/manage-users?authuser=1)


---
2018/8/2 追記

推奨されている方法について、@makotoyc さんからコメントいただきました。ありがとうございます！（まるっと転記させていただきます…）

`firebase.auth().onAuthStateChanged()` のコールバックでログインしているユーザを取得し、ユーザの有無で処理を分ける方法が良さそうです

```js
【略】

router.beforeEach((to, from, next) => {
  const requiresAuth = to.matched.some(record => record.meta.requiresAuth)
  if (requiresAuth) {
    // このルートはログインされているかどうか認証が必要です。
    // もしされていないならば、ログインページにリダイレクトします。
    firebase.auth().onAuthStateChanged(function (user) {
      if (user) {
        next()
      } else {
        next({
          path: '/signin',
          query: { redirect: to.fullPath }
        })
      }
    })
  } else {
    next() // next() を常に呼び出すようにしてください!
  }
})

【略】
```


---
HelloWorldコンポーネントにサインアウトボタンと Welcome メッセージを追加します。

```html:src/components/HelloWorld.vue
<template>
  <div class="hello">
    <h1>Hello {{ name }}!!</h1>
    <h1>{{ msg }}</h1>
    <h2>Essential Links</h2>
    <ul>
      <li><a href="https://vuejs.org" target="_blank">Core Docs</a></li>
      <li><a href="https://forum.vuejs.org" target="_blank">Forum</a></li>
      <li><a href="https://chat.vuejs.org" target="_blank">Community Chat</a></li>
      <li><a href="https://twitter.com/vuejs" target="_blank">Twitter</a></li>
      <br>
      <li><a href="http://vuejs-templates.github.io/webpack/" target="_blank">Docs for This Template</a></li>
    </ul>
    <h2>Ecosystem</h2>
    <ul>
      <li><a href="http://router.vuejs.org/" target="_blank">vue-router</a></li>
      <li><a href="http://vuex.vuejs.org/" target="_blank">vuex</a></li>
      <li><a href="http://vue-loader.vuejs.org/" target="_blank">vue-loader</a></li>
      <li><a href="https://github.com/vuejs/awesome-vue" target="_blank">awesome-vue</a></li>
    </ul>
    <button @click="signOut">Sign out</button>
  </div>
</template>

<script>
import firebase from 'firebase'

export default {
  name: 'HelloWorld',
  data () {
    return {
      msg: 'Welcome to Your Vue.js App',
      name: firebase.auth().currentUser.email
    }
  },
  methods: {
    signOut: function () {
      firebase.auth().signOut().then(() => {
        this.$router.push('/signin')
      })
    }
  }
}
</script>

<style scoped>
 /* 省略 */
</style>
```

完成です！

# まとめ

いかがだったでしょうか。バックエンドをFirebaseに任せることでかなり簡単にユーザ認証を実装できました。


余談ですが、記事書いてる途中で、ほぼおんなじテーマで記事が公開され、しかも筆者はVue.jsのコミッターやないかい、ということがありました。びっくりすることにタイトルも考えていたものとほぼ同じでした。決してパクリではなく、、

[Nuxt.jsとFirebaseを組み合わせて爆速でWebアプリケーションを構築する](https://qiita.com/potato4d/items/cfddeb8732fec63cb29c) 

Nuxt.jsはほんと使いやすいです。SSRできるけどSPAモードあるし、わりとNuxt.jsでいいんじゃないか感はあります。

# 参考

Firebase リファレンス: https://firebase.google.com/docs/auth/web/start?authuser=1
vue-router - ナビゲーションガード https://router.vuejs.org/ja/advanced/navigation-guards.html
vue-router - ルートメタフィールド https://router.vuejs.org/ja/advanced/meta.html