クラウドインテグレーションサービス「雲斗」のブログ

芝公園にある創研情報株式会社がAWS を 中心にクラウドの基本から便利な使いかたまでをお伝えしていきます。

Amazon Cognito Amazon DynamoDB AWS IAM

Amazon DynamoDBへのアクセスをCognitoにより制限してみる

2018/08/31

だいぶ時間が経過してしまったが、前回記事の続きである。

簡単に振り返ると、前回は取得したWatsonのSTT/TTSトークンをブラウザのJSから利用するためAWS DynamoDBを使用する方法を紹介した。しかしながらDynamoDBアクセスの認証情報がソースコードに埋め込まれており、そのままでは誰でも利用できてしまう問題があった。今回はこのDynamoDBへのアクセスをCognito Poolの認証により制限する方法を紹介する。

利用したサービス

AWS DynamoDB、IAM、Cognito

動作確認したブラウザ

Google Chrome 61.0.3163.79(64bit)

事前準備

  1. 必要なファイルをダウンロードしてくる。
    AWS SDK for JavaScript v2.112.0
    aws-sdk.min.js
    Amazon Cognito Identity SDK for JavaScript
    amazon-cognito-identity.min.js
    aws-cognito-sdk.min.js
    Stanford JavaScript Crypto Library
    sjcl.js
    RSA and ECC in JavaScript
    jsbn.js
    jsbn2.js
    jQuery Core 3.2.1 - minified
    jquery-3.2.1.min.js
    jQuery UI v1.12.1
    jquery-ui.min.js
    jquery-ui.min.css
    images/*.png
  2. ダウンロードしたファイルを以下のフォルダ構成になるよう配置する。
    [フォルダ]
        │ index.html(後述)
        ├─css
        │ │ jquery-ui.min.css
        │ └─images
        │     * .png
        └─js
            amazon-cognito-identity.min.js
            aws-cognito-sdk.min.js
            aws-sdk.min.js
            jquery-3.2.1.min.js
            jquery-ui.min.js
            jsbn.js
            jsbn2.js
            sjcl.js
            main.js(後述)

AWS設定

Cognito関連の設定はCLIツールを使用せず、AWSコンソールを使用した。

  1. ユーザプール作成とアプリクライアント追加 1-1-dynamo-cognito
    ここでは以下のとおり設定した。
    プール名:DynamoDBUsers

    1-2-dynamo-cognito

    アプリクライアントを指定する以外はデフォルト値のままとするため、「デフォルトを確認する」クリックで確認画面へ進む。

    1-3-dynamo-cognito

    確認画面の「アプリクライアントの追加」クリックで表示される画面より以下を入力、指定し「アプリクライアントの作成」をクリックする。

    アプリクライアント名:DynamoDBClient
    クライアントシークレットを生成する:チェックを外す

    1-4-dynamo-cognito

    ここまでで必要な項目は設定済みなので「プールの作成」をクリックする。

    1-5-dynamo-cognito

    後述のIDプール作成で使用するため、"ユーザープールは正常に作成されました。"の下に表示される「プール ID」と"全般設定 > アプリクライアント"クリックで表示される「アプリクライアント ID」をコピーしておく。

    1-6-dynamo-cognito

  2. ユーザー追加
    前の画面から引き続き"全般設定 > ユーザーとグループ"をクリックする。

    2-1-dynamo-cognito

    「ユーザーの作成」クリックで表示されるダイアログからユーザー情報を入力し、「ユーザーの作成」をクリックする。今回はユーザー名=Eメールとした。入力した仮パスワードは"Eメール"に指定したメールアドレス宛に送信されるので正常に受信できることを確認しておく。

    2-2-dynamo-cognito

    ユーザー作成直後のステータスは"FORCE_CHANGE_PASSWORD"となっている。

    2-3-dynamo-cognito

    開発者ガイド:ユーザーアカウントのサインアップと確認 の「パスワードの強制変更」のセクションに以下の記述があるのでステータスとしては問題ないようだ。
    ユーザーアカウントが確認され、ユーザーは一時パスワードを使用してサインインできますが、初回のサインイン時にパスワードを変更して、他の操作を行う前に値を変更できないようにする必要があります。 管理者または開発者が作成したユーザーアカウントは、この状態から開始します。
  3. IDプール作成(フェデレーティッドアイデンティティの管理)
    開発者ガイド:ユーザープールをフェデレーテッドアイデンティティと統合するのとおり、クライアントアプリ(JSコード)でユーザープールのユーザーがAWSリソース(DynamoDB)にアクセスできるよう、ユーザープールと関連付けられたユーザーを許可するようCognitoフェデレーテッドアイデンティティを設定する。

     

    IDプール名:DynamoDBROAccess
    認証プロバイダー:Cognito
    ユーザープールID:1.で作成した「ユーザープールID」
    アプリクライアントID:1.で作成した「アプリクライアントID」

    を指定し、「プールの作成」をクリックする。

    3-1-dynamo-cognito

    IAMロールを設定する画面に遷移するので、認証されている/いない場合のそれぞれについてIAMロール、ポリシーを割り当てる。

    3-2-dynamo-cognito

    認証されている場合のロール名:Cognito_DynamoDBROAccessAuth_Role
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": [
            "mobileanalytics:PutEvents",
            "cognito-sync:*",
            "cognito-identity:*",
            "application-autoscaling:DescribeScalableTargets",
            "application-autoscaling:DescribeScalingActivities",
            "application-autoscaling:DescribeScalingPolicies",
            "cloudwatch:DescribeAlarmHistory",
            "cloudwatch:DescribeAlarms",
            "cloudwatch:DescribeAlarmsForMetric",
            "cloudwatch:GetMetricStatistics",
            "cloudwatch:ListMetrics",
            "datapipeline:DescribeObjects",
            "datapipeline:DescribePipelines",
            "datapipeline:GetPipelineDefinition",
            "datapipeline:ListPipelines",
            "datapipeline:QueryObjects",
            "dynamodb:BatchGetItem",
            "dynamodb:DescribeTable",
            "dynamodb:GetItem",
            "dynamodb:ListTables",
            "dynamodb:Query",
            "dynamodb:Scan",
            "dynamodb:DescribeReservedCapacity",
            "dynamodb:DescribeReservedCapacityOfferings",
            "dynamodb:ListTagsOfResource",
            "dynamodb:DescribeTimeToLive",
            "dynamodb:DescribeLimits",
            "iam:GetRole",
            "iam:ListRoles",
            "sns:ListSubscriptionsByTopic",
            "sns:ListTopics",
            "lambda:ListFunctions",
            "lambda:ListEventSourceMappings",
            "lambda:GetFunctionConfiguration"
          ],
          "Resource": [
            "*"
          ]
        }
      ]
    }
    
    認証されていない場合のロール名:Cognito_DynamoDBROAccessUnauth_Role
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": [
            "mobileanalytics:PutEvents",
            "cognito-sync:*"
          ],
          "Resource": [
            "*"
          ]
        }
      ]
    }
    
    「Amazon Cognito での作業開始」画面で右上の「IDプールの編集」をクリックする。

    3-3-dynamo-cognito

    後述のコード作成のため、「IDプールのID」をコピーしておく。画面上、認証されていないロール、認証されたロールの順で指定するようになっているので逆に指定しないようにご注意を。

    3-4-dynamo-cognito

コード作成

index.htmlには特に処理を記述していないため、リンク先を参照いただきたい。
処理の本体であるmain.jsについて以下にコードを掲載する。
$(function(){
	// 定数
	var REGION = 'us-west-2';
	var ID_POOL_ID = 'フェデレーティッドアイデンティティの[IDプールのID]';
	var COGNITO_IDP = 'cognito-idp.us-west-2.amazonaws.com/ユーザープールの[プールID]';
	var COGNITO_USER_POOL_PARAMS_WITH_PARANOIA = {
		UserPoolId: 'ユーザープールの[プールID]',
		ClientId: '[アプリクライアントID]',
		Paranoia : 7 /* between 1 - 10らしい */
	};
	var COGNITO_USER_POOL_PARAMS = {
		UserPoolId: 'ユーザープールの[プールID]',
		ClientId: '[アプリクライアントID]'
	};
	// 環境設定
	AWS.config.region = REGION;
	AWS.config.credentials = new AWS.CognitoIdentityCredentials({ IdentityPoolId: ID_POOL_ID });
	AWSCognito.config.region = REGION;
	AWSCognito.config.credentials = new AWS.CognitoIdentityCredentials({ IdentityPoolId: ID_POOL_ID });

	var procResetPwdFlow = function(cognitoUser,callbackDialog){

		cognitoUser.forgotPassword({
			onSuccess: function (result) {
				callbackDialog({ state: 'reauth', detail: "" });
			},
			onFailure: function(err) {
				callbackDialog({ state: 'failure', detail: err });
			},
			inputVerificationCode() {
				while(true){
					var verifyCode = prompt('確認コードを入力してください。' ,'');
					var newPwd = prompt('新しいパスワードを入力してください。 ' ,'');
					if(verifyCode && newPwd){
						cognitoUser.confirmPassword(verifyCode, newPwd, this);
						break;
					}
				}
			}
		});
	}
	var procAuthFlow = function(getDlgValue,callbackDialog,mainCallback){

		var authData = new Object();
		authData['Username'] = getDlgValue('name');
		authData['Password'] = getDlgValue('password');

		var authenticationDetails = 
			new AWSCognito.CognitoIdentityServiceProvider.AuthenticationDetails(authData);

		var userData = new Object();
		userData['Username'] = authData['Username'];
		userData['Pool'] = 
			new AWSCognito.CognitoIdentityServiceProvider.CognitoUserPool(COGNITO_USER_POOL_PARAMS_WITH_PARANOIA);

		var cognitoUser = 
			new AWSCognito.CognitoIdentityServiceProvider.CognitoUser(userData);

		cognitoUser.authenticateUser(authenticationDetails, {
			onSuccess: function (authresult) {
				callbackDialog({state:'success', detail:authresult});
				procSession(authresult, mainCallback);
			},
			onFailure: function(err) {
				if(err.code == "PasswordResetRequiredException"){
					procResetPwdFlow(cognitoUser,callbackDialog);
				} else {
					callbackDialog({ state: 'failure', detail: err});
				}
			},
			newPasswordRequired: function(userAttributes, requiredAttributes) {
				while(true){
					var newPwd = prompt('新しいパスワードを入力してください。 ' ,'');
					if(newPwd){
						var attributesData = {};
						cognitoUser.completeNewPasswordChallenge(newPwd, attributesData, this);
						break;
					}
				}
			}
		});
	}
	var procSession = function(userSession, mainCallback){
		var jwtToken = userSession.getIdToken().getJwtToken();

		var logins = new Object();
		logins[COGNITO_IDP] = jwtToken;
		var params = new Object();
		params['IdentityPoolId'] = ID_POOL_ID;
		params['Logins'] = logins;

		AWS.config.credentials = new AWS.CognitoIdentityCredentials(params);
		AWS.config.credentials.get(mainCallback);
	}
	var signIn = function(mainCallback){

		var userPool = 
			new AWSCognito.CognitoIdentityServiceProvider.CognitoUserPool(COGNITO_USER_POOL_PARAMS);
		var cognitoUser = userPool.getCurrentUser();
		if(cognitoUser){
			cognitoUser.getSession(function(err, signInUserSession) {
				if (signInUserSession) {
					procSession(signInUserSession,mainCallback);
				} else {
					$("#dialog").dialog("open");
				}
			});
		} else {
			$("#dialog").dialog("open");
		}
	}
	var getToken = function(table,param) {
		var dfd = $.Deferred();

		table.getItem(param,function(err,data){
			if(err){
				return dfd.reject(err);
			} else {
				var token = data.Item.data.S;
				dfd.resolve(token);
			}
		});
		return dfd.promise();
	}

	var getItemFromDB = function(err){

		if(err){
			alert("getItemFromDB err["+err+"]");
			return ;
		}
		var table = new AWS.DynamoDB({params: {TableName: 'watsonTokens'}, region: REGION });
		$.when(
			getToken(table, { Key:{ tokenType:{S:'STTtoken'}}}),
			getToken(table, { Key:{ tokenType:{S:'TTStoken'}}})
		).done(function(STTtoken,TTStoken){
			alert("STT Token\n---\n"+STTtoken+"\n---\n\nTTS Token\n---\n"+TTStoken+"\n---");
		})
		.fail(function(res){
			alert(res);
		});
	}
	$("#dialog").dialog({
		autoOpen: false,
		modal: true,
		show: 'explode',
		buttons: {
			'ログイン': function() {
				procAuthFlow(getDlgValue, controlDialog, getItemFromDB);
			},
			'キャンセル': function() {
				$(this).dialog('close');
			},
		}
	});
	var getOpenDlg = function(){
		if($('#dialog').dialog('isOpen') === true){
			return $('#dialog');
		}
		return null;
	}
	var controlDialog = function(result){
		var dlg = getOpenDlg();
		if(dlg){
			if(result.state == 'success'){
				dlg.dialog('close');
			} else if(result.state == 'failure'){
				alert(result.detail.message);
			} else if(result.state == 'reauth'){
				dlg.dialog('close');
				$("#dialog").dialog('open');
			} else {
				dlg.dialog('close');
			}
		}
	}
	var getDlgValue = function(id){
		return $('#'+id).val();
	}
	signIn(getItemFromDB);
});

