「Amazon Cognito」で提供するログインページを自作してみる(`aws-cli`で構築)

2019-06-26Amazon, AWS, Bash, JavaScript

はじめに

「Amazon Cognito」を使って既存のWebサービスのログイン認証機能を一部置き直す方法について、何パターンか検討してみました。

今まで検討してみた方法ですが、いずれも「Amazon Cognito」が提供するログインページをそのまま利用する、というものでした。

どうしてもログインページを既存のWebシステムのデザインに合わせたい、という場合にはログインページも既存のWebページのものを使う方法もあります。
既存のWebページのものを流用する方法についてまとめてみました。

準備

今回はなるべくAWSの「マネージメントコンソール」からの操作を行わずに「Cognito」を使ったログイン機能を実装してみます。

そのため、以下のコマンドを利用することを前提としています。

  • aws コマンド
  • jq コマンド
  • npm コマンド

検証環境

$ aws --version
aws-cli/1.16.170 Python/3.7.3 Darwin/18.6.0 botocore/1.12.160

$ jq --version
jq-1.6

$ npm --version
6.9.0

作業開始

今回作成したプログラムは以下のGithubリポジトリにコミット済みです。
試行錯誤した結果作成されたプログラムを負いながら手順を説明します。

圧縮されたソースファイルをダウンロードするか、 git コマンドで clone しておいてください。
(Githubリポジトリページのソースコードを見ても良いです。)

$ git clone git@github.com:genzouw/aws-cognito-example-custom-page.git

※(2019-06-06) 便宜上、当エントリにGithubリポジトリ上のコードと同じ内容を貼り付けておきますが、メンテナンスはGithubリポジトリの方だけとする予定です。乖離する可能性があることをご了承ください。

ユーザープール・IDプールの作成

「Cognito」のユーザープール・IDプールを作成していきましょう。
以下のシェルスクリプトを実行すれば、ユーザープール・IDプールの設定はほぼ完了します。

ただし、ソースコード内の以下の3つの環境変数はご自分で構築したい「ユーザープール」「アプリクライアント」「IDプール」の名前に変更しておきましょう。(何でも構いません)
「リージョン」はご自分で環境を構築したい対象リージョンを設定ください。

※注意点として、いくつかの名前はハイフン - を利用するとエラーとなるため、 アンダースコア _ を利用することをおすすめします。

declare -r AWS_REGION="ap-northeast-1"
declare -r AWS_COGNITO_USER_POOL_NAME="genzouw_user_pool"
declare -r AWS_COGNITO_USER_POOL_CLIENT_NAME="client_custom_login"
declare -r AWS_COGNITO_ID_POOL_NAME="genzouw_id_pool"

bin/setup-aws.sh

#!/usr/bin/env bash
set -o errexit
set -o nounset

# (0) Change to your settings
declare -r AWS_REGION="ap-northeast-1"
declare -r AWS_COGNITO_USER_POOL_NAME="genzouw_user_pool"
declare -r AWS_COGNITO_USER_POOL_CLIENT_NAME="client_custom_login"
declare -r AWS_COGNITO_ID_POOL_NAME="genzouw_id_pool"

# (1) Create "USER_POOL"
declare -r AWS_COGNITO_USER_POOL_ID=$(
  aws cognito-idp create-user-pool \
    --pool-name ${AWS_COGNITO_USER_POOL_NAME} \
    --auto-verified-attributes email \
    --username-attributes email \
    | jq -r '.UserPool.Id'
)
echo "* AWS_COGNITO_USER_POOL_ID='${AWS_COGNITO_USER_POOL_ID}'"

# (2) Create "USER_POOL_CLIENT"
declare -r AWS_COGNITO_USER_POOL_CLIENT_ID=$(
  aws cognito-idp create-user-pool-client \
    --user-pool-id "${AWS_COGNITO_USER_POOL_ID}" \
    --client-name "${AWS_COGNITO_USER_POOL_CLIENT_NAME}" \
    --no-generate-secret \
    --refresh-token-validity 1 \
    --explicit-auth-flows "ADMIN_NO_SRP_AUTH" \
    --supported-identity-providers "COGNITO" \
    | jq -r .UserPoolClient.ClientId
)
echo "* AWS_COGNITO_USER_POOL_CLIENT_ID='${AWS_COGNITO_USER_POOL_CLIENT_ID}'"

