API GatewayのCustom AuthorizerでIP制限する

# やりたかったこと
Amazon API Gatewayの実行をソースIPアドレスで制御したい。  

社内などの特定かつセキュアな環境からしか実行できないAPIを作りたい場合、やっぱりIP制限が安全なので実装したくなります。（決して認証つくるのが面倒とかじゃないよ？たぶん。）いままでCloudFront+WAFで実現していたのですが、コンテンツ配信があるわけではないのでAPI Gatewayだけで完結したいなーと思ってたところ、「カスタムオーソーライザーでできるらしい」と聞いたので試してみました。

#### **2018/4/7 追記**
API Gatewayのアップデートで、設定に直接API Gatewayのリソースポリシーを書くことで実行を制御できるようになりました。この記事の「カスタムオーソーライザーでポリシーを評価、IP制限」はもう古いのでご注意ください！
[Amazon API Gatewayが、APIのリソースポリシーをサポート](
https://aws.amazon.com/jp/about-aws/whats-new/2018/04/amazon-api-gateway-supports-resource-policies/)

参考：[リソースポリシーによるAPI GatewayのIPアドレス制限を試す](https://qiita.com/hayao_k/items/4af8874b6ef2b63a63d1)

# やったこと
API GatewayのCustom Authorizerで、対象メソッドを実行できるsourceIPを記載したPolicyを設定することで実現できました。

* API Gatewayの作成
* Custom Authorizer用のLambdaの作成
* API GatewayにCustom Authorizerを作成
* メソッドに作成したCustom Authorizerを設定
* テスト


# Custom Authorizer概要
API Gatewayの認証をLambdaで実装するものです。APIが受け取ったヘッダーやパラメータを自分で作成したLambda関数に渡し、値に応じてPolicyを返却、そのPolicyを評価してAPIのバックエンド処理の実行を制御します。  
![custom-auth-workflow.png](https://qiita-image-store.s3.amazonaws.com/0/29276/0ff3b6d2-9356-215f-010e-ebc71528df45.png)

公式ドキュメントはこちら。
https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/use-custom-authorizer.html


この図の「Lambda Auth Function」で返却するPolicyのConditionにSourceIPを書いてあげることで実行を制限できます。

ポリシー例：

```
{
  'principalId': user-identify-string,
  'policyDocument': {
    'Version': '2012-10-17',
    'Statement': [{
      'Action': 'execute-api:Invoke',
      'Effect': 'Allow',
      'Condition': {
        'IpAddress': {
          'aws:SourceIp': [source-ip-address],
        }
      },
      'Resource': ['arn:aws:execute-api:region:account-id:api-id/stage/method-type/method-name']
    }]
  },
  'context': {'message': 'Allowed!!'}
}
```

# 検証

作ったもの

* API Gateway: sampleApi
* Lambda
  * sampleHelloWorld - APIのバックエンド用
  * sampleAuth - APIのCustom Authorizer用

## API作成
テキトーにJSONレスポンスを返すGetメソッドのAPIを作ります。（具体的な手順は省略）  

* Lambda(sampleHelloWorld)

```helloWorld.py
def lambda_handler(event, context):
    return {
        'status': 'ok',
        'message': 'Hello from Lambda'
        }
```

* API Gateway

<img width="996" alt="api-sample.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/29276/45448e2f-4f1c-2fd2-abb9-1612f58418f5.jpeg">

## Custom Authorizer用Lambdaの作成
Policyを返すLambdaを作成します。本来ならヘッダーに入ってくるトークンの検証など、よりセキュリティを担保した作りにするべきですが、今回は検証のためPolicyを返すだけのLambdaにしています。

```sampleAuth.py
def lambda_handler(event, context):
    
    policy_doc = {
        'principalId': event['authorizationToken'],
        'policyDocument': {
            'Version': '2012-10-17',
            'Statement': [{
                'Action': 'execute-api:Invoke',
                'Effect': 'Allow',
                'Condition': {
                    'IpAddress': {
                        'aws:SourceIp': ['<ソースIPアドレス>'],
                    }
                },
                'Resource': ['<APIメソッドのARN>']
            }]
        },
        'context': {'message': 'Allowed!!'}
    }
    return policy_doc
```
* `principalId` - ユーザーを識別できる適当な文字列。上記の例ではトークンタイプのCustom Authorizerで取得できる、トークンを入れています。
* `policyDocument` - 返却するPolicy。ここで`Condition`にソースIPアドレスを指定しています。また、`Resource`にはAPIメソッドのARNを指定します。
* `context` - API Gatewayの統合バックエンドに渡せるキーマップ。オプション。

## APIにCustom Authorizerを作成する
APIの設定でトークンタイプのCustom Authorizerを作成します。  
コンソールでの設定例です。

<img width="723" alt="custom-auth-api.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/29276/5515a548-7677-8d83-f8f3-4274894fb4a1.jpeg">


* 「Lambda関数」で作成したAuth用Lambdaを設定します。
* イベントペイロードで「トークン」を選択します。
  * 「トークンのソース」にトークン名を指定します。API実行時にヘッダーとして必要になります。このトークンは、 Auth用Lambdaで`event['authorizationToken']`で取得できます。
  * 「トークンの検証」で、有効なトークン名を正規表現で指定します。API実行時にこの正規表現にマッチしない場合、Lambdaに渡す前に弾かれます。
* 認証のキャッシュは要件に合わせてどうぞ。

## メソッドにCustom Authorizerを設定する
Custom Authorizerは作成しただけでは有効になりません。メソッドの認証で設定する必要があります。
設定するメソッド＞メソッドリクエストと進んでいき、「認証」で作成したCustom Authorizerを選択することで設定できます。
<img width="1003" alt="custom-auth-api-conf.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/29276/67a67e3c-e591-1832-8cc8-b867cbc589fa.jpeg">

設定したら忘れずに「APIのデプロイ」をして設定を反映させましょう。

## テスト
ここまででAPI（メソッド）にCustom Authorizerを設定しました。
うまく動いているか実行してみます。

* 許可されたIPアドレスから実行

```
$ curl -H 'auth-token:1111' https://<hogehoge>.execute-api.ap-northeast-1.amazonaws.com/prd/sample
{"status": "ok", "message": "Hello from Lambda"}
```
APIが正常に実行され、作成したLambdaのレスポンスが返ってきました。

* 許可されていないIPアドレスから実行

```
$ curl -H 'auth-token:1111' https://<hogehoge>.execute-api.ap-northeast-1.amazonaws.com/prd/sample
{"Message":"User is not authorized to access this resource"}
```
認証失敗のメッセージが返ってきました。
うまく動いているようです！

また、トークンが設定した正規表現にマッチしない場合やトークンがないリクエストの場合、`{"message":"Unauthorized"}`とだけ返ってきます。

```
$ curl https://<hogehoge>.execute-api.ap-northeast-1.amazonaws.com/prd/sample
{"message":"Unauthorized"}
```

# 実際に使うときの注意点
今回は検証のために簡素な実装になってますが、実際に使う時はいくつか考えないといけないことがありそうです。  

#### トークンの検証
本番環境ならば、トークンの払い出しをして、API実行時にトークンの検証を行うのがよいです。DynamoDBでトークン管理している場合はLambdaでリクエストに含まれるトークンとDynamoDBのレコードを検証する、というようなフローが思いつきます。

#### カスタムオーソーライザーのレスポンスタイムへの影響
間に認証用Lambdaを挟むので、その分確実に遅くなります。まったく検証してないのでどのくらい遅くなるかは不明（実装による）なのですが、このレスポンスタイムへの影響を無視できない場合もありそうです。

# 最後に
API GatewayのIP制限を、Custom AuthorizerでソースIPアドレス制限つきのポリシーを返すことで実現しました。
久しぶりにAPI Gatewayを触ったのですが、機能多すぎじゃないですか？何人くらい使いこなせるんだろうか。あとだれかAPI Gatewayをセキュアに使うプラクティスのまとめとかあったら教えてください（切実）。なければ書きます（怒）。