動作確認

Windowsデスクトップ上にHTMLファイルを配置し、動作を確認した。今回はユーザーのステータス遷移を確認したかったため、DynamoDBへのアクセスを確認する前にちょっと寄り道して、仮パスワードからのログイン、パスワードリセットからのログインの流れとなっている。

  1. 初回は仮パスワードでログインを実行する。
    4-1-dynamo-cognito
  2. パスワード変更画面に遷移するので、新パスワード(1)を設定する。
    4-2-dynamo-cognito
  3. 新パスワード(1)設定後、AWSコンソールから確認するとステータスが"CONFIRMED"に変わっていることが分かる。
    4-3-dynamo-cognito
  4. 一覧からユーザー名をクリックし、「パスワードをリセットする」をクリックしてみる。
    4-4-dynamo-cognito
  5. ステータスが"RESET_REQUIRED"になった。
    4-5-dynamo-cognito
  6. ログイン画面が出る事を期待し、index.htmlをブラウザからリロードしたが何も出ないので、F12でデベロッパーツールを起動し、Local Strageに格納されている値をクリアしてみる。再度リロードしたらログイン画面が出た!(けどこれで良い?)
    4-6-1-dynamo-cognito4-6-2-dynamo-cognito
  7. 2.で設定したパスワードを入力すると、確認コードと新パスワード(2)を入力するプロンプトを表示する。確認コードはメールで送信された。(SMSについては未確認)
    4-7-dynamo-cognito
    4-8-1-dynamo-cognito
  8. 再度ログイン画面が表示されるが、AWSコンソール上ではすでにステータスは"CONFIRMED"になっていた。
    4-8-2-dynamo-cognito
  9. 新パスワード(2)でログイン後、JSコードからDynamoDBへアクセスし、格納されている値を取得できている事が確認できた。
    4-9-dynamo-cognito
今回のキーワード:半年前の自分は他人

-Amazon Cognito, Amazon DynamoDB, AWS IAM

Bitnami