概要

Amazon Cognitoのユーザー認証で多要素認証(MFA)を有効にすると、SMSテキストメッセージによる認証ができることは知っていたのですが、時間ベースのワンタイムパスワード(TOTP)にも対応していることは知らなかったので、利用してみました。

Amazon Cognito – TOTP ソフトウェアトークン MFA
https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/user-pool-settings-mfa-totp.html

Node.js実行のイメージ

そもそもCognitoとか、多要素認証、TOTPってなに?という方は下記あたりをご参考ください。

Amazon Cognitoとは

Amazon Cognito とは
https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/what-is-amazon-cognito.html

Amazon Cognito は、ウェブアプリケーションやモバイルアプリケーションの認証、許可、ユーザー管理をサポートしています。 ユーザーは、ユーザー名とパスワードを使用して直接サインインするか、Facebook、Amazon、Google などのサードパーティーを通じてサインインできます。

多要素認証(Multi-Factor Authentication)とは

多要素認証 – Wikipedia
https://ja.wikipedia.org/wiki/%E5%A4%9A%E8%A6%81%E7%B4%A0%E8%AA%8D%E8%A8%BC

多要素認証(たようそにんしょう)は、アクセス権を得るのに必要な本人確認のための要素(証拠)を複数、ユーザーに要求する認証方式である。 必要な要素が二つの場合は、二要素認証や二段階認証とも呼ばれる。

やはりお前らの多要素認証は間違っている
https://dev.classmethod.jp/etc/multi-factor-authentication/

前述の通り、コンピュータの世界では knowledge factor を使った認証が一般的です。 しかし昨今、パスワードが流出したり、簡単すぎるパスワードを推測されたり、という事故が多発しています。 従って、そのような事故があっても直ちに損害が出ないように、2つの要素を組み合わせて認証を行うシステムがあります。これを2要素認証と呼びます。

今さら聞けない2段階認証の話いろいろ
https://qiita.com/isaoshimizu/items/5ca25efebdc5ecee7d9b

ここ数年、2段階認証(2要素認証、Two Factor Authencication)に対応したサービスがどんどん増えてきています。2段階認証は、IDとパスワードだけでなく、ユーザー専用のコード(数値)を入力することでセキュリティを強化するというもの。

時間ベースのワンタイムパスワード(Time-based One Time Password)

今すぐできる、Webサイトへの2要素認証導入
https://blog.ohgaki.net/use-2-factor-authentication-with-your-web-sites

TOTPは名前の通りシードと時間をベースにパスワードを生成します。時間と共にパスワードは変化します。

2要素認証のTOTPとHOTP、どちらがより安全か?
https://blog.ohgaki.net/2fa-totp-hotp-which-is-safer

前提

  • AWSアカウントがありAmazon Cognitoが利用できる
  • Node.jsとnpmがインストールされている

Amazon Cognitoでユーザープールを作成する

こちらの記事を参考にさせていただき、MFAを有効にしたユーザープールを作成します。

javascriptでCognitoユーザプール認証
https://qiita.com/tamo_breaker/items/2cba901565a4fe9dff1a

AWS マネジメントコンソールでAmazon Cognitoを開く

Amazon Cognitoを開いたら「ユーザープールの管理」を選択し、「ユーザープールを作成する」ボタンをクリックします。

ユーザープールをステップに従って作成する

プール名を指定する

プール名は任意指定してください。

サインイン方法を指定する

「ユーザー名」でログイン可能にします。

属性を指定する

検証なので、属性指定はなしにしています。
ただし、ユーザープール後に属性の変更ができないので、実運用ではお気をつけください。

パスワードなどのポリシーを指定する

初期設定のままですすめます。

多要素認証を指定する

「必須」を選択します。
第2の要素に「時間ベースのワンタイムパスワード」を指定します。

Eメールまたは電話番号の検証要求を指定する

今回は検証なしにしました。アラート表示されますが、今回は検証なので気にしません。「SMSメッセージの送信を許可するロール」も作成せずにすすめます。

メッセージをカスタマイズする

今回は、メッセージ送信しないので、初期設定のままですすめます。

