はじめに
こんにちは!BFT名古屋支店のマッチです。今回はCognitoのカスタム認証チャレンジを使ってワンタイムパスワードを発行し、SMSで通知してサインインする仕組みを実装してみたいと思います。
幸いにも、同じような仕組みをメール認証で実装されている方がおりましたので、こちらのブログ内容をベースに改修する形で実装することとしました。
https://dev.classmethod.jp/articles/cognito-lambda-passwordless/#toc-10
構成概要
今回構築する環境の構成図は以下のようになります。
①ユーザーがサインインページで電話番号を入力する。
②認証チャレンジ定義Lambdaが実行され、認証フローが決定される。
③認証チャレンジ作成Lambdaが実行され、ワンタイムパスワード(OTP)を発行する。
④~⑤Amazon SNSが呼び出され、SMSが送信される。
⑥ユーザーが受信したOTPをサインインページに入力する。
⑦認証チャレンジ検証Lambdaが実行され、入力されたOTPが正しいか判定する。
⑧認証チャレンジ定義Lambdaが実行され、判定結果が正しければ認証成功し、トークンが発行される。
⑨認証成功後に、認証後Lambdaが実行され、ユーザー属性を更新する(電話番号を検証済みとする)。
作ってみた
リソースのデプロイ
AWS Serverless Application Repositoryから、CognitoユーザープールおよびLambda関数をデプロイします。 ただし、Cognitoユーザープールについてはここで作成されるものは使用せず、後ほど新しく作成するため、デプロイ後必要に応じて削除いただいて構いません。
以下にアクセスし、[Deploy]をクリックします。
Application Search - AWS Serverless Application Repository
送信元メールアドレスとユーザープール名を入力し(SESおよびユーザープールは使用しないため適当で構いません)、[デプロイ]をクリックします。
デプロイ履歴から[Create complete]と表示されることを確認します。
Cognitoユーザープールの作成
先程作成したユーザープールはメール認証用のもので、サインインオプションにメールアドレスが指定されているため、SMS認証用に電話番号を指定したユーザープールを新たに作成します。
AWSマネジメントコンソールから[Amazon Cognito]の[ユーザープール]に移動し、[ユーザープールを作成]をクリックします。
画面に従い、各項目を設定していきます。ただし、次の項目については必ず以下のように設定してください。
Cognito ユーザープールのサインインオプション:電話番号にのみチェックを入れる
パスワードポリシー:最小文字数8文字、パスワード要件のチェックはすべて外す
多要素認証:MFAなし
必須の属性:phone_numberのみ
認証フロー:ALLOW_CUSTOM_AUTH
設定が終わったら、[確認および作成]画面で[ユーザープールを作成]をクリックします。
作成したユーザープールを選択し、[ユーザープールのプロパティ]から[Lambdaトリガーの追加]をクリックし、以下のようにLambdaトリガーを設定します。
サインアップ前Lambdaトリガー:PreSignUp
認証チャレンジレスポンスを確認Lambdaトリガー:VerifyAuthChallengeResponse
認証チャレンジを作成Lambdaトリガー:CreateAuthChallenge
認証チャレンジを定義Lambdaトリガー:DefineAuthChallenge
認証後Lambdaトリガー:PostAuthentication
以下からユーザープールIDとユーザープールクライアントIDを確認します。この2つは後ほど使うので控えておきます。
ユーザープールID:ユーザープールの概要
ユーザープールクライアントID:[アプリケーションの統合]-[アプリケーションクライアントのリスト]-[クライアントID]
Lambdaの改修
以下2つのLambda関数のコードとアクセス権限をSMS認証用に改修します。
Create Auth Challenge (認証チャレンジ作成Lambda )
create-auth-challenge.jsを以下のように改修します。元のコードからの主な変更点としてはEメールの送信を行う部分をSMSの送信に置き換えています。
"use strict"; // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: MIT-0 Object.defineProperty(exports, "__esModule", { value: true }); const crypto_secure_random_digit_1 = require("crypto-secure-random-digit"); const aws_sdk_1 = require("aws-sdk"); const sns = new aws_sdk_1.SNS(); exports.handler = async (event) => { let secretLoginCode; if (!event.request.session || !event.request.session.length) { // This is a new auth session // Generate a new secret login code and mail it to the user console.log(event.request.session); secretLoginCode = crypto_secure_random_digit_1.randomDigits(6).join(''); await sendSMSviaSNS(event.request.userAttributes.phone_number, secretLoginCode); } else { // There's an existing session. Don't generate new digits but // re-use the code from the current session. This allows the user to // make a mistake when keying in the code and to then retry, rather // then needing to e-mail the user an all new code again. console.log(event.request.session); const previousChallenge = event.request.session.slice(-1)[0]; secretLoginCode = previousChallenge.challengeMetadata.match(/CODE-(\d*)/)[1]; } // This is sent back to the client app event.response.publicChallengeParameters = { phone: event.request.userAttributes.phone_number }; // Add the secret login code to the private challenge parameters // so it can be verified by the "Verify Auth Challenge Response" trigger event.response.privateChallengeParameters = { secretLoginCode }; // Add the secret login code to the session so it is available // in a next invocation of the "Create Auth Challenge" trigger event.response.challengeMetadata = `CODE-${secretLoginCode}`; return event; }; async function sendSMSviaSNS(phoneNumber, secretLoginCode) { const params = { "Message": "Your secret code: " + secretLoginCode, "PhoneNumber": phoneNumber }; await sns.publish(params).promise(); }
また、SNSへのアクセス権を付与するため、[設定]-[アクセス権限]から以下のIAMポリシーを追加します。
Allow: sns:Publish
PostAuthentication (認証後Lambda)
post-authentication.jsを以下のように改修します。元のコードではメールアドレスの属性を更新していましたが、今回は電話番号を使うので電話番号の更新に変更しています。
"use strict"; // Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: MIT-0 Object.defineProperty(exports, "__esModule", { value: true }); const aws_sdk_1 = require("aws-sdk"); const cup = new aws_sdk_1.CognitoIdentityServiceProvider(); exports.handler = async (event) => { if (event.request.userAttributes.phone_number_verified !== 'true') { const params = { UserPoolId: event.userPoolId, UserAttributes: [{ Name: 'phone_number_verified', Value: 'true', }], Username: event.userName, }; await cup.adminUpdateUserAttributes(params).promise(); } return event; };
また、ユーザー属性のアップデート権限を付与する対象が始めに作成されたユーザープールのままになっているため、[設定]-[アクセス権限]から以下のIAMポリシーのリソースを新規作成したユーザープールのarnに変更します。
Allow: cognito-idp:AdminUpdateUserAttributes
クライアントサーバの構築
今回はEC2のインスタンスを作成し、以下GitHubからWebサーバを構築します。
https://github.com/aws-samples/amazon-cognito-passwordless-email-auth/tree/master/client
EC2インスタンスの作成
画面に従い、各項目を設定していきます。今回は以下のような設定としました。
AMI:Amazon Linux 2
インスタンスタイプ:t3.small
セキュリティグループ:インバウンド設定で4200ポートを追加(Angularを使用するため)
Webサーバの構築
作成したインスタンスにSSH接続し、アップデートを行った後、gitをインストールします。
$ sudo yum update -y $ sudo yum install -y git
Node.jsをインストールします。
$ curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.32.0/install.sh | bash $ . ~/.nvm/nvm.sh $ nvm install --lts
インストール後、バージョンを確認します。
$ node -v $ npm -v
レポジトリをクローンし、依存パッケージをインストールします。
$ git clone https://github.com/aws-samples/amazon-cognito-passwordless-email-auth.git
$ cd amazon-cognito-passwordless-email-auth/client
$ npm install
src/environments/environment.tsファイルに先程確認したユーザープールIDとユーザープールクライアントIDを入力します。
environment.ts
export const environment = { production: false, region: 'ap-northeast-1', // リージョン userPoolId: 'ap-northeast-1_XXXXXXXXX', // ユーザプールID userPoolWebClientId: 'XXXXXXXXXXXXXXXX', // ユーザプールクライアントID };
アプリをビルドし、起動します。
node_modules/@angular/cli/bin/ng serve --host=0.0.0.0 --public-host=<ホスト名>
電話番号の登録
Amazon SNSのサンドボックスを解除していない場合にはSMSメッセージを送るために以下手順で電話番号を登録する必要があります。
AWSマネジメントコンソールから[Amazon SNS]-[テキストメッセージング(SMS)]に移動し、[サンドボックスの送信先電話番号]の[電話番号を追加]をクリックします。
送信先の電話番号を入力し、[電話番号を追加]をクリックします。
動作確認
環境構築ができたため、実際にSMSメッセージが送られるか検証します。なお、サインインページのUIを変更していないので画面表示はメール認証のままとなっていますが、今回はCognitoの検証なので一旦このままとします。また機会があればページUI等もいじってみたいと思います。
初回サインアップ時の動き
初回サインアップ時(Cognitoユーザープールに登録されていないユーザーを認証するとき)は以下の手順でサインインします。
以下のページで[Sign up]をクリックします。
電話番号を入力し、[SIGN UP]をクリックします(入力欄はE-mailとなっていますが電話番号を入力します)。
スマホにワンタイムパスワードが届きます。
サインイン画面にワンタイムパスワードを入力し、[CONTINUE]をクリックします。
以下のようにユーザー情報が表示されれば認証成功です。
2回目以降のサインイン時の動き
2回目以降のサインイン時(すでにCognitoユーザープールに登録済みのユーザーを認証するとき)は以下の手順でサインインします。
以下のページで電話番号を入力し、[SEND SIGN-IN CODE]をクリックします。
スマホにワンタイムパスワードが届くので、以降は初回サインアップ時と同じ手順で認証を進めていきます。
まとめ
今回は既存のコードを改修することでCognitoのカスタム認証チャレンジでSMS認証の仕組みを実装することができました。今回の手順はIVR認証など他の認証方法を使用する際にも応用ができそうなので、また機会があればいろいろと試してみたいと思っています。