世界に先駆けてAWSサーバレスアーキテクチャでユーザ認証とAPI認可の実装をしてみた

下記GitHubにサンプルをアップロードしました。
https://github.com/teradonburi/CognitoUserAuthApiApproval

# サーバレスアーキテクチャとは
サーバを運用する場合、大まかにわけて次の３つの選択肢が存在すると思います。
1. 自社でサーバを管理するケース（オンプレミス型）
2. クラウドサービスでサーバを管理してもらうケース（IaaSクラウド型）
3. クラウド上でビジネスロジックのみ記述して、必要な時だけサーバを稼働させ、スケールもクラウドサービスに任せるケース（サーバレス型）

クラウドに依存するほど自前でカスタマイズできる自由度が減る代わりにサーバ運用コストが下がります。

ざっくり言ってしまうとオンプレのサーバは運用が大変なのとコストが高くて自社で持ちたくないのでクラウドのサーバを使いたい。クラウド型だと常時稼働していると運用コストもかかってしまう。そこで、ユーザが使うときにだけ稼働してサーバのスケールアウトも全てクラウドサービスに任せてしまいたいというのがサーバレスアーキテクチャです。
スタートアップのサービスなどでユーザがどれくらいつくかわからないけど
最初はお金をかけたくない、後でスケールアウトも簡単にできるようにしたいという場合に
有効なアーキテクチャです。