タグ追加、デバイス記憶の設定

両方とも初期設定のままですすめます。

アプリクライアントを追加する

Node.jsを利用して認証を検証するので、アプリクライアントを追加します。

アプリクライアントは任意で入力してください。
「クライアントシークレットを生成」は利用しないので、外しておきます。

ワークフローのカスタマイズ

トリガーを使用してカスタマイズがいろいろとできますが、カスタマイズせずにすすみます。

設定確認して作成

ステップの最後に設定確認が表示されます。下の方に「プールの作成」ボタンがありますので、それをクリックしたら作成完了です。

作成完了すると、「プールID」や「プールARN」が発行されます。「プールID」はのちほど利用します。

Node.jsで実装する

Node.jsによる実装もこちらの記事で紹介されている実装をベースにさせてもらいました。

javascriptでCognitoユーザプール認証
https://qiita.com/tamo_breaker/items/2cba901565a4fe9dff1a

実装はGitHubにアップしていますので、よろしければご参考ください。
https://github.com/kai-kou/use-cognito-totp-mfa

環境設定とライブラリのインストール

> node -v
v10.11.0
> npm -v
6.4.1

> mkdir 任意のディレクトリ
> cd 任意のディレクトリ

> npm init
> npm install --save node-fetch
> npm install --save amazon-cognito-identity-js
> npm install --save prompt
> npm install --save qrcode-terminal

CognitoへのアクセスにAWS JavaScript SDK(amazon-cognito-identity-js)を利用します。ユーザー名、パスワード、ワンタイムパスワードを入力するのにはprompt、QRコードを生成・表示するのにqrcode-terminalを利用しています。

amazon-archives/amazon-cognito-identity-js
https://github.com/amazon-archives/amazon-cognito-identity-js

flatiron/prompt
https://github.com/flatiron/prompt

gtanner/qrcode-terminal
https://github.com/gtanner/qrcode-terminal

実装

> touch config.js
> touch mfa-auth.js

設定ファイル

CognitoのユーザープールIDとアプリクライアントのIDを設定します。

config.js

module.exports =  {
  UserPoolId: '[CognitoのユーザープールID]',
  ClientId: '[アプリクライアントのID]'
}

ユーザープールIDはAWS マネジメントコンソールのCognitoページでユーザープール選択→「全般設定」から確認できます。

アプリクライアントのIDはユーザープール選択→「全般設定」→「アプリクライアント」から確認できます。

認証部分の実装

基本的には参考にさせていただいた記事をベースにして、cognitoUser.authenticateUser にMFA時に発生するイベントを追加して、認証成功時にトークンが取得できるところまでを実装しています。

MFAに関する実装には公式ドキュメントを参考にしました。

Amazon Cognito – 例: JavaScript SDK の使用
「MFA メソッドを選択し、TOTP MFA を使用して認証する」
https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/using-amazon-cognito-user-identity-pools-javascript-examples.html

mfa-auth.js

global.fetch = require('node-fetch')
const AmazonCognitoIdentity = require('amazon-cognito-identity-js');
const prompt = require('prompt');
const qrcode = require('qrcode-terminal');

const config = require('./config');


