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

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

DEVELOPMENT

web
js
aws
cognito
vue

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

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

Amplifyは?

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

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

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

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

状況

  • node v10.0.0(ndenv使用)
  • vue アプリはvue cliで作成している
  • ページ遷移はvue-routerを使っている
  • 必要に応じて、store(vuex)を使う

使うライブラリ

  • amazon-cognito-identity-js
  • aws-sdk

※vue-cli での始め方は、下記を参考にして下さい。 ▶ Vue.js を vue-cli を使ってシンプルにはじめてみる

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を選択してそのコードをコピーしておきます。

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

これまでの内容は ▶ 参考Angular+Cognitoのユーザー認証付きSPAのサンプル を参考にして下さい。

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

$ 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を記載しときます

  • 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として用意していきます。

🙏よかったらシェアお願いします🙏
うかい / 株式会社UKAI
うかい@エンジニア出身CEO株式会社UKAI
Nobuyuki Ukai

株式会社UKAI 代表取締役CEO。建築を専攻していたが、新卒でYahoo!Japanにエンジニアとして入社。その後、転職、脱サラ、フリーランスエンジニアを経て、田舎へ移住し、ゲストハウスの開業、法人成り。ゲストハウス2軒、焼肉店、カフェ、食品製造業を運営しています。詳細はこちらから

ホームページ作成、ECサイト掲載、商品ブランディング等、ご依頼やご相談は


CONACT
入力して下さい
WRITTEN BY
うかい / 株式会社UKAI
うかい@エンジニア出身CEO株式会社UKAI

Nobuyuki Ukai

株式会社UKAI 代表取締役CEO。建築を専攻していたが、新卒でYahoo!Japanにエンジニアとして入社。その後、転職、脱サラ、フリーランスエンジニアを経て、田舎へ移住し、ゲストハウスの開業、法人成り。ゲストハウス2軒、焼肉店、カフェ、食品製造業を運営しています。2020年コロナウイルスの影響で、ゲストハウスとカフェを閉店。現在は、ECサイト新規リリース、運営と、黒毛和牛の牝牛ブランディングをしメディア立ち上げ。牝牛のお肉と、独自食品開発した食品をまもなく販売開始予定。詳細はこちらから

COPYRIGHT © 2021 UKAI CO., LTD. ALL RIGHTS RESERVED.