IAM認証がかかったAPI Gatewayで作成したAPIをLambda(Python)から叩く

本題は[LambdaでIAM認証がかかったAPIを叩く方法](#lambdaでiam認証がかかったapiを叩く方法
)からですので、コードだけ知りたいよ、という方はこちらからどうぞ。

# API Gatewayで利用できる認証方法
API Gatewayで利用可能な認証方法にはいくつかあります。REST API、HTTP APIの両方に共通している認証方法として、 ①Lambdaオーソライザー ②JWT オーソライザー ③標準のAWS IAMロールとポリシーの3つがあります。

③については、AWSコンソール上では「IAM認証、IAM(組み込み型)」（HTTP API）、「認可：AWS_IAM」（REST API）と表記されます。

https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/apigateway-control-access-to-api.html

https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/http-api-access-control.html

:::note alert
REST APIでは使用量プランが利用でき、`x-api-key`ヘッダーにAPIキーを含めることで認証のようなことが可能ですが、AWSは推奨していません。
> API キーを、API へのアクセスを制御するための認証または認可に使用しないでください。使用量プランに複数の API がある場合、その使用量プランの 1 つの API に対して有効な API キーを持つユーザーは、その使用量プランのすべての API にアクセスできます。代わりに、API へのアクセスを制御するには、IAM ロール、Lambda オーソライザー、または Amazon Cognito ユーザープールを使用します。
https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/api-gateway-api-usage-plans.html

:::

# IAM認証をかけると認証なしのリクエストはエラーとなる
認証が不要なAPI（パブリックAPI）は基本的に外部に公開されているものになるので、URLさえ分かれば誰でもAPIを叩くことが可能です。(XXXXXXXXX はAPI作成時に生成されるランダムな文字列です)
![スクリーンショット 2025-03-15 14.08.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2448259/a368a428-22de-41a1-a9b3-234463da05e6.png)

REST APIでサンプルAPIを作成しました。何も認証がなければcurl等でAPIを叩くことが可能です。
```shell-session
$ curl -X GET "https://XXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/dev/"
<html>
    <head>
    
(中略)

{
    "type" : "cat",
    "price" : 123.11
}
        </pre>
    </body>
</html>%   
```

ここにIAM認証を設定してみます。
REST APIの場合は認証を設定したいリソースを選択した上で、メソッドリクエストを編集します。
![ddpkj3yc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2448259/245f6de3-aa45-46ea-a4a3-313bb313fce1.png)


編集画面から、認可のところで「AWS IAM」を選択すると設定が可能です。
![スクリーンショット 2025-03-15 14.23.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2448259/b6c9fdd0-8d03-40b4-b795-96e7796432bb.png)

:::note warn
メソッドリクエストを編集した場合は、APIをデプロイしないと設定が反映されません。
:::


この設定を入れた上で先ほどと同じようにcurlでAPIを叩くと先ほどとは異なる結果が表示されます。
```shell-session
$ curl -X GET "https://XXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/dev/"
{"message":"Missing Authentication Token"}%    
```

-vオプションをつけて実行すると詳細な情報が見ることができますが、403エラーとなっていることがわかります。
```shell-session
$ curl -v -X GET "https://XXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/dev/"

(中略)

* Request completely sent off
< HTTP/2 403 
< date: Sat, 15 Mar 2025 13:55:27 GMT
< content-type: application/json
< content-length: 42
< x-amzn-requestid: 3eeae658-a67f-4671-9cef-018e7098614e
< x-amzn-errortype: MissingAuthenticationTokenException
< x-amz-apigw-id: HeIAfHd9tjMEvPw=
< 
* Connection #0 to host XXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com left intact
{"message":"Missing Authentication Token"}%  
```


# IAM認証がかかったAPIはSigV4署名が必要
普段、AWS SDKやAWS CLIを利用する時は意識する必要がありませんが、AWS内部のAPIが呼ばれる際にはSigV4による署名が必要になります。

:::note info
> AWS SDK (「サンプルコードとライブラリ」を参照) または AWS Command Line Interface (AWS CLI) ツールを使用して API リクエストを AWS に送信する場合、SDK および CLI クライアントが指定したアクセスキーを使用してリクエストを認証するため、署名プロセスをスキップできます。正当な理由がない限り、常に SDK または CLI を使用することをお勧めします。
https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/reference_sigv.html

:::


