API GatewayにLamba Authorizerを組み込む

## はじめに
API GatewayにLambda Authorizerを組み込んで、以下のことを実施しました。
* Headerにセットされている値をチェックする認証処理の実装。
* 後続のバックエンド（Lambda）に渡すデータをセット。

一応前回までで構築しているPubSubの仕組みにAuthorizerを追加した構成になりますが、Authorizerについては前提構成なしで本記事だけで内容を追っていけると思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/0f329dcd-32a5-4c4c-4cf7-b4401292c23a.png)
前回までの構成は以下記事で投稿しています↓

https://qiita.com/kennyQiita/items/9311e065529785ed7f50

以下順序で整理していこうと思います。
1. Authorizer用のLambdaの作成。
1. コンソールからAPI GatewayにAuthorizerを設定する。
1. 後続Lambdaに追加データを引渡すようにAuthorizer Lambdaを修正。

## Authorizer用のLambdaの作成
関数名を`AuthForPubSub`とし、Authorizer用の関数をpythonで作成していきます。
今回はリクエストヘッダーを確認したいのでチェックする`token`に`event['headers']['Authorization']`を指定します。値が`abc`であれば後続Lambdaを呼び出すポリシーを返却します。
```python
import json
from logging import getLogger, INFO

logger = getLogger(__name__)
logger.setLevel(INFO)

def lambda_handler(event, context):
    print("============ output event =================")
    logger.info(json.dumps(event))
    
    token = event['headers']['Authorization']
    effect = 'Deny'
    if token == 'abc':
        effect = 'Allow'
        
    return_policy = {
        'principalId': '*',
        'policyDocument': {
            'Version': '2012-10-17',
            'Statement': [
                {
                    'Action': '*',
                    'Effect': effect,
                    'Resource':  event['methodArn']
                }    
            ]
        }
    }
    
    logger.info(json.dumps(return_policy))
    return return_policy
```

## API Gatewayコンソールにて、Authorizerの設定
API Gatewayのコンソールから対象のAPIを選択し、`Authorizers`>>`Create New Authorizer`からAuthorizerを選択します。
Lambda関数は先ほど作成した関数を指定します。Event Payloadを`Request`とし、`Header`に`Authorization`が含まれている前提で認証ロジックを実施したいので、以下のように設定します。
また、リクエスト毎にAuthorizerを起動させたいので、キャッシュはしません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/6e6a9d4a-3bc1-6777-6219-0bf30461f8e8.png)

ここで少し躓いたのですが、今回はコンソールからAuthorizerを設定しているため、以下の箇所でもAuthorizerの設定が必要です。（CloudFormationとかを利用していれば意識しなくてよいのかも）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/93920bb2-75c6-21ba-7655-3f2f256859d3.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/95441831-7e3b-a76a-d324-768ad914236c.png)

そして更に忘れがち（実際に忘れていて、テスト実行したらエラーとなって焦りました）なのが、このようにAuthorizerを設定したら再度APIをデプロイすることです。忘れずに実施しましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/efd88912-dbae-155a-f791-666a1e3ff7cf.png)

## 動作確認テスト
PostmanからRESTリクエストを実施します。まずは正常系から確認します。ヘッダーに`Authorization`をKeyとしてValueに`abc`をセットします。bodyは適当にセット。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/dd20e4f7-a5e9-0fd0-d259-4257e5489fd2.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/fd8f3525-0d60-a3ed-b034-d158d752ca1a.png)
リクエストをSendすると、無事にステータス200が返却され、バックエンドLambdaで設定していた返却値が確認できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/ad1e3280-52b4-ea67-0779-5041e1396fd8.png)
また、今回は事前に作成していたバックエンドの仕組みでDynamoDBに新規Itemも格納しています。そちらでも無事に`body`にセットした値が格納されているのを確認できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/1f55701f-1512-aee8-70d7-fb168d8d6a38.png)

次に異常系も確認していきます。まずはHeaderがセットされていないバージョン。
`401`の`Unauthorized`が返ってきました。Authorizerの設定の際に指定したHeaderがそもそもセットされていないので、Authorizer Lambdaに渡されるより前にAPI Gatewayではじかれています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/fa2b9806-1d9b-d448-d67c-fe5782f36d51.png)

