AWS API Gateway - IAM アクセス許可により API へのアクセスを制御する

## はじめに
lambda や API の実態と認証のロジックを切り離したい。
そしたらもっとシンプルにバックエンド開発ができる。
API はすべて API Gateway を通る構成なので、API Gateway をリバースプロキシのように扱い、そこでアクセス制御をしたい。

## API Gateway で IAM アクセス制御を設定すると...

> AWS サービスがリクエストを受信すると、リクエストで送信した署名を計算したときと同じステップが実行されます。続いて AWS は、計算された署名とそのリクエストで送信した署名を比較します。署名が一致すると、リクエストが処理されます。署名が一致しない場合、リクエストは拒否されます。

https://docs.aws.amazon.com/ja_jp/general/latest/gr/signature-version-4.html

この「**署名**」を作る処理がとても面倒だったので、メモを残しておきます。

## 準備

##### 1. API Gateway で認可の設定を 「AWS IAM」 に設定する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62209/d34bf3f2-e73a-b97c-71a7-57e435b2449c.png)


##### 2. API Gateway へのアクセス権限をもつ IAM ユーザーを用意する

プログラムからアクセスするので、ユーザーは 「プログラムによるアクセス」 にチェック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62209/a9688c90-b94d-5223-bc16-a7874769a742.png)

ポリシーの設定
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62209/f5d4b282-bd08-2e72-e96f-b88375480a68.png)

著名をつけずに実行しても

```
{"message":"Missing Authentication Token"}
```

のように、リクエストが拒否される。

## 実装

最初は [こちら](https://dev.classmethod.jp/articles/api-gateway-iam-authentication-sigv4/) を参考に著名を作成しようと思ったが、typescript だといまいちだったので、別の方法を模索。
どうやら [AWS SDK for JavaScript の v3](https://github.com/aws/aws-sdk-js#aws-sdk-for-javascript) だと著名作成の処理が公開されているようなので、それを使用。

以下の記事をからほぼ流用させてもらった。
実際に扱うなら、記事のコードをそのまま使用したほうが良い。（というか実践的で完成度高いです。）

https://spin.atomicobject.com/2021/12/20/securing-api-gateway-aws-iam/

なかなか難しかったので自分なりに砕いたのが下記のコード。

```typescript
import { SignatureV4 } from "@aws-sdk/signature-v4";
import { Sha256 } from "@aws-crypto/sha256-js";
import { HttpRequest } from "@aws-sdk/protocol-http";
import axios from "axios";

const accessKeyId = "IAMのアクセスキーID"; // process.env.IAM_ACCESS_KEY_ID!
const secretAccessKey = "IAMのシークレットアクセスキー"; // process.env.IAM_SECRET_ACCESS_KEY_ID!
const hostname = "xxxxxxx.execute-api.ap-northeast-1.amazonaws.com";

// 著名を作るメソッド
async function signRequest(
  headers: Record<string, string>,
  method: string,
  path: string,
  body?: unknown
) {
  const request = new HttpRequest({
    body: body ? JSON.stringify(body) : undefined,
    headers,
    hostname: hostname,
    method: method.toUpperCase(),
    path,
  });

  const signer = new SignatureV4({
    credentials: {
      accessKeyId,
      secretAccessKey,
    },
    service: "execute-api",
    region: "ap-northeast-1",
    sha256: Sha256,
  });

  const signedRequest = await signer.sign(request);
  return signedRequest;
}

async function main() {
  const headers: Record<string, string> = {
    host: hostname,
    "Content-Type": "application/json",
  };
  const path = "/hello";
  const data = {
    name: "tubakuro",
  };
  
  // 著名（ヘッダー）を作成して
  const req = await signRequest(headers, "POST", path, data);
  // 著名と同じ内容でリクエスト
  const resp = await axios.post(`https://${hostname}${path}`, data, {
    headers: req.headers,
  });
  console.dir(resp.data);
}

main();
```

以下のようなリクエストが作られて、リクエストが成功したらOK

```
POST /hello HTTP/1.1
x-amz-date: 20220222T135041Z
x-amz-content-sha256: xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
authorization: AWS4-HMAC-SHA256 Credential=xxx, SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date, Signature=xxx
Content-Length: xx
Content-Type: application/json
Host: xxxxxxx.execute-api.ap-northeast-1.amazonaws.com
[body]
```

GET の場合だとこう。 axios GET時の仕様がややこい。

```typescript
async function main() {
  const headers: Record<string, string> = {
    host: hostname,
  };
  const path = "/hello";
  const req = await signRequest(headers, "GET", path);
  const resp = await axios.get(`https://${hostname}${path}`, {
    headers: req.headers,
  });
  console.dir(resp.data);
}
```

## まとめ
一度作ってしまえば楽ちんですね 😇

AWS SDK for JavaScript v3 のリファレンス置いておきます。

https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/index.html