login = function () {
    prompt.start();
    let prompt_schema = {
      properties: {
        username: { required: true },
        password: { hidden: true }
      }
    };
    prompt.get(prompt_schema, function (err, result) {
        let username = result['username'];
        let password = result['password'];

        // ユーザープールID、アプリクライアントIDの設定
        let poolData = {
            UserPoolId: config.UserPoolId,
            ClientId: config.ClientId
        };

        // ユーザーの設定
        let userData = {
            Username: username,
            Pool: new AmazonCognitoIdentity.CognitoUserPool(poolData)
        };
        let cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);

        // 認証情報の設定
        let authenticationData = {
            Username: username,
            Password: password
        };
        let authenticationDetails = new AmazonCognitoIdentity.AuthenticationDetails(authenticationData);

        // Cognitoに認証を要求
        cognitoUser.authenticateUser(authenticationDetails, {
            onSuccess: function (result) {
                // 認証成功時に発生します
                console.log('onSuccess');
                console.log('認証成功時に発生します');
                let idToken = result.getIdToken().getJwtToken();
                console.log('トークン取得できました^^');
                console.log(idToken);
            },
            newPasswordRequired: function (userAttributes, requiredAttributes) {
                // ユーザー作成後、初回ログイン時にパスワード変更が必要になる
                // とりあえず、仮パスワードをそのまま設定しています
                console.log('ユーザー作成後、初回ログイン時にパスワード変更が必要');
                cognitoUser.completeNewPasswordChallenge(password, {}, this);
            },
            mfaSetup: function (challengeName, challengeParameters) {
                // ユーザープールでMFAが有効化されていると発生します
                console.log('mfaSetup');
                console.log('ユーザープールでMFAが有効化されていると発生します');
                cognitoUser.associateSoftwareToken(this);
            },
            associateSecretCode: function (secretCode) {
                // MFA有効化されていてTOTP初回認証時に発生します
                // SecretCodeが発行されるので、Google Authenticatorなどに登録できるよう
                // QRコードを生成します。(シークレットコードを手動入力するのもあり)
                console.log('associateSecretCode');
                console.log('MFA有効化されていてTOTP初回認証時に発生します');

                // QRコードを生成してターミナルに表示
                let url = 'otpauth://totp/Test?secret=' + secretCode + '&issuer=Cognito-TOTP-MFA';
                console.log('シークレットコード: ' + secretCode);
                qrcode.generate(url, {small: true});

                let _this = this;
                let getValue = 'Google AuthenticatorでQRコードを読み取り、ワンタイムパスワードを入力してください';
                prompt.get([getValue], function (err, result) {
                    challengeAnswer = result[getValue];
                    cognitoUser.verifySoftwareToken(result[getValue], 'My TOTP device', _this);
                    console.log('2回目からはQRコードの読み取りは不要です');
                });
            },
            totpRequired: function (secretCode) {
                // ワンタイムパスワード要求時に発生
                // Google Authenticatorなどからワンタイムパスワードを入力して認証します
                console.log('totpRequired');
                console.log('ワンタイムパスワード要求時に発生');

                let _this = this
                let getValue = 'Google Authenticatorのワンタイムパスワードを入力してください';
                prompt.get([getValue], function (err, result) {
                    var challengeAnswer = result[getValue];
                    cognitoUser.sendMFACode(challengeAnswer, _this, 'SOFTWARE_TOKEN_MFA');
                });
            },
            onFailure: function (err) {
                console.log('onFailure');
                console.log('認証失敗時に発生します');
                console.log(err);
            }
        });
    });
};

login();

cognitoUser.authenticateUser メソッドのイベント

メソッドを実行すると、ユーザー名とパスワードによる認証から始まり、必要に応じてイベント?が発生する流れになります。

  • newPasswordRequired : 初回ログイン時のパスワード変更要求
  • mfaSetup : MFA有効時に発生
  • associateSecretCode : TOTP認証の初回時に発生
  • totpRequired : ワンタイムパスワード要求時に発生
  • onFailure : 認証失敗時に発生

他にもselectMFAType というのがありますが、これはSMSテキストメッセージによる認証やTOTP認証など要素が複数ある場合に、発生するみたいですが、今回はTOTP認証のみなので、割愛しています。

TOTP対応アプリ用にQRコード生成

QRコード生成に必要となるURIフォーマットについては下記を参考にさせてもらいました。

二段階認証(TOTP)メモ
https://qiita.com/xylitol45@github/items/4f8418554a6550189341

TestCognito-TOTP-MFA は任意で設定可能です。

mfa-auth.js(抜粋)

// QRコードを生成してターミナルに表示
let url = 'otpauth://totp/Test?secret=' + secretCode + '&issuer=Cognito-TOTP-MFA';

prompt利用時の注意点

イベント処理にprompt.get を利用する場合、cognitoUser のメソッドにthis パラメータを直接渡すと、UnhandledPromiseRejectionWarning: TypeError: callback.onFailure is not a function とエラーが発生するので、ご注意ください。非同期怖いです。

mfa-auth.js(抜粋)

