Cognito + S3 + AWS Amplify + Vue.js でユーザー登録 / ログイン機能をサクッと作成

概要

インターネット界隈で Google の Firebase が注目を集める今日このごろ、AWS Amplify を使って、ユーザー登録 / ログイン機能を雑に試してみた。

激ショボ。

構成要素

Amazon Cognito

Amazon Cognito(アイデンティティおよびデータ同期) | AWS

認証基盤を提供する AWS のサービス。

下記の 3 要素からなる。

  • Cognito User Pools
    • ユーザー情報の DB
    • Facebook、Google アカウント等と連携可能
  • Cognito ID プール (フェデレーティッドアイデンティティ)
    • 認証された ID に対して、IAM ロールを使うための一時的な認証情報を付与する仕組み
    • Cognito ユーザープール、Facebook、Google アカウントなどを ID として使用
  • Cognito Sync (deprecated 扱い?)
    • ユーザー情報を記録する DB や複数端末間で同期するための仕組みを提供
    • Amazon Cognito Sync を初めて使用する場合は、AWS AppSync を使用してください

正直、上 2 つは 30 回くらい説明読んでも違いを明確に説明できる気がしない。

AWS Amplify

AWS Amplify: The foundation for your cloud-powered mobile & web apps

Cognito を JavaScript やスマホアプリからサクッと使用するための AWS 公式のライブラリ。

昔は「Amazon Cognito Identity SDK for JavaScript」という渋い名前のライブラリを公式としていたが、 Firebase に影響されたのか、「Amplify」とイケてる感じに改めたらしい。

Vue.js

Vue.js

所謂フロントエンド JS のライブラリ。UI 周りを作るためのもの。
最近は名前を目にしない日がないくらい流行ってる。

それはそうと、React って難しいよね。

コード

cloudfront_s3_with_cognito_auth_sample/cognito_self_ui at master · hiraro/cloudfront_s3_with_cognito_auth_sample

作り方

  • 詳細は、前述のコードなどを参照していただけたら幸いです
    • 筆者は Vue.js も最近のフロントエンド周りも全部素人です

前提

  • Node.js インストール済み
  • Vue CLI 3 を使用
  • S3 でホスティングする都合上、SPA (single-page application) でなく、MPA (multi-page application、要は普通の Web ページのような感じ) として作成

Cognito ユーザープール作成

認証用のユーザープール。

とりあえず、デフォルト設定で作成。

ここの属性周りの設定は作成時しか設定できない (後から変更できない) ので、 実運用時は慎重に設定を検討しよう。

作成後にアプリクライアントを作成して、このユーザープールを使うアプリの名前や設定を指定。
アプリクライアント ID は後ほど使うのでメモっておく。

Web アプリから使う場合は、クライアントシークレットを生成 のチェックを外す。

Cognito ID プール作成

こっちは認可用の ID プール。

Cognito タブで先程作成したユーザープールの ID とアプリクライアントの ID を指定。

こちらで指定する IAM ロールが認証済みユーザーに認可される。

Amplify で Cognito ユーザー登録・ログインするまで

Amplify インストール。

$ npm install --save aws-amplify

ドキュメントを参考に、ユーザー登録 / ログインまわりの処理を実装。

といっても、Amplify が全部よしなにやってくれるのでなんもやることないっす。

src/utils/AwsUtil.js

import Amplify, {
  Auth,
  API,
} from 'aws-amplify';

Amplify.Logger.LOG_LEVEL = 'DEBUG';
Amplify.configure({
  Auth: {
    region: process.env.VUE_APP_AWS_REGION,
    identityPoolId: process.env.VUE_APP_AWS_COGNITO_ID_POOL_ID,
    userPoolId: process.env.VUE_APP_AWS_COGNITO_USERPOOL_ID,
    userPoolWebClientId: process.env.VUE_APP_AWS_COGNITO_USERPOOL_CLIENT_ID,
  },
  API: {
    endpoints: [{
      name: process.env.VUE_APP_API_NAME,
      endpoint: process.env.VUE_APP_API_ENDPOINT,
      region: process.env.VUE_APP_AWS_REGION,
    }]
  },
});

export function signUp(username, password) {
  return Auth.signUp({
    username,
    password,
  });
}

export function signIn(username, password) {
  return Auth.signIn(username, password);
}

export function signOut() {
  return Auth.signOut();
}

あとは、コイツらをユーザー登録 / ログイン画面でボタン押下時に実行すれば良い。

Vue CLI インストール

参考: Installation | Vue CLI 3

$ npm install -g @vue/cli

Vue CLI でスキャフォールド

参考: Creating a Project | Vue CLI 3

$ vue create hello-world
Vue CLI v3.0.1
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, Router, Linter
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a linter / formatter config: Airbnb
? Pick additional lint features: Lint on save
? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? In package.json
? Save this as a preset for future projects? No