サーバレスアーキテクチャに関しては下記が詳しいです。
[サーバーレスアーキテクチャという技術分野についての簡単な調査](http://qiita.com/zerobase/items/3bc0d15980b472af841d)


# サーバレスアーキテクチャ構成図
AWS上でのサーバレスアーキテクチャ構成図を描いてみました。
あくまで一例です。
![ServerlessArchitecture.png](https://qiita-image-store.s3.amazonaws.com/0/55077/7cbeb143-12a6-4cb4-1ff7-a7933fc87510.png)

AWSではAPI GatewayとLambdaというサービスがサーバレスアーキテクチャのコアとなるサービスです。
LambdaはビジネスロジックをNodeJSやPythonなどで記述することができ、AWS内の別サービスからトリガーで実行できます。（バックグラウンドではトリガーでの起動時にAmazon Linuxが立ち上がり、ビジネスロジックを実行してくれます。）
API GatewayはAPIのエンドポイント（URL）を提供します。エンドポイントアクセス時にトリガーでLambdaを起動させることができるため、組み合わせることでRest APIをサーバなしで実装することが可能になります。また、Swaggerなどで記述したAPI定義もインポートすることが可能です。

Lambdaは実行回数で課金されるため、稼働していなかった場合、EC2の運用コストより大幅に安いことがメリットです。[Lambda料金参考](https://codezine.jp/article/detail/8446?p=3)
デメリットはLambdaではC/C++などのネイティブで実装されたミドルウェア（OpenCV、ffmpegなど）のインストールができない点です。
サービスによってはEC2にミドルウェアをインストールしてAPI Gateway経由で呼び出すという形になるのでしょう。

~~ちなみに、個人でt2.microのEC2サーバを運用していたのですが、月間のページプレビュー数が５リクエストにかかわらず月額6000円以上かかってました。（泣）~~
上記はRedHatの場合なので実際EC2の最小構成のみでやる場合は
EC2(t2.micro+Amazon Linux)+Route53で月額2000円程度でしょうか（スケールしませんが・・・）

# AWSでのユーザ認証・API認可
2016年の4月中旬にユーザ認証サービスであるCognito User Pool（Beta版）が解放されました。（まだ、us-east-1リージョンのみ）
これにより、ユーザ認証とAPI認可含めてサーバレスアーキテクチャで実現できるようになりました。
今回、世界に先駆けてサーバレスでのユーザ認証とAPI認可を実装してみました。

Cognito User Poolにてユーザ認証を行い、
Cognito Federated Identitlesにてロール認可を行います。
IAMのロール側ではAmazon Security Token Service(STS)という機能を用いて一時認証情報を払い出しします。
一時認証情報を用いてAPI Gatewayの認可APIにアクセスします。

![Auth.png](https://qiita-image-store.s3.amazonaws.com/0/55077/dc66d8e5-a5e9-6f37-d48d-665284ccc002.png)

ユーザ認証からAPI認可のフローは次の図のようになります。

![authFlow.png](https://qiita-image-store.s3.amazonaws.com/0/55077/b063c0df-f5b9-e41e-0306-478a07876375.png)

今回、Login ProviderのところにCognito User Poolを使うことができるようになりました。
この図ではAmazon CognitoはCognito Federated Identitiesを指します。
つまり、AWSのCognito User Poolに登録したユーザ認証とFacebook、Twitterなどの外部プロバイダーのユーザ認証をCognito Federated Identitiesで一元化し、STSでIAMロールでの認可を行うことが可能になりました。

## AWS側の設定
以下AWSサービスの設定をそれぞれ行います。

* Cognito User Pool
* Cognito Federated Identites
* IAM
* Lambda
* API Gateway

以下、us-east-1リージョンにて設定を行います。

### Cognito User Poolの作成
作成方法は下記を参考にさせてもらいました。
[[新機能] Amazon Cognito に待望のユーザー認証基盤「User Pools」が追加されました！](http://dev.classmethod.jp/cloud/aws/cognito-user-pool/)

作成時に細かい部分で気をつけたほうが良いことを追記しておきます。
<Font color="red">Attributes項目の必須チェックはUser Pool作成後に変更できません。</Font>
（この項目はクライアントからユーザ登録する際に必須パラメータとなってしまいます。
ここには記載されていませんが、これと別途に<Font color="red">ユーザ名とパスワードはユーザ登録時に必須</Font>です。
Custom Attributesに関しては後から変更が可能なため、不要な場合は全てチェックを外しておくことをお勧めしておきます。）
![userpool1.png](https://qiita-image-store.s3.amazonaws.com/0/55077/49b0c953-412e-5f06-2b2e-b8e6c5a4b36b.png)

Verificationの項目はEmail、Phone Numberにチェックを入れておきます。
ログインするためにはVerificationによるユーザのConfirmedが必須です。
（ユーザ登録時にメールアドレス、もしくはSMSに送信されたアクティベートコードを入力する必要があります。）

![userpool2.png](https://qiita-image-store.s3.amazonaws.com/0/55077/eda92595-81ec-1a36-36dc-f1ee2a6034b5.png)

Apps項目で
Webブラウザ用とモバイル用（iOS、Android）にAppを二つ作ります。
Webブラウザ用はGenerate client secretのチェックを外して作成します。（ブラウザからのアクセス時には不要なため）

![userpool3.png](https://qiita-image-store.s3.amazonaws.com/0/55077/1cc20213-4064-cffb-3359-3ea3eebabff7.png)


### Cognito Identity Poolの作成
Cognito Identity Poolの作成方法とCognito User Poolの紐付けは下記を参考にしてください。
[Amazon Cognito User Poolsを使って、webサイトにユーザ認証基盤を作る](http://qiita.com/horike37/items/1d522f66452d3abe1203)

注意点としては、
Identity Poolには１つしかCognito User Poolの割り当てができないため、
ブラウザとモバイルで同一のCognito User Poolを割り当てたい場合は
Identity Pool(とIAMロール)をブラウザ用とモバイル用に２つずつ作成する必要があります。

### Lambdaの設定
特にLambda側では特別な設定は必要ありません。
今回のサンプルでは単純に成功を返すLambda関数を作成してテストしてます。
<Font color="red">ただし、レスポンスには必ずJavascriptのObject型を返すようにしてください。
理由はAndroid側のAPI Gateway SDKがそれ以外の型に対応していないためです。（後述）</Font>

```
'use strict';
console.log('Loading function');

exports.handler = (event, context, callback) => {
    callback(null, {message:"成功"});  
    // callback('Something went wrong');
};
```

### API Gatewayの設定

APIのMethod RequestのAuthorizationにAWS_IAMを設定します。
![gateway1.png](https://qiita-image-store.s3.amazonaws.com/0/55077/df9fe8c9-638e-4e4c-19b4-bbdbe18ffa5a.png)

認可したいAPIのarnを控えます。(後述のIAMロールで設定します。)
![gateway2.png](https://qiita-image-store.s3.amazonaws.com/0/55077/a5e8b2e3-f7ad-1bd0-0714-e5c83b9e7f95.png)

#### CORSの設定(ブラウザ用APIのみ)
上記に加え、Ajax経由でAPIにアクセスするためには、APIにCORSの設定が別途必要です。
下記を参考にしてください。
[jQueryからAmazon API Gatewayを使用する](http://qiita.com/naomichi-y/items/999328ad910e5ab9589d)

作成時にAccess-Control-Allow-Headersに下記ヘッダーを設定します。
'Content-Type,X-Amz-Date,Authorization,X-Requested-With,X-Requested-By,X-Amz-Security-Token'

X-Requested-With,X-Requested-ByはXHR(Ajax) で必要なヘッダーパラメータです。
X-Amz-Security-Tokenは今回のようなSTSでのAPI認可には必須ヘッダーパラメータです。

![gateway3.png](https://qiita-image-store.s3.amazonaws.com/0/55077/dae1bae1-7740-7e9c-32d3-17c84cec8043.png)

 
### IAMのロールの設定
Cognito Identity Poolで指定したAuthenticatedロールの権限を設定します。
UnAuthenticatedのほうは特に権限を付与する必要はありません。
IAMのロール項目でAuthenticatedロールを開きます。
ポリシーの編集より、ポリシーを編集します。
Actionに"execute-api:invoke"を付与します。
Resourceに認可したいAPI GatewayのAPIのARNを指定します。

```
{
　　"Version": "2012-10-17",
　　"Statement": [
　　{
　　"Effect": "Allow",
　　"Action": [
　　"execute-api:Invoke",
　　"cognito-sync:*",
　　"cognito-identity:*"
　　],
　　"Resource": [
　　　　　"arn:aws:execute-api:us-east-1:<AccountID>:<API Gateway API ID>/*/<Method Type>/<API名>"
　　]
　}
]
}
```

信頼関係の編集より
1.Actionがsts:AssumeRoleWithWebIdentity
2.cognito-identity.amazonaws.com:audが対象のIdentity Pool Id
3.cognito-identity.amazonaws.com:amrにauthenticated（UnAuthenticatedのロールの方はunauthenticatedになっています）
になっていることを確認してください

```
{
  "Version": "2012-10-17",
  "Statement": [
  {
   "Effect": "Allow",
   "Principal": {
      "Federated": "cognito-identity.amazonaws.com"
  },
  "Action": "sts:AssumeRoleWithWebIdentity",
  "Condition": {
     "StringEquals": {
      "cognito-identity.amazonaws.com:aud": ""us-east-1:<Identity Pool Id>"
     },
     "ForAnyValue:StringLike": {
      "cognito-identity.amazonaws.com:amr": "authenticated"
     }
  }
 }
 ]
}
```

## 実装

### Javascript（ブラウザ）での実装
ローカルから起動する場合はAWS-CLIの設定が必要です。
[AWS CLI インストール&セットアップ メモ（Mac）](http://qiita.com/mikoto/items/3b78f7c03e991c9fc46e)

#### 導入

* Cognito SDKの導入

[amazon-cognito-identity-js](https://github.com/aws/amazon-cognito-identity-js)

Crypto.jsに関しては--with-codecBytes オプションをつけて自前でビルドする必要があります。

* API Gateway SDKの導入

API GatewayのステージからSDKをダウンロードします。
![apigateway.png](https://qiita-image-store.s3.amazonaws.com/0/55077/65421eb1-78d7-7ffa-e82e-b1befbea4bec.png)

* 初期設定

下記パラメータを設定してください（AWSのID）

```login.html
        /////////////////////////
        // Config Param
        /////////////////////////
        
        var UserPoolId = 'us-east-1_<Pool Id>'; // Cognito User Pool Id
        var ClientId = '<Client Id>'; // Client Id(Cognito User Pool App Client Id)
        var IdentityPoolId = 'us-east-1:<Identity Pool Id>'; // Cognito Identity Pool Id
        var endpoint = 'https://<API>.execute-api.us-east-1.amazonaws.com/<Stage>'; // API Gateway EndPoint
        var testAPI = '/<API>'; // API Gateway API
        
        /////////////////////////
```

#### ユーザの登録・ユーザのアクティベート・ユーザのログイン
下記記事と変わりませんので割愛します。
[Amazon Cognito User Poolsを使って、webサイトにユーザ認証基盤を作る](http://qiita.com/horike37/items/1d522f66452d3abe1203)

#### 認可
AccessTokenではなく、IdTokenで認可を行います。
getId関数で認証プロバイダー（Cognito User Pool）に問い合わせを行い、
GetCredentialsForIdentity関数で認可を行います。（Authenticateのロールに切り替わります。）

```login.html
        var cognitoUser = new AWSCognito.CognitoIdentityServiceProvider.CognitoUser(userData);
            cognitoUser.authenticateUser(authenticationDetails, {
                onSuccess: function (result) {
                    var IdToken = result.getIdToken().getJwtToken();
                    console.log('IdToken:' + IdToken);
                    
                    
                    var provider = {};
                    provider['cognito-idp.us-east-1.amazonaws.com/' + UserPoolId] = IdToken;

                    var params = {
                        IdentityPoolId:IdentityPoolId,
                        Logins: provider
                    };
                    var cognitoidentity = new AWS.CognitoIdentity({apiVersion: '2016-04-18'});
                    cognitoidentity.getId(params, function(err, data) {
                        if (err){
                            console.log(err, err.stack);
                        }  
                        else{
                                                        
                            var params = {
                                IdentityId: data.IdentityId,
                                Logins:provider
                            };
                            
                            cognitoidentity.getCredentialsForIdentity(params, function(err, data) {
                                if (err){
                                    console.log(err, err.stack);
                                }  
                                else{
                                    console.log("data.Credentials:",data.Credentials);
                                    // Access AWS resources here.
                                    callAPIGateway(data.Credentials);
                                }
                            });

                            
                        }    
                    }); 

                    
                    
                },

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

            }); 
```

#### 認可APIのコール

STSより取得した一時認証情報

* AccessKeyId
* SecretKey
* SessionToken

を用いてAPI GatewayのAPIをコールします。

```
       function callAPIGateway(token){
            var apigClient = apigClientFactory.newClient(endpoint,{
                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 = {                
            };

            apigClient.doPost(testAPI,param, body)
                .then(function(result){
                console.log("result:",result);
            }).catch( function(result){
                console.log("err:",result);
            });
        }
```

API GatewayのSDKに関しては自動生成されるAPI（apigClient.js）が気に入らなかったので汎用化してます。

```apigClient.js
var apigClientFactory = {};
apigClientFactory.newClient = function (gatewayPath,config) {
    var apigClient = { };
    if(config === undefined) {
        config = {
            accessKey: '',
            secretKey: '',
            sessionToken: '',
            region: '',
            apiKey: undefined,
            defaultContentType: 'application/json',
            defaultAcceptType: 'application/json'
        };
    }
    if(config.accessKey === undefined) {
        config.accessKey = '';
    }
    if(config.secretKey === undefined) {
        config.secretKey = '';
    }
    if(config.apiKey === undefined) {
        config.apiKey = '';
    }
    if(config.sessionToken === undefined) {
        config.sessionToken = '';
    }
    if(config.region === undefined) {
        config.region = 'us-east-1';
    }
    //If defaultContentType is not defined then default to application/json
    if(config.defaultContentType === undefined) {
        config.defaultContentType = 'application/json';
    }
    //If defaultAcceptType is not defined then default to application/json
    if(config.defaultAcceptType === undefined) {
        config.defaultAcceptType = 'application/json';
    }

    
    // extract endpoint and path from url
    var invokeUrl = gatewayPath;
    var endpoint = /(^https?:\/\/[^\/]+)/g.exec(invokeUrl)[1];
    var pathComponent = invokeUrl.substring(endpoint.length);

    var sigV4ClientConfig = {
        accessKey: config.accessKey,
        secretKey: config.secretKey,
        sessionToken: config.sessionToken,
        serviceName: 'execute-api',
        region: config.region,
        endpoint: endpoint,
        defaultContentType: config.defaultContentType,
        defaultAcceptType: config.defaultAcceptType
    };

    var authType = 'NONE';
    if (sigV4ClientConfig.accessKey !== undefined && sigV4ClientConfig.accessKey !== '' && sigV4ClientConfig.secretKey !== undefined && sigV4ClientConfig.secretKey !== '') {
        authType = 'AWS_IAM';
    }

    var simpleHttpClientConfig = {
        endpoint: endpoint,
        defaultContentType: config.defaultContentType,
        defaultAcceptType: config.defaultAcceptType
    };

    var apiGatewayClient = apiGateway.core.apiGatewayClientFactory.newClient(simpleHttpClientConfig, sigV4ClientConfig);
    
    
    
    apigClient.doGet = function (path,params, body, additionalParams) {
        if(additionalParams === undefined) { additionalParams = {}; }
        
        apiGateway.core.utils.assertParametersDefined(params, [], ['body']);
        
        var GetRequest = {
            verb: 'get'.toUpperCase(),
            path: pathComponent + uritemplate(path).expand(apiGateway.core.utils.parseParametersToObject(params, [])),
            headers: apiGateway.core.utils.parseParametersToObject(params, []),
            queryParams: apiGateway.core.utils.parseParametersToObject(params, []),
            body: body
        };
        
        
        return apiGatewayClient.makeRequest(GetRequest, authType, additionalParams, config.apiKey);
    };
    
    apigClient.doPost = function (path,params, body, additionalParams) {
        if(additionalParams === undefined) { additionalParams = {}; }

        apiGateway.core.utils.assertParametersDefined(params, [], ['body']);

        var PostRequest = {
            verb: 'post'.toUpperCase(),
            path: pathComponent + uritemplate(path).expand(apiGateway.core.utils.parseParametersToObject(params, [])),
            headers: apiGateway.core.utils.parseParametersToObject(params, []),
            queryParams: apiGateway.core.utils.parseParametersToObject(params, []),
            body: body
        };


        return apiGatewayClient.makeRequest(PostRequest, authType, additionalParams, config.apiKey);
    };
        

    return apigClient;
};

```

### iOSでの実装

#### 導入

* AWS SDKの導入

Cocoa Podにて依存ライブラリをインストールします。

```Podfile
platform :ios, '8.0'

pod 'UICKeyChainStore','2.1.0'
pod 'AWSCore','2.4.2'
pod 'AWSCognitoIdentityProvider','2.4.2'
pod 'AWSCognito','2.4.2'
pod 'AWSAPIGateway','2.4.2'
```

* API Gateway SDKの導入

API GatewayのステージからSDKをダウンロードします。
![apigateway.png](https://qiita-image-store.s3.amazonaws.com/0/55077/65421eb1-78d7-7ffa-e82e-b1befbea4bec.png)

* 初期設定

下記パラメータを設定してください（AWSのID）

```ViewController.swift
    //////////////////////
    // Config Param
    //////////////////////
    
    var UserPoolId = "us-east-1_<Pool Id>" // Cognito User Pool Id
    var ClientId = "<Client Id>" // Cognito User Pool Client Id
    var ClientSecret = "<Client Secret>" // Cognito User Pool Client Secret
    var IdentitityPoolId = "us-east-1:<Identity Pool Id>" // Cognito Identity Pool Id
    let EndPoint = "https://<API>.execute-api.us-east-1.amazonaws.com/<Stage>" // API Gateway Endpoint
    let API = "/<API>" // API Gateway API
    
    //////////////////////
```

#### ユーザの登録・ユーザのアクティベート・ユーザのログイン
下記記事と変わらないので割愛します。
[Amazon Cognito User Poolsを使って、webサイトにユーザ認証基盤を作る](http://qiita.com/horike37/items/1d522f66452d3abe1203)

~~一箇所だけ補足で現在Cognito User Pool SDKのバグで英語以外の言語（ロケール）でログインできないバグがあります。~~
~~https://forums.aws.amazon.com/thread.jspa?messageID=719881~~
AWS iOS SDK 2.4.2で修正されたようです。

#### 認可

Cognito User Poolログイン後、IdTokenを取得し、
AWSCognitoCredentialsProviderクラスでCognito User Providerを認証プロバイダーとして設定します。
credentialメソッドで一時認証情報を取得します。
キャッシュとして認証情報は保持されるようなので（ポリシーの変更が即時反映されない）
キャッシュクリアする場合はclearKeychainメソッドを呼びます。

```ViewController.swift

        let user: AWSCognitoIdentityUser = pool!.getUser(self.loginName.text!)
        
        
        user.getSession(self.loginName.text!, password: self.loginPassword.text!, validationData: nil, scopes: nil).continueWithBlock({task in
            if((task.error) != nil) {
                print(task.error)
            } else {
                print(task.result)
                
                let cognitoIdentityPoolId = self.IdentitityPoolId
                let ret = task.result as! AWSCognitoIdentityUserSession
                let IdToken : String =  ret.idToken!.tokenString
                
                // 認証プロバイダーにUser Poolを使う
                let provider = "cognito-idp.us-east-1.amazonaws.com/" + self.UserPoolId
                
                let userpoolProvider = UserPoolProvider()
                userpoolProvider.setting(provider, token: IdToken)
                let credentialsProvider = AWSCognitoCredentialsProvider(
                    regionType: .USEast1,
                    identityPoolId: cognitoIdentityPoolId,
                    identityProviderManager: userpoolProvider
                )
                // credentialのキャッシュをクリア
                credentialsProvider.clearKeychain()
                
                
                // credential取得
                credentialsProvider.credentials().continueWithBlock { (task: AWSTask!) -> AnyObject! in
                    
                    if (task.error != nil) {
                        print(task.error)
                        
                    } else {
                        print(task.result)

                        // 認証情報を渡し、API GatewayのAPIをコール
                        self.callAPI(credentialsProvider)
 
                        
                    }
                    return nil
                }

                
                
            }
            
            return nil
        })
```

UserPoolProviderクラスはAWSIdentityProviderManagerを実装したクラスです。
参考：[モバイルアプリでユーザー認証やデータ同期が行えるAmazon CognitoがiOSで動かないのを何とか調べた話](http://animane.hatenablog.com/entry/2016/05/04/200142)

```UserPoolProvider.swift
import Foundation

public class UserPoolProvider:NSObject, AWSIdentityProviderManager{
    
    var providerName:String!
    var token:String!
    
    public func setting(providerName:String,token:String){
        self.providerName = providerName
        self.token = token
    }
    
    public func logins() -> AWSTask {
        var providers = [String:String]()
        providers[self.providerName] = self.token
        return AWSTask(result: providers as AnyObject)
    }
}
```

#### 認可APIのコール

認証に成功すると認証プロバイダークラス内で一時認証情報を保持するようになります。
API Gatewayのクラスに認証プロバイダーをセットし、
認可APIをコールします。

```ViewController.swift
     func callAPI(credentialsProvider:AWSCognitoCredentialsProvider){
        
        // API Gatewayのエンドポイント設定
        ServerlessClient.setEndPoint(EndPoint)
        
        // Cognito identityPoolIdによるAPI認可
        let configuration = AWSServiceConfiguration(region: .USEast1, credentialsProvider: credentialsProvider)
        ServerlessClient.registerClientWithConfiguration(configuration,forKey: "Auth")
    
        
        ///////////////////////API認可されたAPIの呼び出し///////////////////////////
        
        
        let client = ServerlessClient(forKey: "Auth")
        
        client.Post(API,param: ["":""]).continueWithBlock { (task: AWSTask!) -> AnyObject! in
            
            if (task.error != nil) {
                print(task.error)
                
            } else {
                print(task.result)
                
            }
            return nil
        }
        
    }

```

エクスポートされたAPI Gatewayのクラスのほうは汎用性に欠けるのでカスタマイズしています。
（Serverless.h、Serverless.m）

```Serverless.h

#import <Foundation/Foundation.h>
#import <AWSAPIGateway/AWSAPIGateway.h>
#import <AWSCore/AWSCore.h>

@interface ServerlessClient: AWSAPIGatewayClient

+ (void)setEndPoint:(NSString*)endpoint;


+ (instancetype)defaultClient;
+ (void)registerClientWithConfiguration:(AWSServiceConfiguration *)configuration forKey:(NSString *)key;
+ (instancetype)clientForKey:(NSString *)key;
+ (void)removeClientForKey:(NSString *)key;

- (AWSTask *)Get:(NSString *)path
            param:(NSDictionary *)param;

- (AWSTask *)Post:(NSString *)path
            param:(NSDictionary *)param;


@end
```

```Serverless.m
#import "ServerlessClient.h"
#import <AWSCore/AWSSignature.h>
#import <AWSCore/AWSSynchronizedMutableDictionary.h>
#import <AWSCognitoIdentityProvider/AWSCognitoIdentityProvider.h>


@interface SeverlessModel : AWSModel

@property (strong, nonatomic) NSDictionary* param;

@end

@implementation SeverlessModel

+ (NSDictionary *)JSONKeyPathsByPropertyKey {
    return @{};
}

@end


@interface AWSAPIGatewayClient()

// Networking
@property (nonatomic, strong) NSURLSession *session;

// For requests
@property (nonatomic, strong) NSURL *baseURL;

// For responses
@property (nonatomic, strong) NSDictionary *HTTPHeaderFields;
@property (nonatomic, assign) NSInteger HTTPStatusCode;

- (AWSTask *)invokeHTTPRequest:(NSString *)HTTPMethod
                     URLString:(NSString *)URLString
                pathParameters:(NSDictionary *)pathParameters
               queryParameters:(NSDictionary *)queryParameters
              headerParameters:(NSDictionary *)headerParameters
                          body:(id)body
                 responseClass:(Class)responseClass;

@end

@interface ServerlessClient()

@property (nonatomic, strong) AWSServiceConfiguration *configuration;

@end

@interface AWSServiceConfiguration()

@property (nonatomic, strong) AWSEndpoint *endpoint;

@end

@implementation ServerlessClient

@synthesize configuration = _configuration;

static AWSSynchronizedMutableDictionary *_serviceClients = nil;
static NSString *url = nil;


+ (void)setEndPoint:(NSString*)endpoint;
{
    url = endpoint;
}

+ (instancetype)defaultClient {
    AWSServiceConfiguration *serviceConfiguration = nil;
    if ([AWSServiceManager defaultServiceManager].defaultServiceConfiguration) {
        serviceConfiguration = AWSServiceManager.defaultServiceManager.defaultServiceConfiguration;
    } else {
        serviceConfiguration = [[AWSServiceConfiguration alloc] initWithRegion:AWSRegionUnknown
                                                           credentialsProvider:nil];
    }

    static ServerlessClient *_defaultClient = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _defaultClient = [[ServerlessClient alloc] initWithConfiguration:serviceConfiguration];
    });

    return _defaultClient;
}

+ (void)registerClientWithConfiguration:(AWSServiceConfiguration *)configuration forKey:(NSString *)key {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _serviceClients = [AWSSynchronizedMutableDictionary new];
    });
    [_serviceClients setObject:[[ServerlessClient alloc] initWithConfiguration:configuration]
                        forKey:key];
}

+ (instancetype)clientForKey:(NSString *)key {
    return [_serviceClients objectForKey:key];
}

+ (void)removeClientForKey:(NSString *)key {
    [_serviceClients removeObjectForKey:key];
}

- (instancetype)init {
    @throw [NSException exceptionWithName:NSInternalInconsistencyException
                                   reason:@"`- init` is not a valid initializer. Use `+ defaultClient` or `+ clientForKey:` instead."
                                 userInfo:nil];
    return nil;
}

- (instancetype)initWithConfiguration:(AWSServiceConfiguration *)configuration {
    if (self = [super init]) {
        _configuration = [configuration copy];

        NSString *URLString = url;
        if ([URLString hasSuffix:@"/"]) {
            URLString = [URLString substringToIndex:[URLString length] - 1];
        }
        _configuration.endpoint = [[AWSEndpoint alloc] initWithRegion:_configuration.regionType
                                                              service:AWSServiceAPIGateway
                                                                  URL:[NSURL URLWithString:URLString]];

        AWSSignatureV4Signer *signer = [[AWSSignatureV4Signer alloc] initWithCredentialsProvider:_configuration.credentialsProvider endpoint:_configuration.endpoint];

        _configuration.baseURL = _configuration.endpoint.URL;
        _configuration.requestInterceptors = @[[AWSNetworkingRequestInterceptor new], signer];
    }
    
    return self;
}



- (AWSTask *)Get:(NSString *)path
            param:(NSDictionary *)param
{
    
    
    
    NSDictionary *headerParameters = @{
                                       @"Content-Type": @"application/json",
                                       @"Accept": @"application/json",
                                       };
    
    
    NSDictionary *queryParameters = @{
                                      
                                      
                                      };
    NSDictionary *pathParameters = @{
                                     
                                     };
    
    
    SeverlessModel* model = [[SeverlessModel alloc] init];
    model.param = param;
    
    return [self invokeHTTPRequest:@"GET"
                         URLString:path
                    pathParameters:pathParameters
                   queryParameters:queryParameters
                  headerParameters:headerParameters
                              body:model
                     responseClass:nil];
    
}



- (AWSTask *)Post:(NSString *)path
            param:(NSDictionary *)param
{
    
  
    
    NSDictionary *headerParameters = @{
                                       @"Content-Type": @"application/json",
                                       @"Accept": @"application/json",
                                       };
    
    
    NSDictionary *queryParameters = @{
                                      
                                      
                                    };
    NSDictionary *pathParameters = @{
                                     
                                     };
    
    
    SeverlessModel* model = [[SeverlessModel alloc] init];
    model.param = param;
    
    return [self invokeHTTPRequest:@"POST"
                         URLString:path
                    pathParameters:pathParameters
                   queryParameters:queryParameters
                  headerParameters:headerParameters
                              body:model
                     responseClass:nil];
    
}

@end
```

### Androidでの実装

下記参考にしてます。

* [Cognito User Pool Sample](https://github.com/awslabs/aws-sdk-android-samples/tree/master/AmazonCognitoYourUserPoolsDemo)
* [Amazon API Gateway の API のクライアント SDK を生成して Android アプリから呼び出す](http://dev.classmethod.jp/cloud/aws/api-gateway-with-android/)

#### 導入

* AWS SDKの導入

Android Studioのbuild.gradleのdependenciesにAWS SDKの各種ライブラリを追加します。

```build.gradle
dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    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+'
}
```

* API Gateway SDKの導入

下記、API GatewayのステージからSDKを<Font color="red">ダウンロードしません</Font>
![apigateway.png](https://qiita-image-store.s3.amazonaws.com/0/55077/65421eb1-78d7-7ffa-e82e-b1befbea4bec.png)

理由はカスタマイズが利かないからです。
ダウンロードしたSDKはMavenにてJarファイルを作成する形式になっているのですが
この方式だとAPI GatewayのAPIを追加、変更するたびにダウンロードして、
Mavenでコンパイルしてjarを作成するという作業が必要になります。

そのため、今回Android版はAPIコール部分はスクラッチで実装します。(後述)

* 初期設定

下記パラメータを設定してください（AWSのID）

```MainActivity.java
    //////////////////////
    // Config Param
    //////////////////////
    private static final String userPoolId = "us-east-1_<User Pool Id>";
    private static final String clientId = "<User Pool Client Id>";
    private static final String clientSecret = "<User Pool Client Secret>";
    private static final String identityPoolId = "us-east-1:<Identity Pool Id>";
    //////////////////////
```

#### ユーザの登録・ユーザのアクティベート・ユーザのログイン

~~どうもAndroid版のほうも言語設定をEnglishにしないとCognito User PoolにログインできないSDKのバグがあるようです。（Beta版だし仕方ないね・・・）~~
~~https://github.com/aws/aws-sdk-android/issues/132~~
AWS Android SDK 2.2.17で修正されたようです。

* Cognito User Pool初期化

コンストラクタにて初期化パラメータを渡してCognitoUserPoolクラスを作成します。

```MainActivity.java
CognitoUserPool userPool = new CognitoUserPool(this, userPoolId, clientId, clientSecret, new ClientConfiguration());
```

* ユーザ登録

CoginitoUserPoolクラスのsignUpInBackgroundメソッドでユーザ登録します。

```MainActivity.java
                // Read user data and register
                CognitoUserAttributes userAttributes = new CognitoUserAttributes();

                String name = nameEdit.getText().toString();
                String password = passwordEdit.getText().toString();
                String email = emailEdit.getText().toString();
                userAttributes.addAttribute("email", email);

                userPool.signUpInBackground(name, password, userAttributes, null, new SignUpHandler() {
                    @Override
                    public void onSuccess(CognitoUser user, boolean signUpConfirmationState,
                                          CognitoUserCodeDeliveryDetails cognitoUserCodeDeliveryDetails) {
                        // Check signUpConfirmationState to see if the user is already confirmed
                        Boolean regState = signUpConfirmationState;
                        if (signUpConfirmationState) {
                            // User is already confirmed
                            Toast.makeText(getApplicationContext(), "すでにConfirmedされています", Toast.LENGTH_LONG).show();
                        }
                        else {
                            // User is not confirmed
                            Toast.makeText(getApplicationContext(), "認証コードを入力してアクティベートしてください", Toast.LENGTH_LONG).show();
                        }
                    }

                    @Override
                    public void onFailure(Exception exception) {
                        Toast.makeText(getApplicationContext(), "ユーザ登録に失敗しました", Toast.LENGTH_LONG).show();
                    }
                });

```

* ユーザのアクティベート

confirmSignUpInBackgroundメソッドにてアクティベートします。
User Poolの登録ユーザがConfirmedされます。

```MainActivity.java
                String verifyName = verifyNameEdit.getText().toString();
                String activateCode = activateCodeEdit.getText().toString();

                userPool.getUser(verifyName).confirmSignUpInBackground(activateCode, true, new GenericHandler() {
                    @Override
                    public void onSuccess() {
                        Toast.makeText(getApplicationContext(), "アクティベートされました", Toast.LENGTH_LONG).show();
                    }

                    @Override
                    public void onFailure(Exception exception) {

                    }
                });
```

* ユーザのログイン

getSessionInBackgroundメソッドにてログインします。

```MainActivity.java
                String loginName = loginNameEdit.getText().toString();
                final String loginPassword = loginPasswordEdit.getText().toString();


                userPool.getUser(loginName).getSessionInBackground(new AuthenticationHandler() {
                    @Override
                    public void onSuccess(CognitoUserSession cognitoUserSession) {
                        Toast.makeText(getApplicationContext(), "ログイン成功", Toast.LENGTH_LONG).show();

　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　// API認可の処理
                        
                    }

                    @Override
                    public void getAuthenticationDetails(AuthenticationContinuation authenticationContinuation, String username) {

                        AuthenticationDetails authenticationDetails = new AuthenticationDetails(username, loginPassword, null);
                        authenticationContinuation.setAuthenticationDetails(authenticationDetails);
                        authenticationContinuation.continueTask();

                    }

                    @Override
                    public void getMFACode(MultiFactorAuthenticationContinuation multiFactorAuthenticationContinuation) {
                        // MFAの設定がない場合は不要
                    }

                    @Override
                    public void onFailure(Exception e) {
                        Toast.makeText(getApplicationContext(), "ログインに失敗しました", Toast.LENGTH_LONG).show();
                    }
                });

```

#### 認可

ユーザログイン後に
CognitoCredentialsProviderクラスに
User Pool認証プロバイダーとログインユーザのIdTokenを指定し、
setLoginsメソッドで認可します。

```MainActivity.java
                        CognitoCredentialsProvider credentialsProvider = new CognitoCredentialsProvider(
                                identityPoolId,Regions.US_EAST_1);
                        //credentialsProvider.clear();

                        Map<String, String> logins = new HashMap<String, String>();
                        String userPoolProvider = "cognito-idp.us-east-1.amazonaws.com/" + userPoolId;
                        logins.put(userPoolProvider, cognitoUserSession.getIdToken().getJWTToken());
                        credentialsProvider.setLogins(logins);
                        //credentialsProvider.getCredentials();

                        // 認可後、APIコール
                        callAPI(credentialsProvider);
```

#### 認可APIのコール

認可後の認証プロパイダーをAPIClientFactoryクラスに設定します。
APIClientFactoryクラスのbuildメソッドによりAPIコールクライアントを作成します。
内部的にはProxyクラスによるApiClientHandlerクラスのインジェクションと
APIコール時にはGsonライブラリによりJSONとモデルクラスのORマッピングを行っています。

```MainActivity.java
    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 TestClient client = factory.build(TestClient.class);


                Gson gson = new Gson();
                RequestData requestData = new RequestData("パラメータです");

                ResponseData data = client.testPost(gson.toJson(requestData));
                return null;
            }


        }.execute();


    }
```

APIクライアントクラス（TestClientクラス）は次のように定義します。
Serviceアノテーションのendpointの部分はAPI GatewayのステージデプロイURLに合わせて変更してください。
OperationアノテーションにAPIのパスとメソッド形式を指定します。

```TestClient.java
package com.example.teraidaiki.serverless;

import com.amazonaws.mobileconnectors.apigateway.annotation.Operation;
import com.amazonaws.mobileconnectors.apigateway.annotation.Service;

@Service(
        endpoint = "https://<API Gateway Stage ID>.execute-api.us-east-1.amazonaws.com/test"
)
public interface TestClient {
    @Operation(
            path = "/test",
            method = "POST"
    )
    ResponseData testPost(String param);

}

```

RequestDataクラスは送信用パラメータを定義したクラスです。
GsonライブラリでJSON文字列化します。

```RequestData.java
package com.example.teraidaiki.serverless;

public class RequestData {
    String someData;

    public RequestData(String someData){
        this.someData = someData;
    }
}
```

ResponseDataクラスはレスポンスで受け取るデータを定義したモデルクラスです。

```ResponseData.java
package com.example.teraidaiki.serverless;

public class ResponseData {
    String message;// レスポンスデータに合わせて変える

    public ResponseData() {
    }
}
```

Android側でレスポンスデータを取得するためには、ResponseDataクラスのメンバ変数を適宜変える必要があります。
今回、Lambda側からはレスポンスで次のようなデータを返却します。

```
{message:"成功"}
```


上記のような<Font color="red">JavascriptのObject型のレスポンスでないとAPIコール時にGsonライブラリ内でORマッピングに失敗し、Androidアプリが落ちます。</Font>