AWS: cognitoのuser poolで管理者または開発者が払いだしたIDとパスワードを利用してブラウザからサーバへログインする

今回は、ユーザの登録は管理者がAWSのコンソールなどで実施し、一般ユーザはログインだけするケースです。
ブラウザからのcognitoに接続して認証情報(access token等)を取得することが目的です。

具体的には、以下の「管理者または開発者によって作成されるユーザーの認証フロー」の部分を実装します。
docs.aws.amazon.com

AWSのコンソールを使ってUser Pool の設定をする。

docs.aws.amazon.com
User Poolの設定では、emailの登録を必須にする(orしない)やパスワードの長さや文字種別などを設定します。
(ここは、ドキュメント通りで特に迷わ無いかと思います。)
今回は、emailのみ必須にしました。

User Poolの画面で、ユーザを作成する。

ここは、コンソールの画面の流れに従います。
AWSコンソールで作成した場合は、以下のリンクの「Force Change Password」状態です。
docs.aws.amazon.com

Identity Poolを作成し、User Poolを結びつけます。

ここは、以下のドキュメント通りに実施すれば迷うことは無いかと思います。
ユーザープールをフェデレーテッドアイデンティティと統合する - Amazon Cognito

認証に必要なjavascriptライブラリを集める

基本的には、以下のリンクのステップを実施していくだけです。
Amazon Cognito ID SDK for JavaScript でユーザープールを使用するようにセットアップする - Amazon Cognito

Stanford JavaScript Crypto Library (sjsc.js)は、自前でビルドが必要になります。
sjclをダウンロードしたフォルダに移動して、以下のコマンド(2つ)を実行します。

./configure --with-codecBytes 
make

sjcl.jsが更新されるので、出来上がった新しいsjcl.jsを使用します。

javaが無いと失敗するので、それっぽい警告が出た場合は、javaをインストールしましょう。
homebrewが既にインストールされていれば、以下のコマンドです。

brew cask install java

homebrewが無い場合は、手前味噌ですが、以下の記事のhomebrewのインストールの箇所をご参照ください。
marmarossa.hatenablog.com

また、amazon-cognito-identity.min.js が、初期化で失敗したので、以下からコードを一式持ってきました。
github.com
(細かいですが、ブラウザで動作させるために、importやexportは消さないと動きません。)

amazon-cognito-identity.min.jsそのままだと、なぜか、以下の初期化時に失敗しました…(私だけ?)

new AWSCognito.CognitoIdentityServiceProvider()

ブラウザ側の実装

MFAの設定を省略すると、実装するのは、大きく以下の2ステップです。

  1. ユーザにIDと仮パスワードを入力して貰いcognito側とやり取りする。
  2. 新しいパスワードをcognito側に送信し、「confirmed」状態にする。

AWS側の提供している以下のコード例は、さっぱりしているので、背後で何が起きているのかを付け足します。
docs.aws.amazon.com

まずは、ステップ1のユーザにIDと仮パスワードを入力してもらい、cognitoとやり取りする部分です。

ブラウザとcognitoの間は、 Remote Password (SRP)プロトコル でやり取りしており、実際にはパスワードは送信されません。
(APIには、パスワードを送信するものも用意されていますが、安全な(例えばman in middle攻撃を受け無い)通信路が確保されていることが利用条件です。)

SRP自体は、以下のwikiが詳しいです。
Secure Remote Password protocol - Wikipedia

個別に拾ってきたCognitoUser.jsの中でSRPの全体を制御して、具体的な計算は、AuthenticationHelper.js内で実施しており、
上記のwikiと照らし合わせると大体何をやっているのかが掴めます。

サンプルにすると以下の通りで、冒頭の「管理者または開発者によって作成されるユーザーの認証フロー」のステップだと1から5に相当します。

var poolData = {
    UserPoolId : '{{YOUR USER ID}}',
    ClientId : '{{YOUR Client ID}}',
    Paranoia : 7 //7である必要性はないが、0だと古いIEなどでセキュアでない。
};
var userPool = new CognitoUserPool(poolData);
var userData = {
    Username : '{{username}}', //作成したユーザ名です。
    Pool : userPool
};
var cognitoUser = new CognitoUser(userData);

var authenticationData = {
    Username : '{{username}}',//作成したユーザ名です。
    Password : '{{password}}', //作成したユーザのパスワードです。
};

var authDetails =
    new AuthenticationDetails(authenticationData);

cognitoUser.authenticateUser(authDetails,{
    onFailure : function(err) {
	//何か処理に失敗した場合に呼ばれる。
    },
    
    onSuccess : function(result) {
	//Confirmed状態で認証に成功した場合に呼ばれる。
	//token等はresultに格納されている。
    },
    
    newPasswordRequired: function(userAttributes,requiredAttributes) {
	//Force Change Password状態で、認証に成功した場合に呼ばれる。
	//この後、ユーザから新しいパスワードを受けとり更新する必要がある。
    }
});

次は、ステップ2の新しいパスワードをcognitoへ送る部分です。
注意点として、CognitoUserは、先ほどのインスタンスを使ってください。
CognitoUserクラスは、cognitoとやり取りするときに、内部でSessionを自動付与して送付しており、再度作成し直すと、Sessionは当然クリアされているのでエラーになります。

//第2引数は、追加で設定が必要な属性(誕生日や性別など)がある場合に指定します。
//今回は、emailのみ必須の属性で、管理コンソール側で設定してしまっているのでnullとしています。
cognitoUser.completeNewPasswordChallenge(new_password,null,{
    onFailure : function(err) {
	//なにか失敗した
    },
    onSuccess : function(result) {
	//成功した(confirmedに以降した)
    }
    //MFAを設定する場合は略
});

以上でMFAの設定をしない場合は、無事にトークンが取得できたと思います。

参考

公式のドキュメントは以下の通りです。
http://docs.aws.amazon.com/cognito/latest/developerguide/cognito-dg-pdf.pdf