# (3) Create "ID_POOL"
declare -r AWS_COGNITO_ID_POOL_ID=$(
  aws cognito-identity create-identity-pool \
    --identity-pool-name "${AWS_COGNITO_ID_POOL_NAME}" \
    --no-allow-unauthenticated-identities \
    --cognito-identity-providers "ProviderName=cognito-idp.${AWS_REGION}.amazonaws.com/${AWS_COGNITO_USER_POOL_ID},ClientId=${AWS_COGNITO_USER_POOL_CLIENT_ID},ServerSideTokenCheck=true" \
    | jq -r .IdentityPoolId
)
echo "* AWS_COGNITO_ID_POOL_ID='${AWS_COGNITO_ID_POOL_ID}'"

# (4) Oputput JavaScript config
declare -r SCRIPT_DIR_PATH="$(dirname "$(readlink -f "$0")")"

cat <<EOF >${SCRIPT_DIR_PATH}/../aws.js
export var AWS_REGION = "${AWS_REGION}"
export var AWS_COGNITO_USER_POOL_ID = "${AWS_COGNITO_USER_POOL_ID}"
export var AWS_COGNITO_USER_POOL_CLIENT_ID = "${AWS_COGNITO_USER_POOL_CLIENT_ID}"
export var AWS_COGNITO_ID_POOL_ID = "${AWS_COGNITO_ID_POOL_ID}"
EOF

ログインページの作成

AWS上に必要なものはできたので、ローカルでログインページの開発を行いましょう。

まずは npm を使って必要なライブラリをインストールします。