次にHeaderのValueが異常値のケース。
こちらは`403`の`Forbidden`が返ってきました。AuthorizerのLambdaに渡った後に、ロジックの制御にて`Deny`のポリシーが返却されていることが分かります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/d5ca1daf-ce68-cc4a-d8fa-bd1d4a625afe.png)
このような違いを見てみると、Authorizerの動作なども細かく確認できます。

## 後続Lambdaにデータを引渡すようにAuthorizer Lambdaを修正。
次に、Authorizer側で所定の値（もしくはDBなどから取得したリクエストに適した値など）をセットするような使い方を検証してみます。

### Authorizer Lambdaの更新
以下のように修正します。
```python
import base64
import json
from logging import getLogger, INFO

logger = getLogger(__name__)
logger.setLevel(INFO)

def lambda_handler(event, context):
    print("============ output event =================")
    logger.info(json.dumps(event))
    
    token = event['headers']['Authorization']
    effect = 'Deny'
    if token == 'abc':
        effect = 'Allow'
        
    context = {
        "overwrite_message": "this is override message from authorizer."
    }
    
    json_context = json.dumps(context)
    base64_context = base64.b64encode(json_context.encode('utf8'))
        
    return_policy = {
        'principalId': '*',
        'policyDocument': {
            'Version': '2012-10-17',
            'Statement': [
                {
                    'Action': '*',
                    'Effect': effect,
                    'Resource':  event['methodArn']
                }    
            ]
        },
        'context': {
            'overwrite_message': base64_context.decode('utf-8')
        }
        
    }
    
    logger.info(json.dumps(return_policy))
    return return_policy
```
* `return`データに含まれている`context`に、`overwrite_message`という名前でデータを渡しています。
* contextにはJSONや配列を設定できないため、JSONをbase64でエンコードした文字列を返す様にしています。

バックエンド側のLambdaでは`overwrite_message`を利用して、リクエスト時に`body`にセットされたメッセージを上書きするような内容に編集しました。
```python
import base64
import boto3
from datetime import datetime
import json

client = boto3.client('sns')

def logging(errorLv, LambdaName, errorMsg):
    loggingDateStr=(datetime.now()).strftime('%Y%m%d %H:%M:%S')
    print(loggingDateStr + " " + LambdaName + " " + "[" + errorLv + "] " + errorMsg)
    return

def lambda_handler(event, context):
    
    logging("info", context.function_name, "lambda started")
    
    overwrite_message = event['requestContext']['authorizer']['overwrite_message']
    overwrite_message = base64.b64decode(overwrite_message)
    overwrite_message = json.loads(overwrite_message)
    print(overwrite_message)
    
    params = {
        'TopicArn': 'arn:aws:sns:ap-northeast-1:xxxxxxxxxxxx:myFirstTopic_std',
        'Subject' : 'Published From: deliverTopicToSNS_Test01',
        # 'Message' : event['body']
        'Message' : overwrite_message['overwrite_message']
    }
    
    try:
        response = client.publish(
            TopicArn = params['TopicArn'],
            Subject = params['Subject'],
            Message = params['Message']
        )
        
        print(json.dumps(response))
        return {
            'statusCode': 200,
            'body': json.dumps('A new message has been published')
        }
        

    except Exception as e:
        print(e)
        raise e
```
`event['requestContext']['authorizer']['overwrite_message']`からAuthorizer Lambdaでセットしたメッセージを取得しています。

### 再び動作確認
再度動作確認をPostmanから実施します。ヘッダーは正常値に戻し、リクエストを実施。
リクエストの`body`では以下のようにメッセージをセットしていますが、、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/7a1c026f-c9ca-22a0-7944-9d8a2a4533dc.png)
DynamoDBに新規追加されたItemを確認すると、無事に上書きされたメッセージとなっていることを確認できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484097/e573b95a-0dac-1624-6616-ff8456c8b9c8.png)

## 終わりに
今回API GatewayにLambda Authorizerを追加してみて、柔軟なAPIアクセス制限を実現できることが分かりました。そもそもバックエンド側へ渡したくないようなリクエストはAuthorizerではじけますし、Authorizerロジックとしてはリクエストの内容を確認してDBと参照させたり、後続バックエンドLambdaに渡す`context`をコントロールすることで、後続Lambda側でのパターン分けロジックなどにもつなげられそうです。

## 参考サイト
https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html

https://dev.classmethod.jp/articles/lambda-authorizer/

https://qiita.com/sugimount-a/items/0079a79b94e442204d6f

https://qiita.com/sekitaka_1214/items/e7118146f7b4328c5b74