【AWS】API GateWay + LambdaでSlakcアプリケーションの認証を行う方法

SlackのEventをAPI GateWayで処理し、Lambdaを起動しています。
APIのアドレスが漏れた場合、攻撃を受ける可能性が有る為、API GateWayで何かしらの対策ができないかと思い記事を書こうと思いました。

# Slackの認証方法について
以下の2種類があります。

- リクエストのBody内の文字列を利用した認証
- Signing Secretを用いた認証

# リクエストのBody内の文字列を利用した認証

## SlackのEvent APIについて
Eventが発生すると、下記のようなJSONが発行されます。

```json:Slackのイベント
{
        "token": "XXYYZZ",
        "team_id": "TXXXXXXXX",
        "api_app_id": "AXXXXXXXXX",
        "event": {
                "type": "name_of_event",
                "event_ts": "1234567890.123456",
                "user": "UXXXXXXX1",
                ...
        },
        "type": "event_callback",
        "authed_users": [
                "UXXXXXXX1",
                "UXXXXXXX2"
        ],
        "event_id": "Ev08MFMKH6",
        "event_time": 1234567890
}
```

`token、team、そしてapi_app_idフィールドでは、妥当性、起源、およびリクエストの送信先を識別するのに役立ちます。`
と公式サイトに記載されています。

## tokenの内容で認証を行う方法
Slackアプリの「App Credentials」のページ内に記載されている、`Verification Token`を用いて認証を行います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401878/e84e933f-f862-eea9-786e-5c9fc0de0034.png)

`Verification Token`をLambdaの環境変数「VERIFICATION_TOKEN_APPTEST_WS」に設定して利用します。
eventの内容は、Slackから来るPOSTのBodyが格納されています。

```py:確認用コード
        # TOKEN認証
        if event['token'] != os.environ['VERIFICATION_TOKEN_APPTEST_WS']:
            return {
               'statusCode': 200,
                'body': json.dumps('Error'),
                'text' : 'Token不整合' 
            }            
```

※同様に、App_IDでも認証をする事が可能です。

## Signing Secretを用いた認証を行う方法

[Verifying requests from Slack](https://api.slack.com/docs/verifying-requests-from-slack) の情報を参考に認証を設定していきます。

### API GateWayの設定
SlackのBody以外の内容を受け取れるようにする為に、API GateWayの設定を行います。

下記の図の`統合リクエスト`をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401878/22c82820-4b95-4b86-4652-ff53c41326b2.png)

`Lambda プロキシ統合の使用`のチェックを有効にします。

#### 「Lambda プロキシ統合の使用」を有効化について
「Lambda プロキシ統合の使用」を有効化する事で、これまで、API GateWayが整形していたHTTPヘッダーの情報をきちんとLambda関数で受け取る事ができるようになります。

例）
・「Lambda プロキシ統合の使用」を無効化した状態
　→ HTTPヘッダーの情報が切り捨てられ、Lambdaのeventに渡される内容は、Bodyの内容のみ

```json:無効時
{ Message: 'Hello World!' }
```

・「Lambda プロキシ統合の使用」を有効化した状態
　→ HTTPヘッダーの情報も含めてLambdaのeventに渡される
　※ 注意！ eventの内容はJSONではなく、文字列で渡される為、注意が必要です。

```json:有効時
{
    resource: '/mook',
    path: '/mook',
    httpMethod: 'POST',
    headers: { Accept: ' application/json' },
    queryStringParameters: { param1: 'value1' },
    pathParameters: null,
    stageVariables: null,
    requestContext: {
        accountId: 'xxxxxxxxxxxx',
        resourceId: 'xxxxxx',
        stage: 'test-invoke-stage',
        requestId: 'test-invoke-request',
        identity: {
            cognitoIdentityPoolId: null,
            accountId: 'xxxxxxxxxxxx',
            cognitoIdentityId: null,
            caller: 'xxxxxxxxxxxx',
            apiKey: 'test-invoke-api-key',
            sourceIp: 'test-invoke-source-ip',
            cognitoAuthenticationType: null,
            cognitoAuthenticationProvider: null,
            userArn: 'arn:aws:iam::xxxxxxxxxxxx:root',
            userAgent: 'Apache-HttpClient/4.5.x (Java/1.8.0_102)',
            user: 'xxxxxxxxxxxx'
        },
        resourcePath: '/mook',
        httpMethod: 'POST',
        apiId: 'xxxxxxxxxx'
    },
    body: '{"Message": "Hello World"}'
}
```

### Lambdaで認証の処理を作成
API GateWayでは認証の処理を実装する事ができないので、Lambdaに処理を実装します。

認証用の処理を記載したPythonファイル`slack_signing.py`を作成します。

```py:slack_signing.py
# -*- coding: utf-8 -*-
""" Slackの認証を行う処理 """
import hmac
import hashlib
import datetime
import logging

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


def __generate_hmac_signature(timestamp, body, secretkey):
    """ hmacの生成処理 """
    secretkey_bytes = bytes(secretkey, 'UTF-8')

    message = "v0:{}:{}".format(timestamp, body)
    message_bytes = bytes(message, 'UTF-8')
    return hmac.new(secretkey_bytes, message_bytes, hashlib.sha256).hexdigest()


def is_valid_event(event, secretkey):
    """ 認証処理 """
    if "X-Slack-Request-Timestamp" not in event["headers"] \
        or "X-Slack-Signature" not in event["headers"]:
        print('API GateWayで「Lambda プロキシ統合の使用」が選択されているかチェックしてください。')
        return False

    request_timestamp = int(event["headers"]["X-Slack-Request-Timestamp"])
    now_timestamp = int(datetime.datetime.now().timestamp())

    if abs(request_timestamp - now_timestamp) > (60 * 5):
        return False

    expected_hash = __generate_hmac_signature(
        event["headers"]["X-Slack-Request-Timestamp"],
        event["body"],
        secretkey
    )

    expected = "v0={}".format(expected_hash)
    actual = event["headers"]["X-Slack-Signature"]

    logger.debug("Expected HMAC signature {} : {}".format(type(expected), expected))
    logger.debug("Actual HMAC signature   {} : {}".format(type(actual), actual))

    return hmac.compare_digest(expected, actual)
```

Lambdaのハンドラがあるソースで、`is_valid_event`を呼び出し、認証を行います。



```py:lambda_function.py
import json
import logging
import os

from slack_signing import is_valid_event

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

def lambda_handler(event: dict, context):

    # EventのVerify用
    logger.info(json.dumps(event))
    if "challenge" in event:
        return event.get("challenge")

    # Signing処理の呼び出し
    secretkey = os.environ['SLACK_API_SIGNING_SECRET']
    if is_valid_event(event, secretkey) is False:
        logger.error('×System Error!')
        return {
            'statusCode': 200,
            'body': json.dumps('OK')
        }

    # Bodyの内容は、文字列なので、ここでJSONに変換
    body = json.loads(event['body'])

    # 処理をここに書く
    return {
        'statusCode': 200,
        'body': json.dumps('OK')
    }

```

**SLACK_API_SIGNING_SECRET** を利用するために、環境変数に登録します。
下記の図の`Show`ボタンを押して「Signing Secret」を表示し、Lambdaの環境変数に登録します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401878/4360d40a-80ac-cac4-f1fa-ce1ce74efec3.png)

# まとめ
以上で認証については終了です。

今後、時間が有れば、API GateWayで、Tokenについて認証し、Lambdaで「Signing Secret」を利用し、認証する事ができるかも、試してみたいです。