let _this = this;
let getValue = 'Google AuthenticatorでQRコードを読み取り、ワンタイムパスワードを入力してください';
prompt.get([getValue], function (err, result) {
    challengeAnswer = result[getValue];
    // thisを直接渡すとエラーになる
    cognitoUser.verifySoftwareToken(result[getValue], 'My TOTP device', _this);
    console.log('2回目からはQRコードの読み取りは不要です');
});

ユーザー登録と認証

Cognitoのユーザープールにユーザーを登録して、実際に認証してみます。
今回は手抜きでユーザー登録はAWS マネジメントコンソールから行います。

ユーザープールを選択して、「ユーザーとグループ」から「ユーザーの作成」ボタンをクリックします。

  • ユーザー名: 必須
  • この新規ユーザーに招待を送信しますか?: チェックを外す
  • 仮パスワード: 必須
  • 電話番号: 空
  • 電話番号を検証済みにしますか?: チェックを外す
  • E メール: 空
  • E メールを検証済みにしますか?: チェックを外す

ユーザー作成できたら、Node.jsを実行してみます。
config.js に各IDの設定をお忘れなく(1敗

> node mfa-auth.js

先程登録したユーザ名とパスワードを入力します。
すると、シークレットコードとQRコードが出力されますので、TOTP認証に対応したスマホアプリで読み取ります。

TOTP認証に対応した認証アプリには「Google Authenticator」などがあります。

App Store – iTunes – Apple
https://itunes.apple.com/jp/app/google-authenticator/id388497605?mt=8

Google Play
https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=ja

QRコードを読み取り登録できると、ワンタイムパスワードが確認できるようになりますので、それを入力します。

ワンタイムパスワードが正しければ、無事に認証完了し、トークンを取得することができます。

ワンタイムパスワードの認証成功すると、次回からはassociateSecretCode イベントが発生せずにワンタイムパスワードの要求がされます。

やったぜ。

まとめ

多要素認証(MFA)の実装はややこしくて、大変そうなイメージがありましたが、Amazon Cognitoを利用すると比較的簡単に実装することができました。
ユーザープールの設定で、ユーザーごとに任意選択させることや、SMSテキストメッセージ認証を追加することもできるので、アプリクライアントに対して、よりセキュアな認証を実装することができますね^^

参考

Amazon Cognito – TOTP ソフトウェアトークン MFA
https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/user-pool-settings-mfa-totp.html

Amazon Cognito とは
https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/what-is-amazon-cognito.html

多要素認証 – Wikipedia
https://ja.wikipedia.org/wiki/%E5%A4%9A%E8%A6%81%E7%B4%A0%E8%AA%8D%E8%A8%BC

やはりお前らの多要素認証は間違っている
https://dev.classmethod.jp/etc/multi-factor-authentication/

今さら聞けない2段階認証の話いろいろ
https://qiita.com/isaoshimizu/items/5ca25efebdc5ecee7d9b

今すぐできる、Webサイトへの2要素認証導入
https://blog.ohgaki.net/use-2-factor-authentication-with-your-web-sites

2要素認証のTOTPとHOTP、どちらがより安全か?
https://blog.ohgaki.net/2fa-totp-hotp-which-is-safer

javascriptでCognitoユーザプール認証
https://qiita.com/tamo_breaker/items/2cba901565a4fe9dff1a

Amazon Cognito – 例: JavaScript SDK の使用
https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/using-amazon-cognito-user-identity-pools-javascript-examples.html

amazon-archives/amazon-cognito-identity-js
https://github.com/amazon-archives/amazon-cognito-identity-js

flatiron/prompt
https://github.com/flatiron/prompt

gtanner/qrcode-terminal
https://github.com/gtanner/qrcode-terminal

二段階認証(TOTP)メモ
https://qiita.com/xylitol45@github/items/4f8418554a6550189341

App Store – iTunes – Apple
https://itunes.apple.com/jp/app/google-authenticator/id388497605?mt=8

Google Play
https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=ja

元記事はこちら

Amazon Cognitoのワンタイムパスワード(TOTP)認証をNode.jsで試してみた