Node.js を使用したサーバーでは、DynamoDB へのアクセスに対して、制限をかけていませんでした。
このため、他人のIDでもDynamoDBの情報のアクセスができてしまいます。
この対策として、AWS Cognitoのユーザー認証と、IAM Role の組み合わせで、ユーザーごとのDynamoDBのアクセス制限を実現します。
サーバー環境としては、AWS EC2 のサーバーを使用する必要はないので、これ以降 AWS S3 の静的WebSiteを使用します。
ローカルPCでの動作確認は、Node.jsを使用すると、簡単にhttpサーバー環境が使用できるのでNode.js を使用したサーバーの時と同様に、Node.js の環境を使用します。

今回は、以下を実現します。

  1. Facebook をIDプロバイダとして、Cognito でユーザー認証します。
  2. Cognito認証したユーザーで、DynamoDB のアクセス制限をします。
  3. 未認証ユーザーでも、DynamoDBをアクセスできるようにします。
  4. サーバー環境として、AWS S3 の静的WebSiteを使用します。

サンプルコードは、以下からダウンロードできます。

Facebookへのログイン

Facebook を ID プロバイダーにしたユーザー認証については、AWS の以下にドキュメントがあります。 このドキュメントを参考に、以下の手順で、Facebook のログインを実装します。
  1. Facebook にアプリケーションを登録
  2. AWS Cognito の Identity pool に、外部プロバイダーとしてFacebook を登録
  3. Facebook SDK を使用してログイン処理を実装

Facebookにアプリケーションを登録

  1. Facebook のアカウントを持っていなければ、Facebook にアカウントを作成します。
  2. facebook for developersの、「新しいアプリを追加」からアプリケーションを登録します。
  3. 前回の、Node.js のサーバーの開発環境をそのまま使用してでアプリケーションを作成するため、Facebook の「設定」画面の「Add Platform」で、Website を追加します。
  4. サイト URL には、ローカルPC上のNode.js のサーバーのURL の、 http://localhost:3000 を指定します。
  5. Facebook では、Websiteを一つしか登録できないため、サーバー上にアッブロードして動作させる時は、サイト URL をサーバーのURLに変更してください。

AWS Cognito の Identity pool に、外部プロバイダーとしてFacebook を登録

  1. AWS Cognito コンソールから、「Create new Identity pool」を選択し、Cognito の identity pool を作成します。
    1. Unauthenticated role, Authenticated role は、今回は使用しないのでデフォルトのままでかまいません。
  2. すでに作成済みの場合は、「Edit Identity pool」でIdentity poolの設定を変更します。
  3. Authentication providers で Facebookタブを選択して、Facebook の AppIDを入力します。
    Facebook の AppID は、facebook for developersの設定ページで「アプリID」で確認できます。

