AWSサーバレスアーキテクチャでCloudFrontからWAFをかけてAPI Gatewayを呼ぶ

AWSサーバレスアーキテクチャで
不正なパラメータを含んでいるリクエスト（XSS、SQLインジェクションなど）のチェックをかけたいという要望があったので
WAF（Web Application Firewall）をかけて呼び出ししてみます。
WAFをかけるためには必然的にCloudFrontを利用することになります。
構図としては下記のような構図になります。
（この図では省略していますが、Cognito User Poolを導入することでさらにユーザ認証を導入することも可能です）

![API認可WAF.png](https://qiita-image-store.s3.amazonaws.com/0/55077/359b88e1-3cee-73a1-4b6d-e3235f802e4e.png)

また、CloudFrontは署名付きURL(ワンタイムURL)を利用する以外に外部から直接アクセスを防ぐ方法がないため、
認可API経由でワンタイムURLを払い出しています。
さらにCloudFront経由でAPI Gateway呼び出しをするため、
かなり複雑な構成、設定になります。


# ワンタイム払い出し
Cognito+IAMで/onetime APIにAPI認可をかけます。
CognitoでのAPI認可に関しては下記を参考にしてください。
[世界に先駆けてAWSサーバレスアーキテクチャでユーザ認証とAPI認可の実装をしてみた](http://qiita.com/teradonburi/items/ef535d19c28a009552ec)

IAMロールに次のように/onetime APIの認可をかけます。

```
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "execute-api:Invoke",
                "cognito-sync:*",
                "cognito-identity:*"
            ],
            "Resource": [
                "arn:aws:execute-api:us-east-1:(アカウント情報)/*/POST/onetime"
            ]
        }
    ]
}
```



# API Gatewayの設定

/onetime APIにはIAM認証をかけます。
![apigateway1.png](https://qiita-image-store.s3.amazonaws.com/0/55077/4cf9ee30-a2aa-198e-fe4b-1d9e468896e9.png)


それ以外のAPIに関してはAPIキーでのアクセス制限をかけます。
![APIGateway2.png](https://qiita-image-store.s3.amazonaws.com/0/55077/20fb5f83-111a-d146-7e40-a7aa803d4f3a.png)

CloudFront経由からの呼び出しだと署名情報が壊れてしまうのでIAMでの認証ができませんでした
（回避策ご存知の方いれば教えてください）

## APIキーの払い出し
APIキー項目よりAPIキーを作成します。
作成後、APIのステージに紐付けをします。
APIキーはこの後、CloudFront側で設定します。
<font color="red">APIキーはAPI呼び出しに必要な認証情報なので外部に知られないようにしてください</font>

![APIGateway3.png](https://qiita-image-store.s3.amazonaws.com/0/55077/a40f9631-af6b-0410-86e4-c90b7d65660b.png)

CloudFront側から呼び出すステージのアドレスを控えておきます。
![stage.png](https://qiita-image-store.s3.amazonaws.com/0/55077/ee45e47b-7a25-c9a1-b3f8-8e79b60a55db.png)


# CloudFrontの設定
ここが一番苦労しました。（設定項目が多すぎる・・・）

<font color="red">Web</font>のDistributionを作成します。
![CloudFront1.png](https://qiita-image-store.s3.amazonaws.com/0/55077/b11f315d-c449-9682-c4b3-622fdca04c06.png)

Origin Domain Nameには先ほど控えたAPI Gatewayのアドレスを指定します。（https除外、ステージ名の前まで）
Origin Pathにはステージのパスを設定します。
Origin Protocol PolicyはCloudFront→API Gatewayはhttps経由なのでhttps Onlyにします。
Origin Custom Headersはx-api-keyヘッダーを付与し、先ほど取得したAPIキーを設定します。
![CloudFront2.png](https://qiita-image-store.s3.amazonaws.com/0/55077/17beefed-f7b2-59b9-4dcc-4336028cb63b.png)

Viewer Protocol PolicyはCloudFrontへのアクセス時のプロトコルですHttps Onlyにします。
Allower HTTP Methodsは通信を許可するリクエストメソッドです。
Forward HeaderをWhitelistにし、Originを追加します。これはCloudFront経由のアクセスでもCORSを許可するためです。
（ブラウザからのリクエスト時はCloudFront経由でAPI GatewayのOPTIONSメソッドにアクセスします。）
Object CachingはCustomizeにし、
Minimum TTL、Maximum TTL、Default TTLを1にし、キャッシュを無効にします。
（キャッシュがあるとセッションなどの挙動がおかしくなるため）
Forward CookiesはAllにし、クッキー情報をロストしないようにします。
Forward Query StringsもYesにし、リクエストパラメータをロストしないようにします。
Restrict Viewer AccessをYesにし、署名付きのURL以外CloudFrontに直接アクセスできないようにします。
（署名URLは/onetime APIにて払い出しします。）
![CloudFront3.png](https://qiita-image-store.s3.amazonaws.com/0/55077/b15a68b9-e1b2-0b07-52f0-8cf26c12b1a8.png)

AWS WAF Domain Namesは後でWAF側から設定します。まずはNoneでCreate Distributionします。
![CloudFront4.png](https://qiita-image-store.s3.amazonaws.com/0/55077/85092ad4-ad69-363a-e2e5-66aba9546097.png)

# WAFの設定
フィルターのかけ方は色々あるので下記参考にしてください
[「AWS WAF」を導入してみた。- IP addresses編](http://qiita.com/kooohei/items/5250f8c35d0b310dd467)
[「AWS WAF」を導入してみた。- SQL injection編](http://qiita.com/kooohei/items/8739c0ee023e0bb8b1e2)
[「AWS WAF」を導入してみた。- String matching編](http://qiita.com/kooohei/items/18b908a38a98528550e5)

## 署名付きURLの払い出しAPI実装
参考にさせてもらいました
[CloudFront+S3で署名付きURLでプライベートコンテンツを配信する](http://dev.classmethod.jp/cloud/aws/cf-s3-deliveries-use-signurl/)

<font color="red">ルートアカウント</font>の管理画面右上から「Security Credential」を選択します。
![accesskey1.jpg](https://qiita-image-store.s3.amazonaws.com/0/55077/7dfd3a6d-869a-5b42-de9c-f49abcc627ca.jpeg)
[Your Security Credentials]画面が表示されますので、[CloudFront Key Pairs]を選択します。
![accesskey2.jpg](https://qiita-image-store.s3.amazonaws.com/0/55077/d399f059-9811-62d1-5421-83aae578cea2.jpeg)
プライベートキーをダウンロードします。
![accesskey4.jpg](https://qiita-image-store.s3.amazonaws.com/0/55077/a6a7d385-d2a9-1814-1376-6d7b3194e9d7.jpeg)
アクセスキーを控えます。
![accesskey5.jpg](https://qiita-image-store.s3.amazonaws.com/0/55077/be96d00b-b2cc-24c5-456e-847377b51e59.jpeg)


/onetime APIのLambdaは次のようにCloudFrontの署名付きURLを払い出しします。
[aws-cloudfront-sign](https://github.com/jasonsims/aws-cloudfront-sign)モジュールを使っています。

```
'use strict';

var cf = require('aws-cloudfront-sign');
var options = {
    keypairId: （アクセスキー）,
    privateKeyString: （プライベートキー）
};


module.exports.handler = function (event, context, cb) {
    
    
    if(event.param === void 0){
        // パラメータなしはクーロンで呼ばれた場合（常にスタンバイ状態にする）
        return cb(null, {
            "cron": "stanby call"
        });
    }

    var url = event.param.url;
    var signedUrl = cf.getSignedUrl(url, options);
    console.log('Signed URL: ' + signedUrl);

    return cb(null, {
        url: signedUrl
    });
};
```

# クライアント側サンプルコード(Javascript、iOS、Android)
下記にアップロードしました。
https://github.com/teradonburi/CognitoCloudFrontWafAPIGateway

/onetime APIを経由しているだけで[前回](http://qiita.com/teradonburi/items/ef535d19c28a009552ec)とあまりかわりません。
違う点だけピックアップしていきます。

## Javascript
お使いの環境のIDを設定してください

```login.html
 ///////////////// Config /////////////////////////
 var identityPoolId = 'us-east-1:<indentity-pool-id>';
 var userPoolId = 'us-east-1_<user-pool-id>';
 var clientId = '<user-pool-crient-id>';
 var apiGateway = 'https://<stage-id>.execute-api.us-east-1.amazonaws.com/<stage>';
 var cloudFront = 'https://<cloudfront-id>.cloudfront.net';
 var api = "<api-path>";
 //////////////////////////////////////////////////
```

まず/onetime APIでCloudfrontのワンタイムAPIを払い出します。
払い出したワンタイムAPIをパラメータをつけてリクエストします。
API Gateway SDKのapigClient.jsは呼び出しURLを変更できるように改良してあります。

```login.html
 function callAPIGateway(api, token) {
            var apigClient = apigClientFactory.newClient({
                endpoint: apiGateway,
                accessKey: token.AccessKeyId,
                secretKey: token.SecretKey,
                sessionToken: token.SessionToken,
                region: 'us-east-1' // OPTIONAL: The region where the API is deployed, by default this parameter is set to us-east-1
            });

            var param = {};

            var body = {
                param: {
                    url: cloudFront + api
                }
            };

            apigClient.doPost('/onetime', param, body)
                .then(function (result) {
                    console.log("result:", result);



                    var cloudFrontClient = apigClientFactory.newClient({
                        endpoint: cloudFront,
                        region: 'us-east-1' // OPTIONAL: The region where the API is deployed, by default this parameter is set to us-east-1
                    });

                    var param = {};

                    // JSON送信データ
                    var body = {

                    };

                    return cloudFrontClient.doPost(api + '?' + result.data.url.split('?')[1], param, body);

                })
                .then(function (result) {
                    console.log("result:", result);
                })
                .catch(function (err) {
                    console.log("err:", err);
                });
        }
```

## iOS

お使いの環境のIDを設定してください

```
/////////////////// Config ////////////////////////////////

// Cognito User Pool
let ClientId = "<user-pool-client-id>" // Cognito User Pool Client Id
let ClientSecret = "<user-pool-secret>" // Cognito User Pool Client Secret
let PoolId = "us-east-1_<user-pool-id>" // Cognito User Pool Id
// Congnito Identity Pool
let IdentityId = "us-east-1:<identity-pool-id>" // Cognito Identity Pool Id
// API Gateway
let EndPoint = "https://<apigateway-id>.execute-api.us-east-1.amazonaws.com/<stage>"
let CloudFront = "https://<cloudfront-id>.cloudfront.net"
let api = "<api path>"
///////////////////////////////////////////////////////////

```

認証付きで
API Gatewayでの/onetime API呼び出し
およびCloudFront用にクライアントをそれぞれ作成します。

```ViewController.swift

// Cognito identityPoolIdによるAPI認可
let configuration = AWSServiceConfiguration(region: .USEast1, credentialsProvider: credentialsProvider)
ServerlessClient.registerClientWithConfiguration(configuration,forKey: "Auth",endpoint:self.EndPoint)
ServerlessClient.registerClientWithConfiguration(configuration,forKey: "CloudFront",endpoint:self.CloudFront)
                        
self.callAPI(ServerlessClient(forKey: "CloudFront"),client: ServerlessClient(forKey: "Auth"))

```

/onetime APIを呼び出した後、
CloudfrontのURLにエンドポイントを設定したクライアントを呼び出します。

```ViewController.swift

func callAPI(onetime:ServerlessClient,client:ServerlessClient){
        
        let path = CloudFront + self.api
        client.onetimeUrl(path).continueWithBlock { (task: AWSTask!) -> AnyObject! in
            
            var url:String
            ///////////////////////API認可されたAPIの呼び出し///////////////////////////
            if (task.error != nil) {
                print(task.error)
                return nil
            } else {
                print(task.result)
                url = (task.result as! NSDictionary)["url"] as! String
            }
            
            // JSON送信データ
            let param = [
                "": ""
            ]
            
            let queryParam = ServerlessClient.dictionaryFromQueryString(url)
            
            
            onetime.Post(self.api,param: param,queryParam:queryParam).continueWithBlock { (task: AWSTask!) -> AnyObject! in
                
                if (task.error != nil) {
                    print(task.error)
                    
                } else {
                    print(task.result)
                    
                }
                return nil
            }
            
            return nil
        }
                
}

```

## Android

認証付きで
API Gatewayでの/onetime API呼び出し
およびCloudFront用にクライアントをそれぞれ作成します。
<font color="red">Cloudfront経由での呼び出しはAPI Gateway SDKが対応していないため、okhttpを利用して直接リクエストしてます。</font>

```MainActivity.java
 //////////////////////
 // Config Param
 //////////////////////
    private static final String userPoolId = "us-east-1_<userpool-id>";
    private static final String clientId = "<userpool-client-id>";
    private static final String clientSecret = "<userpool-client-secret>";
    private static final String identityPoolId = "us-east-1:<identity-pool-id>";
    private static final String EndPoint = "https://<apigateway-id>.execute-api.us-east-1.amazonaws.com/<stage>";
    private static final String CloudFront = "https://<cloudfront-id>.cloudfront.net";
    private static final String api = "<api path>";

```

build.gradleにて
AWS SDKおよびokhttpをインストールします。

```build.gradle
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.3.0'
    compile 'com.amazonaws:aws-android-sdk-core:2.2.17+'
    compile 'com.amazonaws:aws-android-sdk-cognito:2.2.17+'
    compile 'com.amazonaws:aws-android-sdk-cognitoidentityprovider:2.2.17+'
    compile 'com.amazonaws:aws-android-sdk-apigateway-core:2.2.17+'
    compile 'com.squareup.okhttp3:okhttp:3.3.1'
}
```

リクエストのパラメータを入れ子にするためには
インナークラスにしてgsonにてjson化します。

```MainActivity.java

 ////////////////////////////////////////////

 class RequestData {

    public class Param {
            public class Data{
                String name;
                String type;
            }

            Data data = new Data();
    }

    Param param = new Param();
}

class ResponseData {
        class Data{
            String app_name;
            String app_type;
        }

        Data data;
        Integer status_cd;
}


private void callAPI(final CognitoCredentialsProvider credentialsProvider){

        new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... params) {
                ApiClientFactory factory = new ApiClientFactory();

                if(credentialsProvider != null){
                    factory.credentialsProvider(credentialsProvider);
                }

                // Build
                final OneTimeClient client = factory.build(OneTimeClient.class);


                OneTimeRequestData onetimeRequest = new OneTimeRequestData();
                onetimeRequest.param.url = CloudFront + api;
                OneTimeResponseData onetimeUrl = client.Post(onetimeRequest);


                Gson gson = new Gson();
                RequestData apiRequest = new RequestData();
                apiRequest.param.data.name = "user";
                apiRequest.param.data.type = "primium";


                String url = onetimeUrl.url;
                String requestJSON = gson.toJson(apiRequest);
                try {
                    Request request =  new Request.Builder()
                            .url(url)
                            .post(RequestBody.create(MediaType.parse("application/json; charset=utf-8"), requestJSON))
                            .build();

                    Response response = new OkHttpClient().newCall(request).execute();
                    if (response.isSuccessful()){
                        ResponseData res = gson.fromJson(response.body().string(),ResponseData.class);
                        Log.i("200", res.toString());
                    }
                }catch(IOException e){
                    Log.e("500", "error:" + e.getMessage(), e);
                }

                return null;
            }


        }.execute();

}

```