$ echo ${PWD##*/}
aws-cognito-example-custom-page

$ npm install

時間がかるのでしばらく待ちましょう。
今回の方式では、「OAuth2」は使用しません。

HTML+JavaScriptから「Cognito」に直接問い合わせし、認証処理を行います。
最終的にブラウザ上で「ID Token」を取得できます。

これを実現するために以下のJavaScriptライブラリが必要ですので、npm install でインストールを行いました。

  • "amazon-cognito-identity-js"
  • "aws-sdk"
  • "jsbn"
  • "sjcl"

またボタンにクリックイベントを設定するために使いなれた jQuery もインストールされるようにしています。

  • "jQuery"

開発用サーバの起動

正常にライブラリがインストールできたら以下のコマンドを実行し、開発用サーバを起動します。

$ npx webpack-dev-server

ブラウザで http://localhost:8080 にアクセスすればWebサーバが起動していることがわかると思います。
ここからHTML+JavaScriptを作成していくことになります。

index.html を作成

こちらはJavaScript読み込みもない単純なHTMLとなっています。

<meta charset="UTF-8">
<title>Top</title>

<div>
   <h1>Top</h1>

   <hr>

   <a rel="stylesheet" href="/signup.html">Next(sign up)</a>
</div>

signup.html という、新規登録ページへのリンクだけが貼ってあります。

次に新規登録ページを作成していきます。

signup.html を作成

新規登録ページになります。
メールアドレスとパスワードの入力項目と、ボタンが一つだけの単純なページです。

<meta charset="UTF-8">
<title>Sign Up</title>
<script src="./bundle.js"></script>

<div id="signup">
  <h1>Sign Up</h1>
  <form name='form-signup' id="form-signup">
    <span style="display: inline-block; width: 150px;">User ID(Email)</span>
    <input type="text" id="email" placeholder="Email Address"><br>
    <span style="display: inline-block; width: 150px;">Password</span>
    <input type="password" id="password" placeholder="Password"><br>
    <br>
    <input type="button" id="createAccount" value="Create Account">
  </form>

  <hr>

  <a href="/activation.html">Next(activation)</a>
</div>

./bundle.js というJavaScriptファイルを読み込んでいますが、こちらの実態は main.js というファイルになります。
先程 npx webpack-dev-server というコマンドで立ち上げた開発用Webサーバですが、 main.js ファイルが変更されたことを検知してトランスコンパイルし bundle.js を生成してくれます。
細かい話は Webpack について学習する必要があるためここでは割愛しますが、ともかく今回の JavaScript ロジックは main.js に記載していくと思ってください。

main.js は最後に作成することとして次のHTMLファイルの作成に移ります。

activation.html を作成

前のページで新規登録が終了すると、「Cognito」がメールアドレスにアクティベーションコードを送信します。
メールを確認した後、アクティベーションコードを入力して新規登録を完了させるためのページです。

新規登録ページ同様、メールアドレスとアクティベーションコードの入力項目と、ボタンが一つだけの単純なページです。

<meta charset="UTF-8">
<title>Activation</title>
<script src="./bundle.js"></script>

<div id="activation">
  <h1>Activation</h1>
  <div><span style="color: red;">All fields are required.</span></div>
  <form name="form-activation">
    <span style="display: inline-block; width: 150px;">Email Address</span>
    <input type="text" id="email" placeholder="Email Address">
    <br/>
    <span style="display: inline-block; width: 150px;">Activation Key</span>
    <input type="text" id="activationKey" placeholder="Activation Key">
    <br/><br/>
    <input type="button" id="activationButton" value="Activation">
  </form>

  <hr>

  <a href="/signin.html">Next(sign in)</a>
</div>

アクティベーションが終了すると、次はログインページへと移動するはずなので、ページ下部にリンクを付与しています。

signin.html の作成

メールアドレスとパスワードを入力し、ボタンを押すとログイン!とならずに今回は「Cognito」から「ID Token」をつけとって下部のテキストエリアに表示することとします。

<meta charset="UTF-8">
<title>Sign In</title>
<script src="./bundle.js"></script>

<div id="signin">
  <h1>Sign In</h1>
  <div id="message"><span style="color: red;"></span></div>
  <form name="form-signin">
    <span style="display: inline-block; width: 150px;">Email Address</span>
    <input type="text" id="email" placeholder="Email Address">
    <br/>
    <span style="display: inline-block; width: 150px;">Password</span>
    <input type="password" id="password" placeholder="Password">
    <br/><br/>
    <input type="button" id="signinButton" value="Sign In">
  </form>

  <hr>

  <h2>ID Token</h2>
  <textarea id="idtoken" rows="10" cols="80"></textarea>
</div>

main.js の作成

いよいよロジックを記述していきます。
先程話をしたとおり、開発用サーバ起動コマンドが main.js の変更を検知して bundle.js に変換してくれます。
また、 index.html 以外の3つのページはいずれも ./bundle.js を読み込む記述があります。

つまり今回のアプリはすべてのページのロジックが main.js に記述されることを想定しています。

import AWS from 'aws-sdk'
import {
  AuthenticationDetails,
  CognitoUserAttribute,
  CognitoUser,
  CognitoUserPool,
} from 'amazon-cognito-identity-js'
import $ from 'jquery'
import * as aws from './aws.js'

// 設定ファイルの内容を各オブジェクトに反映
const userPool = new CognitoUserPool({
  UserPoolId: aws.AWS_COGNITO_USER_POOL_ID,
  ClientId: aws.AWS_COGNITO_USER_POOL_CLIENT_ID,
})
AWS.config.region = aws.AWS_REGION
AWS.config.credentials = new AWS.CognitoIdentityCredentials({
  IdentityPoolId: aws.AWS_COGNITO_ID_POOL_ID,
})

// jQueryのready処理
$(() => {
  // `signup.html` のロジック
  $("#createAccount").click(() => {
    const username = $("#email").val()
    const password = $("#password").val()

    userPool.signUp(username, password, [], null, (err) => {
      if (err) {
        alert(err.message)
      } else {
        alert('OK! Please move to next page.')
      }
    })
  })

  // `activation.html` のロジック
  $("#activationButton").click(() => {
    const email = $("#email").val()
    const activationCode = $("#activationCode").val()

    const cognitoUser = new CognitoUser({
      Username: email,
      Pool: userPool,
    })

    cognitoUser.confirmRegistration(activationCode, true, (err) => {
      if (err) {
        alert(err.message)
      } else {
        alert('OK! Please move to next page.')
      }
    })
  })

  // `signin.html` のロジック
  $("#signinButton").click(() => {
    const email = $('#email').val()
    const password = $('#password').val()

    const authenticationDetails = new AuthenticationDetails({
      Username: email,
      Password: password,
    })

    const cognitoUser = new CognitoUser({
      Username: email,
      Pool: userPool,
    })

    cognitoUser.authenticateUser(authenticationDetails, {
      onSuccess: (result) => {
        const idToken = result.getIdToken().getJwtToken()
        $("#idtoken").val(idToken)
      },
      onFailure(err) {
        alert(err.message)
      },
    })
  })
})

signup.html ページ、 activation.html ページ、 signin.html ページにはそれぞれ #createAccount#activationButton#signinButton というボタンがありましたが、それぞれのボタンクリックイベントに「Cognito」へリクエストを行うための処理を記載しています。

ひとこと

JavaScript部分のロジックは入力チェックがなかったりと単純にしてありますが、応用できると思います。
参考になれば幸いです。

最近はAmplify 使ったらもっとシンプルに作成できるのかもしれません。

2019-06-26Amazon, AWS, Bash, JavaScript