VueにAWS Cognitoのログインをamplifyを使わないで作り込む

目次

最近ではちょっとしたwebアプリなら、サーバーサイドを使わずに、Node環境でReactやVueなどを使った開発が多くなってきました。 しかし、SPAではなく、ちゃんとしたショッピングサイト的なものを作ろうと思うと、どうしてもログイン機能などが必要になって来ると思います。

せっかくならクライアントサイドだけの開発でサーバーサイドはクラウドサービスにお任せしたくなってきましたので、ログインも便利なCognitoにおまかせして実装してみようということで、やってみました。



Amplifyは?

最近良く耳にしたり、記事を目にしたりすることが多いですが、個人的には、まだまだ怖いので、使いたくない印象です。というのも、勝手に作成されるIAMに、ルートのスーパーユーザーの権限を与えたり、見えないところで勝手に色々設定をされるので、ちょっと嫌だなーという印象を受けたので、まだ実サービスでは、使うのはやめておこうというところです。

なので、今回は、Cognitoの設定も自分でして、ログインや認証処理等も下記のライブラリを使って自前で実装しますよ。Vueになれるためにもちょうどいいかもしれません。

今回、cognitoの認証部分等、こちらを参考にさせていただきました。

https://qiita.com/daikiojm/items/b02c19cfea6766c308ca

状況

使うライブラリ

※vue-cli での始め方は、https://qiita.com/567000/items/dde495d6a8ad1c25fa43 を参考にしてください。

AWS上Cognitoの設定

ユーザープールの作成

  1. AWSコンソールにログイン
  2. Cognitoへ
  3. 「ユーザープールの管理」をクリック
  4. 右上「ユーザープールを作成する」をクリック
  5. 好きなプール名を入力して「デフォルトを確認する」をクリック
  6. 左のメニューの属性をクリックし、必要に応じて設定を変更 ユーザープール作成
  7. 左メニューのポリシーでは、パスワードの強度の要求の設定や、有効期限を設定します。また、ユーザーが自分でユーザー登録できるようにするには、ユーザーに自己サインアップを許可するを選択します
  8. MFAを使うなら、MFAそして確認メニューで設定します
  9. 実稼働になったら、メッセージのカスタマイズが必要になりそうです。サインアップやパスワードリセット等の際、送られるメールの本文を編集できます
  10. ユーザーのデバイスを記憶するなら、デバイスの設定から確認してください
  11. アプリクライアントで、アプリクライアントの追加をします。「クライアントのシークレット」のチェックを外します、あとは、そのまま作成してください。
  12. これで、プールの作成をします。

ここまでで、 プールIDと、アプリクライアントIDをメモっておきます。


フェデレーティッドアイデンティティの作成をします

  1. フェデレーティッドアイデンティティの作成
  2. 新しいプールIDの作成
  3. プール名と、「認証されていない ID に対してアクセスを有効にする」にチェック プールID作成
  4. 認証プロバイダーのCognitoの部分で、先程のメモのユーザープールIDと、アプリクライアントIDを入れる 認証プロバイダ
  5. 次に進み、詳細を表示から、そのままロールを作成します。
  6. 作成後表示されるコードをJavascriptを選択してそのコードをコピーしておきます。

ここまで来たらクライアント側に行きましょう

これまでの内容は https://qiita.com/daikiojm/items/c90e9eb7b19f0711c829 も参考にしてみてください。

ライブラリのインストール

$ yarn add aws-sdk
$ yarn add amazon-cognito-identity-js

cognito設定ファイル

先程メモした設定ファイルを用意します

src/congito.js

export default {
  AWSConfig: {
    Region: 'ap-northeast-1',
    UserPoolId: 'ap-northeast-1_XXXXXXXXX',
    ClientId: 'YYYYYYYYYYYYYYYYYYYYYYYYYY',
    IdentityPoolId: 'ap-northeast-1:XXXXXXXX-YYYY-XXXX-YYYY-XXXXXXXXXXXX'
  }
}

こんな感じで設定しました。

Cognitoの処理ファイル

