API Gateway Lambdaオーソライザーでアクセス制御する

API Gateway Lambdaオーソライザーを使ってAPIのアクセス制御を行う方法についてのメモです。実行環境はnode.js(8.10)を使用しました。
## アーキテクチャ
<div align="center">
<img src="https://qiita-image-store.s3.amazonaws.com/0/247791/5fd6ea2c-e1e8-d4e3-9c0c-6081cb058a25.png" title="lambda_authorizer_archi">
</div>
API Gatewayを介してオーソライザー用関数（認証する関数）を呼び、認証が通ればAPI（アクセスを制御したい関数）を呼び出す、というフローになります。
## 構築手順
#### アクセスを制御したいLambda関数を作成
APIにあたるLambda関数を作成しておきます。ここでは「test_api」という関数を作成しました。

```js:index.js
exports.handler = async (event) => {
    // TODO implement
    const response = {
        statusCode: 200,
        body: JSON.stringify('Hello from Lambda!'),
    };
    return response;
};
```

(自動で生成されるコードから何も変更してません...)

#### API Gatewayを作成
作成したLambda関数のトリガーとしてAPI Gatewayを選択します。
<img width="486" alt="1_lambda_auth.png" src="https://qiita-image-store.s3.amazonaws.com/0/247791/12ee6f4e-2e7c-ce48-54c5-579f8983292d.png">
新規APIを追加します。
<img width="585" alt="2_lambda_auth.png" src="https://qiita-image-store.s3.amazonaws.com/0/247791/c56804ca-2468-4c33-a9bd-0b64a703672a.png">
以下のようなAPIが自動生成されました。

<dt>API</dt>
<dd>api-gateway/{api-id}/\*/\*/test_apiAPI</dd>
<dt>エンドポイント</dt>
<dd>https://{api-id}.execute-api.{region}.amazonaws.com/default/test_api</dd>
<dt>API名</dt>
<dd>test_api-API</dd>
#### オーソライザー用関数を作成
公式ドキュメントのサンプル[^1] を参考に以下のような関数を作成します。

```js:auth.js
exports.handler = function(event, context, callback) {
    // HTTPヘッダで送られてくるトークンを取得して...
    var token = event.authorizationToken;
    // 値が allow だったらAPI実行するためのIAMポリシーを生成して返す
    if (token.toLowerCase() == 'allow') {
        callback(null, generatePolicy('user', 'Allow', event.methodArn));
    } else {
        callback(null, generatePolicy('user', 'Deny', event.methodArn));
    } 
};

// IAMポリシーを生成し返却します
var generatePolicy = function(principalId, effect, resource) {
    var authResponse = {};
    
    authResponse.principalId = principalId;
    if (effect && resource) {
        var policyDocument = {};
        policyDocument.Version = '2012-10-17'; 
        policyDocument.Statement = [];
        var statementOne = {};
        statementOne.Action = 'execute-api:Invoke'; 
        statementOne.Effect = effect;
        statementOne.Resource = resource;
        policyDocument.Statement[0] = statementOne;
        authResponse.policyDocument = policyDocument;
    }
    
    return authResponse;
}
```

オーソライザー用関数はIAMポリシーを返却します。
認証が通り、APIの実行を許可する場合は、そのLambda関数を実行するためのIAMポリシーを生成して返してあげる必要があります。
生成したポリシーを受け取った API Gateway がAPIのLambda関数を実行します。
ここでは認証用トークンが「allow」であれば許可し、それ以外は拒否するというBasic認証のようなシンプルな処理になっています。
(トークンはHTTPヘッダとして送信されますが、後ほどAPI Gatewayでヘッダの設定をします。)

#### API Gatewayのオーソライザーを作成
追加したAPI GatewayのAPI(test_api-API)を開き、オーソライザー ＞ + 新しいオーソライザーの作成 をクリックして、以下のような設定で作成します。

<dt>名前</dt>
<dd>sample-auth</dd>
<dt>タイプ</dt>
<dd>Lambda</dd>
<dt>Lambda呼び出しロール</dt>
<dd><空欄のまま></dd>
<dt>Lambda イベントペイロード</dt>
<dd>トークン</dd>
<dt>トークンのソース（ヘッダキーになります。）</dt>
<dd>Auth</dd>

#### オーソライザーをテストする
オーソライザーを作成したら、オーソライザー用関数を呼び出すテストをしてみます。API Gatewayで作成したオーソライザーに「テスト」というボタンがあるので、それをクリックすると、テスト用のウィンドウがでてきました。
認証トークン(上記の トークンのソース: Auth と設定したやつ)を入力するフィールドがあるので、そちらに「allow」と入力すると、以下のようなレスポンスが返ってきます。上記サンプルコード`auth.js`の`generatePolicy`で生成したやつですね。

```
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "execute-api:Invoke",
      "Effect": "Allow",
      "Resource": "arn:aws:execute-api:{region}:{accountId}:{appId}/ESTestInvoke-stage/GET/"
    }
  ]
}
```

APIの実行を許可するIAMポリシーが返ってきたみたいです。
「allow」以外の値を送信すると、

```
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "execute-api:Invoke",
      "Effect": "Deny",
      "Resource": "arn:aws:execute-api:{region}:{accountId}:{appId}/ESTestInvoke-stage/GET/"
    }
  ]
}
```

拒否するポリシーが返ってきました。
#### API Gateway リソースの設定
ところで、Lambdaにおけるトリガーの追加で自動生成したAPIのリソースを見てみると、以下のようになっています。
<img width="221" alt="3_lambda_auth.png" src="https://qiita-image-store.s3.amazonaws.com/0/247791/f2ed87dd-fedc-ac7a-fb6d-a8c553f86848.png">
しかし前項でテストした際に返ってきた"Resource"は`.../ステージ(ESTestInvoke-stage)/GET/`に対して許可していますが、リソースのルート「/」直下にGETメソッドはありませんね...。
オーソライザー用関数を変更してもいいですが、ここではIAMポリシーにあわせてリソースを変更しちゃいましょう。

<img width="220" alt="4_lambda_auth.png" src="https://qiita-image-store.s3.amazonaws.com/0/247791/4dd14332-4982-fc95-95f9-b8c0878fd60e.png">

`/*/test_api`リソースはとりあえず使用しないので削除しました。
作成したANYメソッドの設定にて、認証を作成したオーソライザーを選択します。

<img width="262" alt="5_lambda_auth.png" src="https://qiita-image-store.s3.amazonaws.com/0/247791/b522e539-4750-e3ad-a869-fb9ef93e33bf.png">

設定したらAPIをデプロイしましょう！これで完成です！

#### 動作確認

API Gateway のステージから確認できる呼び出しURLを実行してみます。

ヘッダに設定した認証用トークンを入れて実行すると、正常に実行されました！

```
$ curl -H "Auth: allow" https://{api-id}.execute-api.{region}.amazonaws.com/default
"Hello from Lambda!"
```

不正な認証用トークンをいれてみると、ちゃんと拒否されました。

```
$ curl -H "Auth: hoge" https://{api-id}.execute-api.{region}.amazonaws.com/default
{"Message":"User is not authorized to access this resource with an explicit deny"}
```

ヘッダになにも設定せずに呼び出すとUnauthorizedになります。

```
$ curl https://{api-id}.execute-api.{region}.amazonaws.com/default
{"message":"Unauthorized"}
```

以上です。

[^1]: [API Gateway Lambda オーソライザーの使用](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html)