IAM認証のAWS API GatewayにPythonからSigV4署名してアクセスするには

AWSのAPI GatewayのAPIのメソッドの設定で、IAM認証を使っている場合、APIリクエスト時に署名が必要です。

API Gatewayのメソッド設定画面はこんな感じです。英語のマネジメントコンソールだと「Authorization」、日本語だと「認可」という欄です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490207/ee4811e6-5d2c-5370-1676-7c7fcdbbd690.png)

デフォルトはNONEという設定になっていますが、これをAWS_IAMにするとAPI Gatewayは届いたリクエストの署名を確認するようになります。リクエストする側が署名をする必要があります。

API Gatewayが署名を確認するようになるとAPI GatewayのリソースポリシーにてIAMユーザやIAMロールで制限をかけられるようになります。

Pythonにて署名を付けてAPIリクエストするサンプルコードを書いておきます。


# 前提

IAMユーザのアクセスキーが `~/.aws/credentials` に設定されていて、そのIAMユーザからIAMロールにスイッチするIAM権限があり、そのIAMロールでAPI Gatewayにアクセスするものとします。（`~/.aws/credentials` があればよいので、以下のサンプルコードはGCPのCompute Engineインスタンスで動かしました）


# Pythonコード

```python
import boto3
from botocore.awsrequest import AWSRequest
from botocore.auth import SigV4Auth
import urllib.request
import sys

endpoint_host = "xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com"
endpoint = "https://" + endpoint_host + "/stage"
path = "/hello"
url = endpoint + path

# .aws/credentials または環境変数でアクセスキーが設定されているIAMユーザのセッションを生成。
session = boto3.session.Session()
# .aws/credentials に複数のIAMユーザがある場合は profile_name を指定。
#session = boto3.session.Session(profile_name = "foo")

sts_client = session.client("sts")

# IAMロールのARNを指定して assume_role 。
# IAMロールが不要でIAMユーザのままでAPIリクエストする場合は、ここは不要。
assume_role_response = sts_client.assume_role(
    RoleArn = "arn:aws:iam::XXXXXXXXXXXX:role/ROLENAME",
    RoleSessionName = "test")

# assume_role で得られたトークンなどからIAMロールでのセッションを生成。
# IAMロールが不要でIAMユーザのままでAPIリクエストする場合は、ここは不要。
session = boto3.session.Session(
    aws_access_key_id = assume_role_response['Credentials']['AccessKeyId'],
    aws_secret_access_key = assume_role_response['Credentials']['SecretAccessKey'],
    aws_session_token = assume_role_response['Credentials']['SessionToken'])

# セッション情報からAPIリクエストに署名。
credentials = session.get_credentials()
awsreq = AWSRequest(method = "GET", url = url)
SigV4Auth(credentials, "execute-api", "ap-northeast-1").add_auth(awsreq)

# urllib.request.Request 生成。
# この4つのリクエストヘッダーが必須。
# IAMロールが不要でIAMユーザのままでAPIリクエストする場合は、X-Amz-Security-Token は不要。
req = urllib.request.Request(url, headers = {
    "Authorization": awsreq.headers['Authorization'],
    "Host": endpoint_host,
    "X-Amz-Date": awsreq.context['timestamp'],
    "X-Amz-Security-Token": assume_role_response["Credentials"]["SessionToken"]
})

# APIリクエスト実行
try:
    with urllib.request.urlopen(req) as response:
        # レスポンス出力
        sys.stdout.buffer.write(response.read())
except urllib.error.HTTPError as err:
    # 403などの場合はここに到達
    # エラーを出力
    print(err)
    # レスポンスヘッダを出力
    print(err.headers)
```


# エラーメッセージの例

IAMユーザに assume_role する権限がないと assume_role を呼び出したところで

```
botocore.exceptions.ClientError: An error occurred (AccessDenied) when calling the AssumeRole operation
```

という例外が発生します。

`X-Amz-Security-Token` が必要なのに足りてないと、403 Forbidden が返され、レスポンスヘッダに次のように書かれます。

```
x-amzn-ErrorType: UnrecognizedClientException
```

API GatewayのリソースポリシーでこのIAMロールからのリクエストを拒否していると、403 Forbidden が返され、レスポンスヘッダに次のように書かれます。

```
x-amzn-ErrorType: AccessDeniedException
```

なお、IAMロールにAPI GatewayにアクセスするIAM権限がなくても、API Gatewayのリソースポリシーで許可していれば、AccessDeniedException にはならずに正常に処理できるようです。


# リンク

C#での記事も書きました。

- [IAM認証のAWS API GatewayにC#からIAMユーザでSigV4署名してアクセスするには](https://qiita.com/suzuki-navi/items/385deb31f36a2a66fac0)
- [IAM認証のAWS API GatewayにC#からIAMロールでSigV4署名してアクセスするには](https://qiita.com/suzuki-navi/items/4bf03406069761b8257f)