Cognito関連(signin, signout, signup, password reset等)の処理をプラグインとしてファイルを作成します。 参考にした記事のもと、src/cognito/以下にファイルを用意します

src/cognito/cognito.js

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

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
    this.currentUser = false
  }

  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)
        }
      }
    })
  }

  // サインアップ
  signUp (username, password) {
    const name = { Name: 'name', Value: username }
    const email = { Name: 'email', Value: username }
    const now = Math.floor(new Date().getTime() / 1000)
    const upatedAt = { Name: 'updated_at', Value: String(now) }

    const attributeList = []
    attributeList.push(new CognitoUserAttribute(name))
    attributeList.push(new CognitoUserAttribute(email))
    attributeList.push(new CognitoUserAttribute(upatedAt))

    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)
        }
      })
    })
  }

  // サインイン
  signin (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)
        }
      })
    })
  }

  // サインアウト
  signout () {
    if (this.userPool.getCurrentUser()) {
      this.userPool.getCurrentUser().signOut()
    }
  }

  // 認証ずみかどうか
  isAuthenticated () {
    this.currentUser = this.userPool.getCurrentUser()
    return new Promise((resolve, reject) => {
      if (this.currentUser === null) { reject(this.currentUser) }
      this.currentUser.getSession((err, session) => {
        if (err) {
          reject(err)
        } else {
          if (!session.isValid()) {
            reject(session)
          } else {
            resolve(session)
          }
        }
      })
    })
  }

  // 属性の取得
  getAttribute () {
    return new Promise((resolve, reject) => {
      this.currentUser.getUserAttributes((err, result) => {
        if (err) {
          reject(err)
        }
        else {
          resolve(result)
        }
      })
    })
  }

  // コードの再送
  resentCode () {
    return new Promise((resolve, reject) => {
      this.currentUser.getAttributeVerificationCode("email", {
        onSuccess: (result) => {
          console.log('success getAttributeVerificationCode')
          resolve(result)
        },
        onFailure: (err) => {
          console.log('failed getAttributeVerificationCode: ' + JSON.stringify(err))
          reject(err)
        }
      })
    })
  }

  // Eメールアドレス変更後 emailを有効可する
  verifyAttribute (confirmationCode) {
    return new Promise((resolve, reject) => {
      this.currentUser.verifyAttribute("email", confirmationCode, {
        onSuccess: (result) => {
          console.log('email verification success')
          var user = store.getters.user
          user["email_verified"] = "true"
          store.commit('setUser', user)

          resolve(result)
        },
        onFailure: (err) => {
          console.log('email verification failed')
          reject(err)
        }
      })
    })
  }

  // Eメールアドレスの更新
  updateEmailAddress (email) {
    let attributes = {
      'email': email,
      'name': email
    }
    return new Promise((resolve, reject) => {
      this.updateAttributes(attributes)
        .then(result => { // eslint-disable-line
          resolve(result)
          var user = store.getters.user
          user["email_verified"] = "false"
          store.commit('setUser', user)
        })
        .catch(err => {
          reject(err)
        })
    })
  }

  // パスワード更新
  updatePassword(oldPassword, newPassword) {
    return new Promise((resolve, reject) => {
      this.currentUser.changePassword(oldPassword, newPassword, (err, result) => {
        if (err) {
          reject(err)
        }
        else {
          resolve(result)
        }
      })
    })
  }

  // パスワード忘れメール送信
  forgetPassword(username) {
    const userData = { Username: username, Pool: this.userPool }
    const cognitoUser = new CognitoUser(userData)
    return new Promise((resolve, reject) => {
      cognitoUser.forgotPassword({
        onSuccess: (result) => {
          console.log('email verification success')
          resolve(result)
        },
        onFailure: (err) => {
          console.log('email verification failed')
          reject(err)
        }
      })
    })
  }

  // パスワードリセット
  resetPassword(username, newPassword, code) {
    const userData = { Username: username, Pool: this.userPool }
    const cognitoUser = new CognitoUser(userData)
    return new Promise((resolve, reject) => {
      cognitoUser.confirmPassword(code, newPassword, {
        onSuccess: (result) => {
          console.log('password reset success')
          resolve(result)
        },
        onFailure: (err) => {
          console.log('password reset failed')
          reject(err)
        }
      })
    })
  }

  // プロフィール更新
  updateAttributes (attributes) {
    const attributeList = []
    for(var key in attributes) {
      const attribute = { Name: key, Value: attributes[key] }
      attributeList.push(new CognitoUserAttribute(attribute))
    }
    return new Promise((resolve, reject) => {
      if (this.currentUser === null) { reject(this.currentUser) }

      // update attributes
      this.currentUser.updateAttributes(attributeList, (err, result) => {
        if (err) {
          reject(err)
        } else {
          var user = store.getters.user
          for(var key in attributes) {
            user[key] = attributes[key]
          }
          store.commit('setUser', user)
          resolve(result)
        }
      })
    })
  }
}