MPA としてガワを実装

vue.config.jspages がミソ。
この設定によって、各ページが個別の html として outputDir へ吐き出されるようになる。

vue.config.js

module.exports = {
  baseUrl: '/public',
  outputDir: 'dist',
  assetsDir: 'assets',
  runtimeCompiler: false,
  productionSourceMap: true,
  parallel: true,
  css: {
    modules: false,
    extract: true,
    sourceMap: false,
  },
  lintOnSave: true,
  pages: {
      signin: {
        entry: 'src/main.js',
        template: 'public/pages/index.html',
        filename: 'pages/signin.html',
        title: 'Sign in',
      },
      signup: {
        entry: 'src/main.js',
        template: 'public/pages/index.html',
        filename: 'pages/signup.html',
        title: 'Sign up',
      },
      signout: {
        entry: 'src/main.js',
        template: 'public/pages/index.html',
        filename: 'pages/signout.html',
        title: 'Sign out',
      },
      sorry: {
        entry: 'src/main.js',
        template: 'public/pages/index.html',
        filename: 'pages/sorry.html',
        title: 'Sorry',
      },
  },
};

後は雑に適宜作成。

src/router.js

import Vue from 'vue';
import Router from 'vue-router';

import SignIn from './views/SignIn.vue';
import SignUp from './views/SignUp.vue';
import SignOut from './views/SignOut.vue';
import Sorry from './views/Sorry.vue';

Vue.use(Router);

export default new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/pages/signin.html',
      name: 'signin',
      props: true,
      component: SignIn,
    },
    {
      path: '/pages/signup.html',
      name: 'signup',
      props: true,
      component: SignUp,
    },
    {
      path: '/pages/signout.html',
      name: 'signout',
      component: SignOut,
    },
    {
      path: '*',
      name: 'sorry',
      component: Sorry,
    }
  ],
});

src/views/SignUp.vue

<template>
  <div class="singup">
    <h1>Sign up</h1>
    <form @submit.stop.prevent="signUp">
      <p v-show="msg">{{ msg }}</p>
      <p v-show="errMsg">{{ errMsg }}</p>
      <input type="email" placeholder="Email" v-model="email" required>
      <input type="password" placeholder="Password" v-model="password" required>
      <input type="submit" value="Submit">
    </form>
    <p>Do you have an account?
      <router-link :to="{ name: 'signin'}">Sign in now!!</router-link>
    </p>
  </div>
</template>

<script>
import * as UserUtil from '@/utils/UserUtil';

export default {
  name: 'SignUp',
  props: ['flashMsg', 'flashErrMsg'],
  data() {
    return {
      email: '',
      password: '',
      errMsg: this.flashErrMsg,
      msg: this.flashMsg,
    };
  },
  methods: {
    async signUp() {
      try {
        await UserUtil.signUp(this.email, this.password);
        this.$router.push({ name: 'signin', params: {flashMsg: '確認メールのリンクをクリックしてからサインインしてください。' }});
      } catch(e) {
        this.errMsg = e.message;
      }
    },
  },
};
</script>

src/views/SignIn.vue

<template>
  <div class="singin">
    <h1>Sign in</h1>
    <form @submit.stop.prevent="signIn" method="post">
      <p v-show="msg">{{ msg }}</p>
      <p v-show="errMsg">{{ errMsg }}</p>
      <input type="email" placeholder="Email" v-model="email" required>
      <input type="password" placeholder="Password" v-model="password" required>
      <input type="submit" value="Submit">
    </form>
    <p>Don't you have an account?
      <router-link :to="{ name: 'signup'}">Sign up now!!</router-link>
    </p>
  </div>
</template>

<script>
import * as UserUtil from '@/utils/UserUtil';
import * as AwsUtil from '@/utils/AwsUtil';

export default {
  name: 'SignIn',
  props: ['flashMsg', 'flashErrMsg'],
  data() {
    return {
      email: '',
      password: '',
      errMsg: this.flashErrMsg,
      msg: this.flashMsg,
    };
  },
  methods: {
    async signIn() {
      try {
        await UserUtil.signIn(this.email, this.password);
      } catch(e) {
        this.errMsg = e.message;
      }
    },
  },
};
</script>

動作確認

$ npm run serve
>  App running at:
>  - Local:   http://localhost:8080/public/
>  - Network: http://192.168.xxx.xxx:8080/public/
>
>  Note that the development build is not optimized.
>  To create a production build, run npm run build.

http://localhost:8080/public/pages/signin.html へアクセスすると冒頭のような画面が表示されるはず!!

ブラウザの開発者ツールを開いて、LocalStorage になんか保存されていれば OK。

S3 へデプロイ

ビルド。

$ npm run build

下記のようなファイルが生成される。

gulp で S3 へデプロイするやつを設定してから、下記を実行してデプロイ。

$ npm run sync

成し遂げたぜ。