Cognitoで未認証ユーザーと認証ユーザー(Facebook)でそれぞれAWSリソースを操作してみる

JavaScriptやiOS、AndroidアプリからAWSリソースを利用する場合にアクセスキーなどをそのままコードに書くと不正利用されてしまう可能性があります。

上記よりコード上にキーを書かず、CognitoというAWSが提供するサービスを使って認証することによってセキュアにAWSリソースの操作ができます。

[AWS Black Belt Techシリーズ Amazon Cognito / Amazon Mobile Analytics](http://www.slideshare.net/AmazonWebServicesJapan/aws-black-belt-tech-amazon-cognito-amazon-mobile-analytics)

Cognitoでは認証をしていないユーザー(ゲストユーザー）と認証済みのユーザー(Facebook,Google,Twitterなど）にそれぞれロールを設定することができるので今回試してみました。

+ 未認証ユーザーはNode.jsからS3のバケット一覧を取得
+ 認証ユーザーはFacebook認証を行い、JavaScriptでEC2の一覧を取得

というのをやってみました。

# 参考

+ [Amazon CognitoをJavaScriptから使ってみる](http://qiita.com/KenTamagawa/items/36238d6264ee14c2b50f)

# 前提

上記参考リンクにある以下を実施する。

+ Amazon Cognitoのアイデンティティプールを作成(tokyoリージョンがないので注意)
+ AWS SDK for Node.jsの利用ができるようにする(npm install aws-sdk)
+ Cognitoの未認証のロールにS3アクセスの権限を付与
+ Cognitoの認証ロールにEC2の情報取得権限を付与

# 未認証ユーザー(ゲストアカウント）を試す

以下のコードを実行してみます。xxxxxとなっている部分は適宜自分の設定に変更して下さい。

```cognito.js
var AWS = require('aws-sdk');

var awsRegion = "us-east-1";
var cognitoParams = {
    IdentityPoolId: "us-east-1:xxxxxxxxxxxxxxxxxx"
};

AWS.config.region = awsRegion;
AWS.config.credentials = new AWS.CognitoIdentityCredentials(cognitoParams);
AWS.config.credentials.get(function(err) {
  if (!err) {
    console.log("Cognito Identity Id: " + AWS.config.credentials.identityId);
  }
});

var s3 = new AWS.S3();
s3.listBuckets(function(err, data) {
  if (err) console.log(err, err.stack);
  else console.log(data);
});

```

実行するとS3のバケット一覧が取得出来ます。

```
$node cognito.js
```

次に未認証ユーザーには権限を付与していないEC2の一覧を取得するように変更してみます。

```cognito.js
/*
var s3 = new AWS.S3();
s3.listBuckets(function(err, data) {
  if (err) console.log(err, err.stack);
  else console.log(data);
});
*/

var ec2 = new AWS.EC2();
params = {}
ec2.describeInstances(params, function(err, data) {
  if (err) console.log(err, err.stack); // an error occurred
  else     console.log(data);           // successful response
});

```

権限が付与されていないので実行するとエラーになります。
正しい挙動ですね。

```bash
$node hoge.js
{ [UnauthorizedOperation: You are not authorized to perform this operation.]
  message: 'You are not authorized to perform this operation.',
  code: 'UnauthorizedOperation',
  time: Mon Sep 07 2015 18:05:46 GMT+0900 (JST),
  statusCode: 403,
  retryable: false,
  retryDelay: 30 } 'UnauthorizedOperation: You are not authorized to perform this operation.\n    at Request.extractError ・・・・
```

# 認証ユーザー(Facebook)を試す

[Authentication in the Browser with Amazon Cognito and Public Identity Providers](https://blogs.aws.amazon.com/javascript/post/TxTUNTVES4AL15/Authentication-in-the-Browser-with-Amazon-Cognito-and-Public-Identity-Providers)

Facebook認証はNode.jsでなく、JavaScriptで実施します。

## Facebookアプリの作成

以下を参考にFacebookアプリを作成します。
Facebookのアカウントが必要になると思います。

[[AWS][iOS] Amazon Cognito のモバイルユーザー認証 & データ同期 を iOS で使ってみた](http://dev.classmethod.jp/smartphone/amazon-cognito-ios-auth/)

作成後に確認できるAppIDをメモしておきます。

## CognitoのIdentity PoolでFacebookアプリIDを登録する

Cognitoのdentity Poolで先ほど作成したFacebookアプリのAppIDを登録します。

![Screen Shot 2015-09-07 at 11.12.03 PM.png](https://qiita-image-store.s3.amazonaws.com/0/20193/635ba976-2791-5071-6fd6-5133feb15cca.png "Screen Shot 2015-09-07 at 11.12.03 PM.png")

## S3へHTMLをアップロードする

以下を参考にS3へFacebookログイン用のHTMLをアップロードします。

S3では以下を実施します。

+ バケットを作成
+ Enable website hostingを設定
+ 対象バケットのバケットポリシーで匿名ユーザーからのアクセスを許可
+ [Facebook Login for the Web with the JavaScript SDK](https://developers.facebook.com/docs/facebook-login/web)
にある以下のようなHTMLをアップロードします。なお、{your-app-id}となっている部分は自分のFacebookアプリのIDに変更してください。


```index.html
<!DOCTYPE html>
<html>
<head>
<title>Facebook Login JavaScript Example</title>
<meta charset="UTF-8">
</head>
<body>
<script>
  // This is called with the results from from FB.getLoginStatus().
  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.
      testAPI();
    } else if (response.status === 'not_authorized') {
      // The person is logged into Facebook, but not your app.
      document.getElementById('status').innerHTML = 'Please log ' +
        'into this app.';
    } else {
      // The person is not logged into Facebook, so we're not sure if
      // they are logged into this app or not.
      document.getElementById('status').innerHTML = 'Please log ' +
        'into Facebook.';
    }
  }

  // This function is called when someone finishes with the Login
  // Button.  See the onlogin handler attached to it in the sample
  // code below.
  function checkLoginState() {
    FB.getLoginStatus(function(response) {
      statusChangeCallback(response);
    });
  }

  window.fbAsyncInit = function() {
  FB.init({
    appId      : '{your-app-id}',
    cookie     : true,  // enable cookies to allow the server to access 
                        // the session
    xfbml      : true,  // parse social plugins on this page
    version    : 'v2.2' // use version 2.2
  });

  // Now that we've initialized the JavaScript SDK, we call 
  // FB.getLoginStatus().  This function gets the state of the
  // person visiting this page and can return one of three states to
  // the callback you provide.  They can be:
  //
  // 1. Logged into your app ('connected')
  // 2. Logged into Facebook, but not your app ('not_authorized')
  // 3. Not logged into Facebook and can't tell if they are logged into
  //    your app or not.
  //
  // These three cases are handled in the callback function.

  FB.getLoginStatus(function(response) {
    statusChangeCallback(response);
  });

  };

  // Load the SDK asynchronously
  (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/en_US/sdk.js";
    fjs.parentNode.insertBefore(js, fjs);
  }(document, 'script', 'facebook-jssdk'));

  // Here we run a very simple test of the Graph API after login is
  // successful.  See statusChangeCallback() for when this call is made.
  function testAPI() {
    console.log('Welcome!  Fetching your information.... ');
    FB.api('/me', function(response) {
      console.log('Successful login for: ' + response.name);
      document.getElementById('status').innerHTML =
        'Thanks for logging in, ' + response.name + '!';
    });
  }
</script>

<!--
  Below we include the Login Button social plugin. This button uses
  the JavaScript SDK to present a graphical Login button that triggers
  the FB.login() function when clicked.
-->

<fb:login-button scope="public_profile,email" onlogin="checkLoginState();">
</fb:login-button>

<div id="status">
</div>

</body>
</html>
```

うまく設定できればS3のエンドポイントにブラウザでアクセスすると以下のような画面が表示されます。

![Screen Shot 2015-09-07 at 11.49.37 PM.png](https://qiita-image-store.s3.amazonaws.com/0/20193/5fe29908-28a8-3305-d569-2e4e113a2a13.png "Screen Shot 2015-09-07 at 11.49.37 PM.png")


ただし、この状態ではFacebookへのログインができません。というのもFacebookアプリでは指定されたURL以外からのアクセスでは認証できないためです。

というわけでS3のエンドポイントをメモし、FacebookDeveloperの画面を表示し、接続元のURLを指定します。

+ Settings->Add Platform->Web Site

設定後、Log Inボタンを押下することでログインができます。

![Screen Shot 2015-09-07 at 11.54.28 PM.png](https://qiita-image-store.s3.amazonaws.com/0/20193/0812213e-dbba-6e64-a1da-422c9dadbae7.png "Screen Shot 2015-09-07 at 11.54.28 PM.png")

# Cognitoとの連携

ChromeのDeveloperToolsなどで確認すると`FB.getLoginStatus`のレスポントの中にaccessTokenが入っているのが確認できるのでこれを利用してCognitoの権限を取得し、EC2のリストを取得するようにしてみます。

最終的に以下のように変更しました。

```
<!DOCTYPE html>
<html>
<head>
<title>Facebook Login JavaScript Example</title>
<meta charset="UTF-8">
</head>
<body>
<script src="https://sdk.amazonaws.com/js/aws-sdk-2.1.49.min.js"></script>
<script>
  function getEC2(token) {
    console.log(token);
    var creds = new AWS.CognitoIdentityCredentials({
      IdentityPoolId: 'us-east-1:xxxxxx',
      Logins: { 
        'graph.facebook.com': token
      }
    });
     
    AWS.config.update({
      region: 'us-east-1',
      credentials: creds
    });
    
    var ec2 = new AWS.EC2();
    var params = {}
    ec2.describeInstances(params, function(err, data) {
      if (err) console.log(err, err.stack); // an error occurred
      else     console.log(data);           // successful response
    });
  }
  // This is called with the results from from FB.getLoginStatus().
  function statusChangeCallback(response) {
    console.log('statusChangeCallback');
    console.log(response);
    // The response object is returned with a status field that lets the
    // app know the curent 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.
      getEC2(response.authResponse.accessToken);
      testAPI();
    } else if (response.status === 'not_authorized') {
      // The person is logged into Facebook, but not your app.
      document.getElementById('status').innerHTML = 'Please log ' +
        'into this app.';
    } else {
      // The person is not logged into Facebook, so we're not sure if
      // they are logged into this app or not.
      document.getElementById('status').innerHTML = 'Please log ' +
        'into Facebook.';
    }
  }

  // This function is called when someone finishes with the Login
  // Button.  See the onlogin handler attached to it in the sample
  // code below.
  function checkLoginState() {
    FB.getLoginStatus(function(response) {
      statusChangeCallback(response);
    });
  }

  window.fbAsyncInit = function() {
  FB.init({
    appId      : 'xxxxxxxx',
    cookie     : true,  // enable cookies to allow the server to access 
                        // the session
    xfbml      : true,  // parse social plugins on this page
    version    : 'v2.2' // use version 2.2
  });

  // Now that we've initialized the JavaScript SDK, we call 
  // FB.getLoginStatus().  This function gets the state of the
  // person visiting this page and can return one of three states to
  // the callback you provide.  They can be:
  //
  // 1. Logged into your app ('connected')
  // 2. Logged into Facebook, but not your app ('not_authorized')
  // 3. Not logged into Facebook and can't tell if they are logged into
  //    your app or not.
  //
  // These three cases are handled in the callback function.

  FB.getLoginStatus(function(response) {
    statusChangeCallback(response);
  });

  };

  // Load the SDK asynchronously
  (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/en_US/sdk.js";
    fjs.parentNode.insertBefore(js, fjs);
  }(document, 'script', 'facebook-jssdk'));

  // Here we run a very simple test of the Graph API after login is
  // successful.  See statusChangeCallback() for when this call is made.
  function testAPI() {
    console.log('Welcome!  Fetching your information.... ');
    FB.api('/me', function(response) {
      console.log('Successful login for: ' + response.name);
      document.getElementById('status').innerHTML =
        'Thanks for logging in, ' + response.name + '!';
    });
  }
</script>

<!--
  Below we include the Login Button social plugin. This button uses
  the JavaScript SDK to present a graphical Login button that triggers
  the FB.login() function when clicked.
-->

<fb:login-button scope="public_profile,email" onlogin="checkLoginState();">
</fb:login-button>

<div id="status">
</div>

</body>
</html>
```

変更点は以下になります。

+ AWS SDK for JavaScriptを読み込む処理を追加
+ getEC2というfunctionを追加。この中でFacebook認証によって取得したアクセストークンを利用してCognitoの認証を行い、その権限でEC2一覧を取得し、Console.logに書く
+ 処理の途中でgetEC2を呼び出す処理を追加

上記によってconsole.logにEC2の情報が入ったオブジェクトが取得できるのを確認できました。（ChromeのDevToolなどで確認)

![Screen Shot 2015-09-08 at 10.04.50 PM.png](https://qiita-image-store.s3.amazonaws.com/0/20193/0117a366-90e7-dab9-7648-deb3f666ced5.png "Screen Shot 2015-09-08 at 10.04.50 PM.png")


# まとめ

+ Cognitoを使うことでアクセスキーなどをファイルに埋め込まず、AWSへのリソースアクセスを行う事ができました
+ 認証、未認証によってアクセスできるリソースを変えることが出来ました