Facebook SDK を使用してログインを実装

  1. Facebook 関連は、facebook.js にまとめました。
    checkLoginStatus 関数で、FB.getLoginStatus を呼び出しています。
    この関数のコールバックで、ログインしているかどうかを調べています。
    (function(d, s, id) {
      var js, fjs = d.getElementsByTagName(s)[0];
      if (d.getElementById(id)) return;
      js = d.createElement(s); js.id = id;
      js.src = "//connect.facebook.net/ja_JP/sdk.js";
      fjs.parentNode.insertBefore(js, fjs);
    }(document, 'script', 'facebook-jssdk'));
    
    window.fbAsyncInit = function() {
    	console.log('fbAsyncInit');
    	FB.init({
    		appId	   : '<facebook AppID>',
    		cookie	 : true,  // enable cookies to allow the server to access 
    							// the session
    		xfbml	   : true,	// parse social plugins on this page
    		version	   : 'v2.8' // use version
    	});
    	if (g_fb) {
    		g_fb.checkLoginStatus();
    	} else {
    		console.log("g_fb is null");
    	}
    };
    
    function Facebook() {
    	var m_logInController = null;
    	
    	this.init = function (logInController) {
    		console.log('facebook.init');
    		m_logInController = logInController;
    	};
    	
    	this.checkLoginStatus = function() {
    	  FB.getLoginStatus(function(response) {
    		statusChangeCallback(response);
    	  });
    	}
    
    	function statusChangeCallback(response) {
    		console.log('statusChangeCallback');
    		console.log(response);
    		// The response object is returned with a status field that lets the
    		// app know the current login status of the person.
    		// Full docs on the response object can be found in the documentation
    		// for FB.getLoginStatus().
    		if (response.status === 'connected') {
    			// Logged into your app and Facebook.
    			console.log('Welcome!  Fetching your information.... ');
    			FB.api('/me', function(response) {
    				console.log('Successful login for: ' + response.name);
    				document.getElementById('fb_status').innerHTML =
    					'Thanks for logging in, ' + response.name + '!';
    			});
    	
    			// Check if the user logged in successfully.
    			if (m_logInController) {
    				m_logInController.logIn(response.authResponse);
    			}
    		} else if (response.status === 'not_authorized') {
    			// The person is logged into Facebook, but not your app.
    			document.getElementById('fb_status').innerHTML = 'Please log ' +
    			  'into this app.';
    			if (m_logInController) {
    				m_logInController.logOut();
    			}
    		} else {
    			// The person is not logged into Facebook, so we're not sure if
    			// they are logged into this app or not.
    			document.getElementById('fb_status').innerHTML = 'Please log ' +
    			  'into Facebook.';
    			if (m_logInController) {
    				m_logInController.logOut();
    			}
    		}
    	}
    }
    		
  2. ログインボタンの配置
    1. index.html に、Facebook のログインボタンを配置します。
      				<div class="fb-login-button" data-max-rows="1" data-size="medium" data-show-faces="false" data-auto-logout-link="true" onlogin="g_fb.checkLoginStatus();"></div>
      				<div id="fb_status"></div>
      				
    2. ログインボタンを押すと、ログイン画面が表示されます。
    3. ユーザー名、パスワードを入力すると、 onlogin="g_fb.checkLoginStatus(); で指定した、facebook.js のcheckLoginStatus 関数が呼び出されます。
  3. ログイン、ログアウトの状態が変わったときに、facebookクラスのinit関数で指定したコールバックオブジェクトで通知を受けます。
    この例では、ログインすると、g_cognito.logIn、ログアウトすると、g_cognito.logOutが呼び出されます。
    1. index.html
      					g_fb = new Facebook();
      					g_fb.init(g_cognito);
      				
    2. facebook.js
      	function statusChangeCallback(response) {
      		...
      		if (response.status === 'connected') {
      			...
      			if (m_logInController) {
      				m_logInController.logIn(response.authResponse);
      			}
      		} else if (response.status === 'not_authorized') {
      			...
      			if (m_logInController) {
      				m_logInController.logOut();
      			}
      		} else {
      			...
      			if (m_logInController) {
      				m_logInController.logOut();
      			}
      		}
      				

Facebookログインからcognitoの認証

  1. FB.getLoginStatus のコールバック関数の引数のresponse.authResponse.accessToken を使用して、cognitoの認証を行います。
    1. AWS.CognitoIdentityCredentials(params) のパラメータに、Cognitoのpool Id, roleのarn, facebook のaccessTokenを指定した後、AWS.config.credentials.get を呼び出すと、Confignito Identity Id を取得できます。
    2. AWS.config に Cognito の credential を格納します。
    3. この credential の情報をこの後、DYnamoDB のアクセスで使用します。
    4. cognito.js
      this.logIn = function(credential) {
      	var params = {
      		IdentityPoolId: m_identityPoolId,
      		RoleArn: m_roleArn,
      		Logins:  {
      			'graph.facebook.com' : credential.accessToken
      		}
      	};
      	AWS.config.credentials = new AWS.CognitoIdentityCredentials(params);
      
      	AWS.config.credentials.clearCachedId();	// これがないと、ユーザー切り替え時に。get が失敗する。
      	// Obtain AWS credentials
      	AWS.config.credentials.get(function(err){
      		if (err) {
      			console.log("credentials.get error:" + err);
      			if (m_logInCallback) {
      				m_logInCallback(false, "credentials.get error code:\n" + JSON.stringify(err, null, 2));
      			}
      		} else {
      			console.log("Cognito Identity Id: " + AWS.config.credentials.identityId);
      			if (m_logInCallback) {
      				m_logInCallback(true, AWS.config.credentials.identityId);
      			}
      		}
      	});
      };
      				

Cognitoを使用した、DynamoDB のアクセス制限

Cognito認証したユーザーのみが、DynamoDBの自分のnoteデータにアクセスできるようにアクセス制限を行います。
アクセス制限は、AWS IAM コンソールで、ポリシーを作成し、ロールに割り当てることで実現します。

IAM ロールのポリシーで以下を行います。

  1. DynamoDB のテーブルに対して使用できるAPIの制限
  2. CognitoのIdentity Id を使用した、DynamoDBのレコードのアクセス制限
関連する AWS のドキュメントは、以下にあります。

  1. ポリシーの作成
    1. AWS IAM コンソールのポリシー画面で、「ポリシーの作成」をクリック
    2. 独自のポリシーを作成の「選択」をクリックして、以下をポリシードキュメントに貼りつけ、ポリシー名を入力して、「ポリシーの作成」をクリック
    3. ポリシー名を、「mynote-dynamo-cognito」とします。
      {
          "Version": "2012-10-17",
          "Statement": [
              {
                  "Effect": "Allow",
                  "Action": [
                      "dynamodb:BatchGetItem",
                      "dynamodb:BatchWriteItem",
                      "dynamodb:DeleteItem",
                      "dynamodb:GetItem",
                      "dynamodb:PutItem",
                      "dynamodb:Query",
                      "dynamodb:UpdateItem"
                  ],
                  "Condition": {
                      "ForAllValues:StringEquals": {
                          "dynamodb:LeadingKeys": [
                              "${cognito-identity.amazonaws.com:sub}"
                          ]
                      }
                  },
                  "Resource": [
                      "<DynamoDBのテーブルのARN>"
                  ]
              }
          ]
      }
      				
  2. 次に、AWS IAM コンソールのロール画面で、「新しいロールの作成」をクリックします。
  3. ロール名入力でロール名を、mynote-cognito-role とします。、
  4. 「ロールタイプの選択」で、「IDプロバイダ用のアクセスロール」をチェックして、「ウェブ ID プロバイダにアクセスを付与」を選択します。
  5. ID プロバイダに「Amazon Cognito」を選択、IDプールのID に、Cognito の Identity pool のID を入力して、「次のステップ」をクリックします。
  6. 「ロールの信頼の確認」では、「次のステップ」クリックします。
  7. ポリシーのアタッチ
    作成したポリシー(mynote-dynamo-cognito)を選択して、「次のステップ」をクリックします。
  8. 確認画面で、「ロールの作成」をクリックします。このとき、作成したロールのロールARNを控えておきます。

DynamoDB アクセス

ここまでで、Cognitoに認証での、DynamoDBの制限指定ができたので、DynamoDBをアクセスするコードを作成します。
Node.js を使用したサーバーでは、DynamoDB のアクセスをDbControllerServer.js で Node.js のサーバー経由で行っていましたが、今回は、dbControllerClientCognito.js というクラスから、 DynamoDB に直接アクセスします。
dbControllerClientCognito.js は、DbCOntrollerServer.js と同じインターフェイスにしたので、Node.js 版の index.html の DbControllerServer をDbControllerClientCognito に変えるだけで、DynamoDBのアクセス方法を変更できます。
  • index.html
    function initApp() {
    
    	...
    	
    	g_dbControllerClientCognito = new DbControllerClientCognito();
    	g_dbControllerClientCognito.init();
    	g_doc.setDbController(g_dbControllerClientCognito);
    		
DbControllerClientCognito.js で、DynamoDB にアクセスするコードは、Node.js で実装したコードと基本的に同じです。
AWS.DynamoDB をnew で生成する時に、AWS.config.credentials が゛、m_dynamoDB.config にコピーされます。
この config の情報が、mynote-dynamo-cognito のポリシーを指定したIAM role で参照され、DynamoDBのアクセスが、Cognitoで認証した IdentityId のデータしかアクセスできないように制限されます。
  1. DynamoDB の初期化
    dbControllerClientCognito.js
    var m_dynamoDB = null;
    var m_dynamodbDoc = null;
    var region = "<リージョン名>";
    var endpoint = "https://dynamodb." + region + ".amazonaws.com";
    var table = "Notes";
    
    this.init = function() {
    };
    
    this.open = function() {
    	if (m_dynamoDB === null) {
    		console.log('open');
    		m_dynamoDB = new AWS.DynamoDB();
    		m_dynamoDB.config.update({
    		  region: region,
    		  endpoint: endpoint
    		});
    		m_dynamodbDoc = new AWS.DynamoDB.DocumentClient({service:m_dynamoDB});
    	}
    };
    		
  2. Noteリストの取得の例
    dbControllerClientCognito.js
    this.getNoteList = function(userId, callback) {
    	console.log("userId", userId);
    	var params = {
    		TableName : table,
    		KeyConditionExpression: "user_id = :userId",
    		ExpressionAttributeValues: {
    			":userId": userId
    		}
    	};
    	console.log("query param ", JSON.stringify(params));
    	m_dynamodbDoc.query(params, function(err, data) {
    		if (err) {
    			var msg = JSON.stringify(err, null, 2);
    			console.log("Unable to query. Error:", msg);
    			callback(false, null, "faild to qurey:\n" + msg);
    		} else {
    			var notes = new Array();
    			console.log("Query succeeded.");
    			var itemSize = data.Items.length;
    			if (itemSize == 0) {
    				callback(true, "new", notes);
    			} else {
    				var replyCount = 0;
    				data.Items.forEach(function(item) {
    					replyCount++;
    					console.log(" -", JSON.stringify(item));
    					var note = {
    						user_id : item.user_id,
    						date_time : item.date_time,
    						text : item.text
    					};
    					notes.push(note);
    					if (replyCount >= itemSize) {
    						callback(true, "new", notes);
    					}
    				});
    			}
    		}
    	});
    };
    		

Cognito の未認証ユーザーでアクセス制限

AWS Cognito には、未認証ユーザーも使用できるようになっています。
Facebook 認証で、ログオフしたときに、この、Cognitoの未認証ユーザーでDynamoDBをアクセスをするようにします。
Identity Id を指定しないで、AWS.CognitoIdentityCredentials(params); で、credential を生成すると、毎回違ったIdentity Id で未認証ユーザーが作成されるので、一度作成した IdentityId をLocal Storage に保存しておいて2回目以降は、保存したIdentity Idでユーザーを作成するようにします。

未認証ユーザーでの認証

  • cognito.js
    this.logOut = function() {
    	if (m_unauthUserId !== null) {
    		var params = {
    			IdentityPoolId: m_identityPoolId,
    			RoleArn: m_roleArn,
    			IdentityId: m_unauthUserId
    		};
    		AWS.config.credentials = new AWS.CognitoIdentityCredentials(params);
    
    		// Obtain AWS credentials
    		AWS.config.credentials.get(function(err){
    			if (err) {
    				console.log("credentials.get error:" + err);
    				if (m_logOutCallback) {
    					m_logOutCallback(false, "credentials.get error code:\n" + JSON.stringify(err, null, 2));
    				}
    			} else {
    				console.log("Unauthenticated Cognito Identity Id: " + AWS.config.credentials.identityId);
    				if (m_logOutCallback) {
    					m_logOutCallback(true, AWS.config.credentials.identityId);
    				}
    			}
    		});
    	} else {
    		var params = {
    			IdentityPoolId: m_identityPoolId,
    			RoleArn: m_roleArn
    		};
    		AWS.config.credentials = new AWS.CognitoIdentityCredentials(params);
    
    		AWS.config.credentials.clearCachedId();	// これがないと、ユーザー切り替え時に。get が失敗する。
    		// Obtain AWS credentials
    		AWS.config.credentials.get(function(err){
    			if (err) {
    				console.log("credentials.get error:" + err);
    				if (m_logOutCallback) {
    					m_logOutCallback(false, "credentials.get error code:\n" + JSON.stringify(err, null, 2));
    				}
    			} else {
    				console.log("Unauthenticated Cognito Identity Id: " + AWS.config.credentials.identityId);
    				if (m_logOutCallback) {
    					m_logOutCallback(true, AWS.config.credentials.identityId);
    				}
    			}
    		});
    	}
    };
    		

Local storage でのid の保存

未認証ユーザーのIdentity Id を保持するために、Local Storage を使用します。
storage.js がLocal Storage のためのクラスです。
  • myNoteDoc.js
    var StorageName = 'test.mynote-01';
    var StorageVersion = '1.0';
    var StorageKey_UNAUTH_USER_ID = 'unauth_user_id';
    		
  • index.html
    function initApp() {
    
    	console.log('initApp');
    	AWS.config = new AWS.Config({
    		region : g_region
    	});
    
    	g_cognito = new Cognito();
    	g_cognito.init(logInCallback, logOutCallback);
    	
    	g_fb = new Facebook();
    	g_fb.init(g_cognito);
    
    	g_storage = new Storage(StorageName, StorageVersion);
    	g_storage.load();
    	
    	g_doc = new MyNoteDoc();
    	g_doc.init();
    	g_doc.setStorage(g_storage);
    
    	g_view = new MyNoteView();
    	g_view.init(g_doc);
    	
    	g_dbControllerClientCognito = new DbControllerClientCognito();
    	g_dbControllerClientCognito.init();
    	g_doc.setDbController(g_dbControllerClientCognito);
    
    	setStatusFromStorage();
    }
    
    
    function setStatusFromStorage() {
    
    	var unauthUserId = g_storage.getItem(StorageKey_UNAUTH_USER_ID);
    	if (unauthUserId !== null) {
    		g_cognito.setUnauthUserId(unauthUserId);
    	}
    }
    
    		

ローカルでの動作確認

Node.js を使用したサーバーの時と同様に、ローカルPCでの動作確認は、Node.jsでローカルサーバーを起動して行います。
c:\projects\mynote>set DEBUG=mynote:* & npm start

> mynote@0.0.0 start c:\projects\mynote
> node ./bin/www

  mynote:server Listening on port 3000 +0ms
http://localhost:3000 でローカルPCのサーバーにアクセスできます。

サーバー環境の構築

Node.js を使用したサーバーは、AWS EC2 のインスタンスで、Node.js のサーバーからDynamoDBのアクセスを行いましたが、今回は、クライアント側でDynamoDB のアクセスを行うため、AWS EC2 のインスタンスを起動する必要はありません。
EC2 のインスタンスも停止してかまいません。
サーバー環境は、AWS S3 に WebSite用のバケットを生成し、そこに、index.html等のファイルを配置します。
WebSite の作成方法は、AWS のチュートリアルが参考になります。 以下、S3 での配置手順です。

S3 にバケットを作成

  1. AWS S3 コンソールで、「バケットを作成」をクリックして、バケットの作成を開始します。
  2. 「バケットの作成 - パケット名とリージョンの選択」画面で、バケット名とリージョンを指定します。
  3. 作成したバケットを選択し、「プロパティ」をクリックし、プロパティの表示から、「アクセス許可」を選択し、「バケットポリシーの追加」をクリックします。
  4. バケットポリシーエディターで以下のポリシーを入力し、「保存」をクリックします。
    {
      "Version":"2012-10-17",
      "Statement": [{
        "Sid": "Allow Public Access to All Objects",
        "Effect": "Allow",
        "Principal": "*",
        "Action": "s3:GetObject",
        "Resource": "arn:aws:s3:::作成したバケット名/*"
      }
     ]
    }		
  5. アクセス許可の「保存」をクリックして、設定内容を確定します。

WebSiteのアップロード

  1. AWS S3 コンソールで、作成したバケットを選択します。
    作成したばかりなので中身は空のはずです。
  2. 「アクション」→「アップロード」を選択します。
  3. エクスプローラ上で、mynote/public 内のファイル、ディレクトリをすべて選択して、「アップロード - ファイルとフォルダの選択」画面にドラッグアンドドロップし、「アップロードの開始」をクリックします。
  4. アップロードが完了したら、パケットの「プロパティ」をクリックし、「静的ウェブサイトホスティング」を開きます。
  5. 「ウェブサイトのホスティングを有効にする」を選択し、「インデックスドキュメント」に index.html を指定の後、「保存」をクリックします。
  6. バケットのindex.html をクリックすると、プロパティのリンクにURLが表示されます。
    このURLをクリックすると、WebSiteとして、アプリケーションを起動できます。
    また、AWS のチュートリアルにあるように、ドメイン名を登録すると便利だと思います。
AWS S3のURLは、https: なので、index,html 内のリンクに、http: があるとロード時に "Mixed Content:" エラーとなります。
"http://xxx" の記述は、"//xxx"とすることでエラーはなくなります。

Facebook のアプリケーションで登録しているWebSite の URL も AWS S3 のURLに変更する必要があります。
ただし、Facebook では、WebSite の URL をドメインで判定しているようなので、AWS S3 の同じドメインのURLからでも、アクセス可能となるようです。
最終的に、S3 でWebSite を公開する場合は、別途ドメインを取得するのがよいと思います。

サンプルコードの実行方法

  • mynote-sample-cognito.zip をダウンロードしてください。
  • ダウンロード後、zip ファイルを展開してください。
  • zipファイルの中には、Node.js のmodule は含まれていないので、npm install で、各 module のインストールが必要です。
    c:\projects>cd mynote
    c:\projects\mynote>npm install
    		
  • mynote\public\index.html のリージョン名を指定してください。
    var g_region = '<リージョン名>';
    		
  • mynote\public\js\cognito.js のIAM Role の ARN, Cognito Identity pool Id を指定してください。
    var m_roleArn = '<Role ARN>';
    var m_identityPoolId = '<cognito identity pool id>';
    		
  • mynote\NoteCreateTable.js のリージョン名を指定してください。
    var region = "<リージョン名>";
    		

次回

次は、Lambdaを使用したアプリケーション作成でAWS Lambda でDynamoDBののアクセスを行います。