vue-cliで作成したSPAにシンプルにCognitoログインを組み込む

この記事は、[Vue.js Advent Calendar 2017](https://qiita.com/advent-calendar/2017/vue) 13日目の記事です。

vue-cliを使って作成されたプロジェクトにCognitoを使ったユーザーログイン機能を組み込んでみたいと思います。

# 前提
## 環境

```bash
$ node -v
v8.1.3
$ vue -V
2.9.2
```

## 使用したライブラリなど

* amazon-cognito-identity-js 1.28.0
* aws-sdk 2.168.0


# 手順

以下の手順では、vue-cliで`vue init`した状態のプロジェクトから変更・追加のあるファイルについて実際のコードと、説明を記載していきます。記載のないものに関しては、手を加えていません。

## プロジェクトのセットアップ

まずは、vue-cliでプロジェクトを作成します。
(vue-routerを使用するようにセットアップ)

```bash
$ vue init webpack
...
```

次に必要なパッケージをインストールします。

```bash
$ npm i aws-sdk --save
$ npm i amazon-cognito-identity-js --save
```

[amazon-cognito-identity-js](https://github.com/aws/amazon-cognito-identity-js)はJavaScriptからCognitoを使い際には定番のライブラリです。

Cognito UserPoolなどのAWSに関する情報を設定ファイルに記述します。
今回は、`src/config.js`というファイルを作成しました。

```js:src/config.js
export default {
  Region: 'ap-northeast-1',
  UserPoolId: 'ap-northeast-1_XXXXXXXXX',
  ClientId: 'YYYYYYYYYYYYYYYYYYYYYYYYYY',
  IdentityPoolId: 'ap-northeast-1:XXXXXXXX-YYYY-XXXX-YYYY-XXXXXXXXXXXX'
}
```

Cognito User Pool、Cognito Identity Pool の作成に関しては、次の記事を参考にしてください。
[Angular+Cognitoのユーザー認証付きSPAのサンプル](https://qiita.com/daikiojm/items/c90e9eb7b19f0711c829)


## Cognitoサービス

cognito関連の処理をプラグインとして実装するため、新しくファイルを作成します。
今回は、`src/cognito`以下に次の2ファイルを作成しました。

```js:src/cognito/cognito.js
import {
  CognitoUserPool,
  CognitoUser,
  AuthenticationDetails,
  CognitoUserAttribute
} from 'amazon-cognito-identity-js'
import { Config, CognitoIdentityCredentials } from 'aws-sdk'

export default class Cognito {
  configure (config) {
    if (config.userPool) {
      this.userPool = config.userPool
    } else {
      this.userPool = new CognitoUserPool({
        UserPoolId: config.UserPoolId,
        ClientId: config.ClientId
      })
    }
    Config.region = config.region
    Config.credentials = new CognitoIdentityCredentials({
      IdentityPoolId: config.IdentityPoolId
    })
    this.options = config
  }

  static install = (Vue, options) => {
    Object.defineProperty(Vue.prototype, '$cognito', {
      get () { return this.$root._cognito }
    })

    Vue.mixin({
      beforeCreate () {
        if (this.$options.cognito) {
          this._cognito = this.$options.cognito
          this._cognito.configure(options)
        }
      }
    })
  }

  /**
   * username, passwordでサインアップ
   * username = emailとしてUserAttributeにも登録
   */
  signUp (username, password) {
    const dataEmail = { Name: 'email', Value: username }
    const attributeList = []
    attributeList.push(new CognitoUserAttribute(dataEmail))
    return new Promise((resolve, reject) => {
      this.userPool.signUp(username, password, attributeList, null, (err, result) => {
        if (err) {
          reject(err)
        } else {
          resolve(result)
        }
      })
    })
  }

  /**
   * 確認コードからユーザーを有効化する
   */
  confirmation (username, confirmationCode) {
    const userData = { Username: username, Pool: this.userPool }
    const cognitoUser = new CognitoUser(userData)
    return new Promise((resolve, reject) => {
      cognitoUser.confirmRegistration(confirmationCode, true, (err, result) => {
        if (err) {
          reject(err)
        } else {
          resolve(result)
        }
      })
    })
  }

  /**
   * username, passwordでログイン
   */
  login (username, password) {
    const userData = { Username: username, Pool: this.userPool }
    const cognitoUser = new CognitoUser(userData)
    const authenticationData = { Username: username, Password: password }
    const authenticationDetails = new AuthenticationDetails(authenticationData)
    return new Promise((resolve, reject) => {
      cognitoUser.authenticateUser(authenticationDetails, {
        onSuccess: (result) => {
          // 実際にはクレデンシャルなどをここで取得する(今回は省略)
          resolve(result)
        },
        onFailure: (err) => {
          reject(err)
        }
      })
    })
  }

  /**
   * ログアウト
   */
  logout () {
    this.userPool.getCurrentUser().signOut()
  }

  /**
   * ログインしているかの判定
   */
  isAuthenticated () {
    const cognitoUser = this.userPool.getCurrentUser()
    return new Promise((resolve, reject) => {
      if (cognitoUser === null) { reject(cognitoUser) }
      cognitoUser.getSession((err, session) => {
        if (err) {
          reject(err)
        } else {
          if (!session.isValid()) {
            reject(session)
          } else {
            resolve(session)
          }
        }
      })
    })
  }
}
```

上記では、登録、確認コードからの承認、ログイン、ログアウト、セッションの確認のロジックを実装しています。
基本的には、[amazon-cognito-identity-js](https://github.com/aws/amazon-cognito-identity-js)で用意されているメソッドをPromiseでラップしているだけです。
注意する点は、`install`メソッドでVueプラグインとして記述している点です。
詳細は、以下の記事が参考になりました。

* [プラグイン -Vue.js](https://jp.vuejs.org/v2/guide/plugins.html)
* [SPA ダケジャナイ Vue.js！汎用ライブラリを Vue で作る](http://chibinowa.net/note/vuejs/vue-18.html)


index.jsは次のようになります。

```js:src/cognito/index.js
import Vue from 'vue'
import Cognito from './cognito'
import config from './../config'

Vue.use(Cognito, config)

export default new Cognito()
```

上記で作成したcognito関連の処理をプラグインとしてVueインスタンスに登録します。
main.jsを次のように編集します。

```js:src/main.js
import Vue from 'vue'
import App from './App'
import router from './router'
import cognito from './cognito'

Vue.config.productionTip = false

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

# コンポーネント

## ログイン

ログイン画面は次のようになります。

```html:src/components/Login.vue
<template>
  <div class="login">
    <h2>ログイン</h2>
    <form @submit.prevent="login">
      <div>
        ユーザー名:
        <input type="text" placeholder="username" v-model="username" required>
      </div>
      <div>
        パスワード:
        <input type="password" placeholder="password" v-model="password" required>
      </div>
      <button>ログイン</button>
    </form>
    <router-link to="/confirm">確認コード入力</router-link>
    <router-link to="/singup">ユーザー登録</router-link>
  </div>
</template>

<script>
export default {
  name: 'Login',
  data () {
    return {
      username: '',
      password: ''
    }
  },
  methods: {
    login () {
      this.$cognito.login(this.username, this.password)
        .then(result => {
          this.$router.replace('/home')
        })
        .then(err => {
          this.error = err
        })
    }
  }
}
</script>
...
```

cognito.jsに実装した`login ()`メソッドにフォームから取得したユーザー名(メールアドレス)とパスワードを渡します。
ログインに成功した場合、ここではホーム画面(HelloWorld)に遷移させています。

## ユーザー登録

ユーザー登録画面は次のようになります。

```html:src/components/Signup.vue
<template>
  <div class="signup">
    <h2>ユーザー登録</h2>
    <form @submit.prevent="singup">
      <div>
        メール:
        <input type="text" placeholder="メール" v-model="username" required>
      </div>
      <div>
        パスワード:
        <input type="password" placeholder="パスワード" v-model="password" required>
      </div>
      <div>
        パスワード(確認):
        <input type="password" placeholder="パスワード(確認)" v-model="passwordConfirm" required>
      </div>
      <button>登録</button>
    </form>
    <router-link to="/login">ログイン</router-link>
    <router-link to="/confirm">確認コード入力</router-link>
  </div>
</template>

<script>
export default {
  name: 'Signup',
  data () {
    return {
      username: '',
      password: '',
      passwordConfirm: ''
    }
  },
  methods: {
    singup () {
      if (this.username && (this.password === this.passwordConfirm)) {
        this.$cognito.signUp(this.username, this.password)
          .then(resutl => {
            // 登録に成功したら、確認コードの入力画面を表示
            this.$router.replace('/confirm')
          })
          .catch(err => {
            console.log(err)
          })
      }
    }
  }
}
</script>
...
```

ログインに成功した場合、確認コードの入力画面に遷移させています。

## 確認コードの入力

Cognitoでは、ユーザーアカウント確認のフローに幾つかの種類がありますが、今回は登録メールアドレスに、確認コードを送信し、
確認コード入力画面から確認をするというフローでユーザーを登録します。

確認コードの入力画面は次のようになります。


```html:src/components/Confirm.vue
<template>
  <div class="confirm">
    <h2>確認コード入力</h2>
    <form @submit.prevent="confirm">
      <div>
        メール:
        <input type="text" placeholder="メール" v-model="username" required>
      </div>
      <div>
        パスワード:
        <input type="text" placeholder="確認コード" v-model="confirmationCode" required>
      </div>
      <button>確認</button>
    </form>
    <router-link to="/login">ログイン</router-link>
    <router-link to="/singup">ユーザー登録</router-link>
  </div>
</template>

<script>
export default {
  name: 'Confirm',
  data () {
    return {
      username: '',
      confirmationCode: ''
    }
  },
  methods: {
    confirm () {
      this.$cognito.confirmation(this.username, this.confirmationCode)
        .then(result => {
          this.$router.replace('/login')
        })
        .then(err => {
          this.error = err
        })
    }
  }
}
</script>
...
```


# ルーター

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

Vue.use(Router)

const requireAuth = (to, from, next) => {
  cognito.isAuthenticated()
    .then(session => {
      next()
    })
    .catch(session => {
      next({
        path: '/login',
        query: { redirect: to.fullPath }
      })
    })
}

const logout = (to, from, next) => {
  cognito.logout()
  next('/login')
}

export default new Router({
  mode: 'history',
  routes: [
    { path: '/',
      redirect: 'home'
    },
    {
      path: '/home',
      name: 'HelloWorld',
      component: HelloWorld,
      beforeEnter: requireAuth
    },
    {
      path: '/login',
      name: 'Login',
      component: Login
    },
    {
      path: '/singup',
      name: 'Signup',
      component: Signup
    },
    {
      path: '/confirm',
      name: 'Confirm',
      component: Confirm
    },
    { path: '/logout',
      beforeEnter: logout
    }
  ]
})
```

未ログインのユーザーが`HelloWorld`(ルート)へアクセスするのを許可しない、logoutへのアクセスがあったさいに`logout ()`メソッドを実行するという処理には、vue-routerの[ナビゲーションガード](https://router.vuejs.org/ja/advanced/navigation-guards.html)(コンポーネント内ガード)を使っています。

以上で、最低限の機能を組み込むことができました。
実際に使用する際には、ユーザー登録時にusername,password以外の情報を登録する、ログイン後、session情報からAPIコールに必要なidTokenを取得するなどの機能を追加指定必要があるかと思います。

# 終わりに

このように[amazon-cognito-identity-js](https://github.com/aws/amazon-cognito-identity-js)を使うことで比較的簡単にCognitoによるユーザーログイン機能を実現することができます。
今回はログイン部分のみの実装だったため、Vuexは使いませんでしたが、Vuexを使うことを前提としたアプリケーションでは、AWS SDKによる非同期処理部分はStoreのAction内で行ったほうが全体の構成がシンプルになるかと思います。
ざっくりとした内容になってしまいましたが、以上で終わりたいと思います。

明日は、 @SatoTakumiさんです。