curlでAPIを叩く際には下記のようなリクエストになります。

:::note warn
あらかじめ認証情報を取得する必要があります。下記コマンドを実施する場合は、aws configureなどで認証情報をローカルで設定してから実行してください。
:::

--aws-sigv4、--userオプション、X-Amz-Security-Tokenヘッダーが必要です。
```
$ curl -X GET "https://XXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/dev/" --aws-sigv4 "aws:amz:ap-northeast-1:execute-api" --user "$(aws configure get aws_access_key_id):$(aws configure get aws_secret_access_key)" -H "X-Amz-Security-Token: $(aws configure get aws_session_token)"
<html>
    <head>
    
(中略)

{
    "type" : "cat",
    "price" : 123.11
}
        </pre>
    </body>
</html>%    
```


# LambdaでIAM認証がかかったAPIを叩く方法
さて、ここからが本題。LambdaがIAM認証が必要なAPIを叩く場合の方法です。今回はPython3.12で作成します。

まずはLambda関数を作成します。
![スクリーンショット 2025-03-15 16.41.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2448259/c309f99e-19cf-4cf1-944a-1333905670bd.png)

その後は後述する2パターンのいずれかのコードで、SigV4署名が可能になります。

また、このままだとLambdaにexecute-apiの実行権限がないので、__LambdaについているIAMロールに、「execute-api:Invoke」の権限を追加します__（ResourceをIAM認証をかけているAPIに限定するとなお良いです）。ここではインラインポリシーを作成し、IAMロールにつけています。

```json:inline_policy
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "execute-api:Invoke",
            "Resource": "*"
        }
    ]
}
```

ちなみに、認証関連のコードが書かれていなければ、ステータスコードは403となります。
![スクリーンショット 2025-03-15 18.32.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2448259/6643ff4c-a90f-4e01-8058-53426200e63f.png)

## SigV4Auth
1つ目はSigV4Authを使ったコードになります。
「requests」のモジュールをimportするためにLambdaレイヤーを作成する必要があります。
```python:auth_request.py
import boto3
import json
import os
from botocore.awsrequest import AWSRequest
from botocore.auth import SigV4Auth
from botocore.endpoint import URLLib3Session
from botocore.credentials import Credentials
import requests

def lambda_handler(event, context):
    endpoint = "https://XXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/dev"
    lambda_credentials = boto3.Session().get_credentials()
    credentials = Credentials(lambda_credentials.access_key, lambda_credentials.secret_key, token=lambda_credentials.token)
    request = AWSRequest(method="GET", url=endpoint)
    SigV4Auth(credentials, "execute-api", "ap-northeast-1").add_auth(request)
    headers = {
        "Authorization": request.headers["Authorization"],
        "X-Amz-Date":request.context["timestamp"],
        "X-Amz-Security-Token": lambda_credentials.token
    }

    response = requests.get(endpoint, headers=headers)

    return {
        "statusCode": response.status_code,
        "body": json.dumps(response.text)
    }
```

## AWS4Auth
AWS4Authでは以下のようなコードを書きました。AWS4Authを利用する場合は「requests」と「requests_aws4auth」のモジュールをimportするためにLambdaレイヤーを作成する必要があります。

```python:auth_request.py
import boto3
import json
import requests
from requests_aws4auth import AWS4Auth

def lambda_handler(event, context):
    endpoint = "https://XXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/dev"
    service = "execute-api"
    region = "ap-northeast-1"
    headers = {"Content-Type": "application/json"}
    credentials = boto3.Session().get_credentials()
    auth = AWS4Auth(credentials.access_key, credentials.secret_key, region, service, session_token=credentials.token)
    response = requests.get(endpoint, auth=auth, headers=headers)
    return {
        "statusCode": response.status_code,
        "body":json.dumps(response.text)
    }
```


いずれの手法でも、正しく設定されていればLambdaが正しく実行され、ステータスコード200でレスポンスが返ってきます。
![スクリーンショット 2025-03-15 18.30.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2448259/70a109fd-6653-4985-b3ec-7cb273c87bac.png)

# まとめ
利用する場面はあまりないと思いますが、IAM認証がかかっているAPIについて、Lambdaから叩く方法を紹介しました。AWS4Authを利用したコードの方がシンプルでわかりやすいため、個人的には好ましいと感じます。
# 参考

https://aws.amazon.com/jp/builders-flash/202211/way-to-operate-api-3/