大事なところは、installメソッドでVueプラグインとして記述している部分です。 また、ちょこちょこstoreが出てきますが、vuexでstoreを用意してます。

index.js

import Vue from 'vue'
import Cognito from './cognito'
import config from './../config'

Vue.use(Cognito, config.AWSConfig)

export default new Cognito()

cognito関連処理をvueのプラグインとして読みます src/main.js

import Vue from 'vue'
import App from './App'
import router from './router'
import cognito from './cognito'
import store from './store'

Vue.config.productionTip = false

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

src/store.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
      user: null,
  },
  getters: {
    user: state => state.user,
  },
  mutations: {
    // ユーザー情報保存
    setUser(state, user) {
      state.user = user
    }
  }
});

export default store

ログイン状況や、プロフィール内容をstoreで保持しています。

次にルーターです

src/router.js

import Vue from 'vue'
import Router from 'vue-router'
import cognito from './cognito'
import store from './store'
import {
  Signin,
  Signup,
  Confirm,
  ForgetPassword,
  ResetPassword,
  Mypage,
  Top,
  About,
} from './components/pages'

Vue.use(Router)

const signout = (to, from, next) => {
  cognito.signout()
  next('/signin')
}

const router = new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'top',
      component: Top
    },
    {
      path: '/about',
      name: 'about',
      component: About,
      meta: { requiredAuth: true }
    },
    {
      path: '/signin',
      name: 'Signin',
      component: Signin,
      meta: { notRequiredAuth: true }
    },
    {
      path: '/reset-password',
      name: 'ResetPassword',
      component: ResetPassword,
      meta: { notRequiredAuth: true }
    },
    {
      path: '/forget-password',
      name: 'ForgetPassword',
      component: ForgetPassword,
      meta: { notRequiredAuth: true }
    },
    {
      path: '/signup',
      name: 'Signup',
      component: Signup,
      meta: { notRequiredAuth: false }
    },
    {
      path: '/confirm',
      name: 'Confirm',
      component: Confirm,
      meta: { notRequiredAuth: false }
    },
    {
      path: '/mypage',
      name: 'Mypage',
      component: Mypage,
      meta: { requiredAuth: true }
    },
    {
      path: '/signout',
      beforeEnter: signout,
    },
    {
      path: '*',
      component: NotFound
    },
  ]
})

router.beforeResolve(async(to, from, next) => {
  // Get signin session.
  cognito.isAuthenticated()
    .then(session => { // eslint-disable-line
      const token = session.idToken.jwtToken
      // get attribute.
      cognito.getAttribute()
        .then(result => {
          var user = {}
          for(var v of result) {
            user[v.getName()] = v.getValue()
          }
          user['token'] = token
          store.commit('setUser', user)
        })
        .catch(error => {
          store.commit('setUser', null)
          console.log(error)
          signout(to, from, next)
        })
      if (to.matched.some(record => record.meta.notRequiredAuth)) {
        next({
          path: '/',
        })
      }
    })
    .catch(error => { // eslint-disable-line
      store.commit('setUser', null)
      if (to.matched.some(record => record.meta.requiredAuth)) {
        next({
          path: '/signin',
          query: { redirect: to.fullPath }
        })
      }
    })
  next()
})

export default router

