[ Serverless ]  Cognito、S3、Lambdaで認証機能付きのWebサイトを作ってみました

# 前置き
　半年前くらいに、S3、Cognito、Lambdaを使って、問い合わせフォームが作ったという投稿を見て、初めてCognitoを触ってみました。その時の個人的な感想として、モバイルアプリを作らなくても、S3やLambdaなどと組み合わせてみれば、「ちょっとしたServerlessのWebサイトを作れるじゃん！」と感じました...が、当時、Cognitoには[「User Pool」](https://aws.amazon.com/jp/blogs/news/new-user-pools-for-amazon-cognito/)がまだ提供されていないこともあって、その構成には**認証機能**が実装されていませんでした。だれでもそのS3に公開されているWebページから投稿できてしまいます。便利といえば便利ですが、S3 put攻撃への心配や一部の人にのみ使ってもらいたい時にはちょっと物足りない部分があります。
　つい最近(三ヶ月前かな)、CognitoのUser Poolが使えるようになってから、簡単に認証できるWebサイトを作りたいなあ~と思って、二三日前からCognitoをもう一度触り始めました。
　感想として、「ちゃんと認証とかも簡単にできるようになって、めっちゃ便利だな」ということでした!

# 概要
　今回は認証付きのWebサイトを作ってみました。
　認証したユーザから申請された内容をS3に保存するとともに、LambdaとSlackを連携して、管理者のチャンネルに通知します。
　全体のイメージは下の図のものです。

<img width="881" alt="スクリーンショット 0028-07-02 0.31.50.png" src="https://qiita-image-store.s3.amazonaws.com/0/117156/42071daf-73fa-cbca-40a9-36f79a610dc3.png">

　今回利用したAWSサービス&ライブラリは主に以下になります。

- Cognito
  - User Pools、identity pools
- S3
- Lambda
- Slack
- [amazon-cognito-identity-js](https://github.com/aws/amazon-cognito-identity-js)
  - このライブラリが本当に便利すぎて、感動しました　(T_T)
- [jquery-2.2.3.min.js](https://code.jquery.com/jquery-2.2.3.min.js)

# 流れの説明
　やっていることの流れをざっくり説明します。

 - ① : ユーザがS3においてあるページにアクセスし、ログイン情報をCognitoに送信

 - ② → ③ : CognitoのUser Pollにて認証を行い、Access TokenとID Tokenを返却

 - ④ → ⑤ : Token情報をCognitoのIdentityに送信し、AWSのサービスへのアクセス権限を払い出してもらう

 - ⑥ → ⑦ : ページに投稿したら、S3のBucketに保存され、S3がLambdaにイベント通知
 
 - ⑧ → ⑨: LambdaでS3に保存されたファイルを取得し、その内容を管理者用のSlackのチャンネルに通知

# プロトタイプ
　自分が作ったプロトタイプ(今回は図の① → ⑥までのもの)を共有します。
　試行錯誤しながら作ったものですので、コード自体が美しくありません、ご理解ください！

## プロトタイプ用のCognito User Pool

　クラメソさんの記事を参考に、簡単に作れますので、説明を省きます。
　[[新機能] Amazon Cognito に待望のユーザー認証基盤「User Pools」が追加されました！](http://dev.classmethod.jp/cloud/aws/cognito-user-pool/)

　 ↓↓↓↓↓↓自分が作ったUser Poolです、必須の属性として emailを設定しています。
　　　　　　　　**Pool IdとApp client id**を利用しますので、メモりましょう！
　<img width="1754" alt="1.pic_hd.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/117156/baa631fb-89ca-2518-e412-50ba43ce1737.jpeg">
<img width="1726" alt="2.pic_hd.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/117156/14d1f7e9-321c-17c1-1119-3e258d857ba6.jpeg">

## プロトタイプ用のCognito Identity Pool
　簡単に作成できますので、詳細な作成手順を省きますが、下記4点を説明します。
　1. Unauthenticated identitiesのボックスにチェックを入れてください
　　(入れないと、あとで**認証できなくなります**)
　2. 後は先程メモったUser Pool IDとApp Client IDを入れる必要があります。
　3. IdentityPoolIdを控えておきましょう(あとで使います)
　4. S3にputできるように、Authenticated roleにs3:PutObjectを追加してください
 
<img width="1782" alt="4.pic_hd.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/117156/5451157a-15c2-3abc-601d-45c0553c9000.jpeg">
<img width="1778" alt="11.pic_hd.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/117156/4d3883a2-f987-4aa0-d53b-39f536677831.jpeg">


## ユーザ登録ページ
　利用者はE-mailとパスワードを入力し、「Sign up」ボタンを押すと、AWSから認証コードが送られてきます。その認証コードで、ユーザを有効化します。
<img width="1204" alt="10.pic.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/117156/a0a61124-8bcd-aea5-bc9f-43737414b139.jpeg">

↓↓↓↓↓↓↓↓↓　AWSから認証コードが送られてきました。
<img width="564" alt="スクリーンショット 0028-07-02 8.36.30.png" src="https://qiita-image-store.s3.amazonaws.com/0/117156/2f0b3ee0-c856-6a3d-a1b8-a92b05d68815.png">

ユーザ登録について

1. [amazon-cognito-identity-js](https://github.com/aws/amazon-cognito-identity-js)のコードを利用します。まだv0.9.0ですが、非常に参考になりました。皆さんも是非使ってみてください。
1. 作成したHTMLファイルをS3に格納して、publicします、静的ウェブサイトホスティングを設定してくださいね
1. 下記のamazon-cognito-identity-jsに必要なライブラリをまとめてS3格納します。
  - jsbn.js
  - jsbn2.js
  - sjcl.js
  - moment.js
  - aws-cognito-sdk.min.js
  - amazon-cognito-identity.min.js
1. プロトタイプですので、UIとかあまり考えていません。Bootstrapとか使って、リッチにしていいかもしれないですね。

　コードは下記の通りです。



```signup.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <title>Sign Up</title>
  <!-- aws sdk //-->
  <script src="https://sdk.amazonaws.com/js/aws-sdk-2.3.8.min.js"></script>
  
  <!-- aws cognito sdk(beta)と必要なライブラリ //-->
  <script src="***/jsbn.js"></script>
  <script src="***/jsbn2.js"></script>
  <script src="***/sjcl.js"></script>
  <script src="***/moment.js"></script>
  <script src="***/aws-cognito-sdk.min.js"></script>
  <script src="***/amazon-cognito-identity.min.js"></script>

  <!-- jquery //-->
  <script src="https://code.jquery.com/jquery-2.2.3.min.js" integrity="sha256-a23g1Nt4dtEYOj7bR+vTu7+T8VP13humZFBJNIYoEJo=" crossorigin="anonymous"></script>
</head>
<body>

<div id="form">
  <form>
    <h2>ユーザ登録</h2>
    <label>E-mail</label>
    <input type="text" id="email"></input>
    <label>パスワード</label>
    <input type="password" id="password"></input>
    <input type="button" id="signup" value="Sign Up"></input>
  </form>
</div>
<div style="display: none" id="verification">
  <form>
    <label>認証コード</label>
    <input type="text" id="verification-code"></input>
    <input type="hidden" id="username"></input>
    <input type="button" id="submit" value="Submit"></input>
  </form>
</div>

<script type="text/javascript">

var poolData = {
            UserPoolId : 'us-east-1_*****',
            ClientId : '5ckd9*********iteojtg'
    };

jQuery(document).ready(function($){
  AWS.config.region = 'us-east-1'; // Region
  AWS.config.credentials = new AWS.CognitoIdentityCredentials({
      IdentityPoolId: 'us-east-1:a999fc0e-b*************5d4fb6ffa'
  });

  // Cognito User Pool Id
  AWSCognito.config.region = 'us-east-1';
  AWSCognito.config.credentials = new AWS.CognitoIdentityCredentials({
      IdentityPoolId: 'us-east-1:a999fc0e-b*************5d4fb6ffa'
  });

//ユーザ登録時,signupボタンが押される時のfunction
$('#signup').click(function() {

  var userPool = new AWSCognito.CognitoIdentityServiceProvider.CognitoUserPool(poolData);

  var attributeList = [];

  var dataEmail = {
      Name : 'email',
      Value : $('#email').val()
  };

  var attributeEmail = new AWSCognito.CognitoIdentityServiceProvider.CognitoUserAttribute(dataEmail);

  attributeList.push(attributeEmail);

  userPool.signUp($('#email').val(), $('#password').val(), attributeList, null, function(err, result){
      if (err) {
          alert(err);
          console.log(err);
          return;
      }
      else{
        var cognitoUser = result.user;
        $("#verification").show();
        $("#username").val(cognitoUser.getUsername());
        console.log('user name is ' + cognitoUser.getUsername());
        console.log('call result: ' + result);
      }
  });
});

//ユーザ認証時、submitボタンが押される時のfunction
$('#submit').click(function() {

  var userPool = new AWSCognito.CognitoIdentityServiceProvider.CognitoUserPool(poolData);

  var userData = {
      Username : $('#username').val(),
      Pool : userPool
     };

  var cognitoUser = new AWSCognito.CognitoIdentityServiceProvider.CognitoUser(userData);
      cognitoUser.confirmRegistration($('#verification-code').val(), true, function(err, result) {
      if (err) {
          alert(err);
          return;
      }
      console.log('call result: ' + result);
  });
});


});
</script>

</body>
</html>

```

## 認証付きの申請フォーム
<img width="642" alt="スクリーンショット 0028-07-02 9.57.58.png" src="https://qiita-image-store.s3.amazonaws.com/0/117156/8fa72723-75d2-d502-2aa1-c1adcf6a13cc.png">
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

<img width="1730" alt="15.pic.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/117156/320e6e13-f67f-a5b5-e988-50208ea5600b.jpeg">

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

<img width="1494" alt="16.pic_hd.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/117156/14c88ee0-3c11-55e6-8a5f-ca77cca43280.jpeg">



　ログイン画面と申請フォームは２つのページに見えますが、実はjQueryを使って１ページにまとめてあります。
　ログインしてから表示される申請フォームについてですが、検証する際に、AccessKeyなどの情報を確認したかったので、あえてAccessKeyを表示するようにしてあります(AccessKeyをちゃんと取れていることが分かりますね)。

　コードは下記の通りです。

```webform.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <title>申請フォーム</title>
  <!-- aws sdk //-->
  <script src="https://sdk.amazonaws.com/js/aws-sdk-2.3.8.min.js"></script>
  <!-- aws cognito sdk(beta)と必要なライブラリ //-->
  <script src="***/jsbn.js"></script>
  <script src="***/jsbn2.js"></script>
  <script src="***/sjcl.js"></script>
  <script src="***/moment.js"></script>
  <script src="***/aws-cognito-sdk.min.js"></script>
  <script src="***/amazon-cognito-identity.min.js"></script>
  <!-- jquery //-->
  <script src="https://code.jquery.com/jquery-2.2.3.min.js" integrity="sha256-a23g1Nt4dtEYOj7bR+vTu7+T8VP13humZFBJNIYoEJo=" crossorigin="anonymous"></script>
</head>
<body>
  <div id="form">
    <form>
      <h2>ログイン</h2>
      <label>E-mail</label>
      <input type="text" id="email"></input>
      <label>パスワード</label>
      <input type="password" id="password"></input>
      <input type="button" id="login" value="ログイン"></input>
    </form>
  </div>
<div style="display: none">
  <form>
    <h2>申請フォーム</h2>
    <div>
      <label>E-mail</label>
      <input id="master-account" readonly></input>
    </div>
    <div>
      <label>Access Key</label>
      <input id="accessKey"></textarea>
    </div>
    <div>
      <label>Q1:</label>
      <input type="text" name="q1" id="q1">
    </div>
    <div>
      <label>Q2:</label>
      <input type="text" name="q2" id="q2">
    </div>
    <br/>
    <input type="button" id="submit" value="Submint"></input>
  </form>
</div>


<script type="text/javascript">

// Region
AWS.config.region = 'us-east-1';
AWSCognito.config.region = 'us-east-1';
AWSCognito.config.credentials = new AWS.CognitoIdentityCredentials({
    IdentityPoolId: 'us-east-1:a999fc0e-ba*************5d4fb6ffa'
});

jQuery(document).ready(function($){
$('#login').click(function() {

    var authenticationData = {
        Username : $('#email').val(),
        Password : $('#password').val()
    };
    var authenticationDetails = new AWSCognito.CognitoIdentityServiceProvider.AuthenticationDetails(authenticationData);
    var poolData = {
              UserPoolId : 'us-east-1_ly******MIS',
              ClientId : '5ckd9el*************teojtg'
        };
    var userPool = new AWSCognito.CognitoIdentityServiceProvider.CognitoUserPool(poolData);
    var userData = {
        Username : $('#email').val(),
        Pool : userPool
       };
    var cognitoUser = new AWSCognito.CognitoIdentityServiceProvider.CognitoUser(userData);

    cognitoUser.authenticateUser(authenticationDetails, {
        onSuccess: function (result) {

            $("div").show();
            $('#form').hide();

            console.log('access token + ' + result.getIdToken().getJwtToken());

            AWS.config.credentials = new AWS.CognitoIdentityCredentials({
                IdentityPoolId: 'us-east-1:a999fc0e-ba*************b6ffa',
                Logins: {
                    'cognito-idp.us-east-1.amazonaws.com/us-east-1_lyc*****IS': result.getIdToken().getJwtToken()
                }
            });
            AWS.config.credentials.get(function (err) {
                if(err)
                {
                    console.log('error in autheticatig AWS'+err);
                }
                else
                {
                    // Using authenticated credentials
                    $('#accessKey').val(AWS.config.credentials.accessKeyId);
                    $('#master-account').val($('#email').val());
                }
            });
        },

        onFailure: function(err) {
            alert(err);
        },
    });
  });

  $('#submit').click(function (){

      AWS.config.update({
        accessKeyId: $('#accessKey').val()
      });

      var s3BucketName = "qiu-webform";
      var now = new Date();
      var obj = {"q1":$('#q1').val(), "q2":$('#q2').val()};
      var s3 = new AWS.S3({params: {Bucket: s3BucketName}});
      var blob = new Blob([JSON.stringify(obj, null, 2)], {type:'text/plain'});

      s3.putObject(
          {
              Key: "uploads/" + now.getTime() +".txt",
              ContentType: "text/plain",
              Body: blob
            },
            function(err, data){
              if(data !== null){
                alert("申請しました!");
              }
              else{
                alert("エラー: " + err.message);
              }
      });
    });

});
</script>

</body>
</html>

```
# まとめ
　非常に簡単ではありますが、ユーザ登録から、ログインして申請するまで(図の① → ⑥)をざっくり説明しました。
　申請内容の通知(⑦ → ⑨)については、次回にしようと思います。
　また、①から⑨までの間に、いくつかの問題に頭が悩まされましたが、無事解決できました(正確に言えば、残り１つです)。これについても、ある程度整理できたら、皆さんと共有したいと思っています。

- 2016/07/7 追記　[[ Serverless ] Cognito、S3、Lambdaで認証機能付きのWebサイトを作ってみました<トラブルシューティング編>](http://qiita.com/jobbin/items/a640bea19dfc13721246)

# 参考させて頂いたリンク
　情報を共有して頂いた方々に感謝します。皆さんも、どんどん共有していきましょう！

- [Amazon Cognito User Poolsを使って、webサイトにユーザ認証基盤を作る](http://qiita.com/horike37/items/1d522f66452d3abe1203)
- [amazon-cognito-identity-js](https://github.com/aws/amazon-cognito-identity-js)
- [S3 + Lambda + Cognitoを使って、簡単お問い合わせシステム構築](http://qiita.com/takuma_yoshida/items/3bd3af8b09307d25981a)