beforeResolveにて、毎回sessionのチェックをしてログイン状態であれば、プロフィール情報やtokenを取得してます。

Sign in

参考までに、signin.vueを記載しときます

<template>
  <div class="signin">
    <b-container fluid>
      <b-row>
        <b-col offset-md="4" md="4">
          <h3>{{ $t('signin.title') }}</h3>

          <b-form>
            <b-alert v-if="error" show variant="danger">{{ $t('signin.signin-error') }}</b-alert>

            <b-form-group
              :label="$t('signin.email')"
              label-for="email"
              :invalid-feedback="invalidEmailFeedback"
              :valid-feedback="validEmailFeedback"
              :state="emailValidation"
            >
              <b-form-input id="email" :placeholder="$t('signin.email-placeholder')" v-model="email" :state="emailValidation" trim></b-form-input>
            </b-form-group>

            <b-form-group
              :label="$t('signin.password')"
              label-for="password"
              :invalid-feedback="invalidPasswordFeedback"
              :valid-feedback="validPasswordFeedback"
              :state="passwordValidation"
            >
              <b-form-input type="password" id="password" :placeholder="$t('signin.password-placeholder')" v-model="password" :state="passwordValidation" trim></b-form-input>
            </b-form-group>

            <b-button type="button" variant="outline-dark" @click="signin">
              {{ $t('signin.title') }}
              <span v-if="loading" class="spinner-grow spinner-grow-sm" role="status" aria-hidden="true"></span>
            </b-button>

            <div class="forget-password">
              <font-awesome-icon icon="lock" />
              <b-link to="/forget-password"> {{ $t('signin.forget-password') }}</b-link>
            </div>
          </b-form>
        </b-col>
        <b-col md="4" />
      </b-row>
    </b-container>
  </div>
</template>

<script>
export default {
  name: 'Signin',
  data () {
    return {
      email: '',
      password: '',
      error: false,
      loading: false
    }
  },
  computed: {
    emailValidation() {
      return this.validEmail()
    },
    invalidEmailFeedback() {
      return this.$t('signin.email-error')
    },
    validEmailFeedback() {
      return this.emailValidation === true ? 'OK' : 'InValid'
    },

    passwordValidation() {
      return this.validPassowrd()
    },
    invalidPasswordFeedback() {
      return this.$t('signin.password-error')
    },
    validPasswordFeedback() {
      return this.passwordValidation === true ? 'OK' : 'InValid'
    }
  },
  methods: {
    signin () {
      if (this.emailValidation && this.passwordValidation) {
        this.loading = true
        this.$cognito.signin(this.email, this.password)
          .then(result => { // eslint-disable-line
            var url = this.$route.query.redirect
            if (url) {
              this.$router.replace(url)
            }
            else {
              this.$router.replace('/')
            }
          })
          .catch(err => {
            console.log(err)
            this.error = true
            this.loading = false
          })
      }
      else {
        this.error = true
      }
    },
    validEmail () {
      var re = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/
      return this.email ? re.test(this.email) : null
    },
    validPassowrd() {
      return this.password ? this.password.length > 7 : null
    }
  }
}
</script>

<style lang="scss">
.signin {
  h3 {
    margin-bottom: 30px;
  }
  .forget-password {
    margin-top: 20px;
    font-size: 15px;
  }
}
</style>

VueのBootstrapを使ってます。とi18nで多言語対応もしてます。 その他のsignup等も同じ要領でcomponentとして用意していきます。

U-chan ( Nobuyuki Ukai )

学生時代は建築やデザインを専攻していたが、Yahoo!Japanにエンジニアとして運良く入社し、2年半で波情報を配信する波伝説に転職。3年後、Yahoo!時代の先輩の立ち上げたベンチャーに転職。数年後、伊豆下田に移住し、ゲストハウスを開業しながらリモートでエンジニアを続けたが、焼肉店の開業とともに株式会社UKAIを立ち上げ、法人成り。その後、カフェとゲストハウスをもう一軒開業し、現在は焼肉店、カフェ、ゲストハウス2件目を運営。今季は自社Webサイトの立ち上げ予定!

comments powered by Disqus