SST版：Lambda AuthorizerでAPI Gateway V2（HTTP API）へのリクエストをCloudFrontだけに制限する

# はじめに
最近、SSTを使ったサーバーレスアプリケーションの構築にトライしました。

https://qiita.com/generokenken/private/0a0807e518e045798c7b

そして以前、CDKとSAMを使ったアプリケーションに対して以下の記事を書いたのですが、

https://qiita.com/generokenken/items/b3d86f23351bae533f9a

SST版でも同様の記事を書いておきたかったので、本記事の作成に至った形となります。

# 概要

API Gateway V2（HTTP API）のエンドポイントへのアクセスをCloudFront経由のみに制限したいが、HTTP APIはWAFのサポートがされていないため、Lambda Authorizerで制御します。そのための環境構築を、SSTを用いて行うといった内容になります。

[SSTでサーバーレス環境を構築してみたらすごく良かった](https://qiita.com/generokenken/private/0a0807e518e045798c7b)で登場したコードのカスタマイズを行う形になるため、記事を読んだ後の方が本記事の理解が進みやすいと思います。

※SSTのコードを中心とした簡潔な説明にしたいため、以前のセキュリティ記事では説明したシークレットのローテーション部分は省略します。

# 内容
以下順番で説明していきます。

1. バックエンドへのリクエストをCloudFront経由にする
2. API Gateway v2（HTTP API）側でLambda Authorizerを起動する

## 1. バックエンドへのリクエストをCloudFront経由にする
[SSTでサーバーレス環境を構築してみたらすごく良かった](https://qiita.com/generokenken/private/0a0807e518e045798c7b)の記事では、以下コードがCloudFrontの設定に相当する部分でした。

```sst.config.ts
new sst.aws.StaticSite("MyWeb", {
  domain: {
    // Route53のホストゾーンを参照する
    // Route53にAレコードやCNAMEの追加をしてくれる
    // 自動でus-east-1でSSL証明書を取得し、CloudFrontに証明書を紐づけてくれる
    name: "ドメイン名"
  },
  // 全てのパスのCloudFrontキャッシュが削除されるまで待機する設定
  // https://sst.dev/docs/component/aws/static-site#invalidation
  invalidation: {
    wait: true
  },
  build: {
    command: "npm run generate",
    output: ".output/public"
  },
});
```

よってこのコードを修正し、API Gateway を対象としたオリジンの追加・ビヘイビアの設定を行なっていきます。

具体的な設定方法に関してはサンプルコードがありますので、これを参考にしながら修正していきます。

https://sst.dev/docs/examples#aws-nextjs-add-behavior

まず、オリジンやビヘイビアをカスタマイズしようとすると、transformの設定が必要となります。transformにより、用意されたcomponentの構造を変換する形となります。

**transform**

https://sst.dev/docs/component/aws/static-site#transform

**transformに関して、ドキュメントの読み方を理解するのに手間取った**ので、以下にドキュメントの読み方を記載します。

- ドキュメント内のtransform.cdn?のType [CdnArgs](https://sst.dev/docs/component/aws/cdn/#cdnargs)をクリックし、[origins](https://sst.dev/docs/component/aws/cdn/#origins)と[orderedCacheBehaviors?](https://sst.dev/docs/component/aws/cdn/#orderedcachebehaviors)を確認します。

- originsでは、[DistributionOrigin](https://www.pulumi.com/registry/packages/aws/api-docs/cloudfront/distribution/#distributionorigin)を確認することでPulumi側のサイトに飛び、設定可能なパラメータを諸々確認できます。

- 同様にorderedCacheBehaviors?では、[DistributionOrderedCacheBehavior](https://www.pulumi.com/registry/packages/aws/api-docs/cloudfront/distribution/#distributionorderedcachebehavior)を確認することで、Pulumi側のサイトに飛び、設定可能なパラメータを諸々確認できます。

最終的な完成版はこちらです。

```sst.config.ts
// API Gateway Settings
    const api = new sst.aws.ApiGatewayV2("MyApi", {
      cors: {
        allowOrigins: [
          // cors許可するドメイン名
          "ドメイン名"
        ]
      }
    });


// 省略

    
// CloudFront Settings
// オリジン設定
    const origin = {
      domainName: $resolve(api.url).apply(url => new URL(url).host), // https://の除去を行い、ドメインのみ抽出する
      originId: "apiOrigin", // オリジン名になる
      customOriginConfig: {
        httpPort: 80,
        httpsPort: 443,
        originSslProtocols: ["TLSv1.2"],
        originProtocolPolicy: "https-only", // HTTPSのみに制限する
      },
      customHeaders: [{
        name: "x-origin-verify",
        value: "トークン値",
      }],
    };

// ビヘイビア設定
    const cacheBehavior = {
      pathPattern: "/api/*", // /api/* パスのリクエストを対象とする
      targetOriginId: origin.originId, // 上記originをターゲットに設定する
      viewerProtocolPolicy: "redirect-to-https", // HTTPのリクエストはHTTPSにリダイレクトする
      allowedMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"], // リクエスト許可するHTTPメソッド
      cachedMethods: ["GET", "HEAD"], // キャッシュ対象にするHTTPメソッド
      cachePolicyId: "4135ea2d-6df8-44a3-9df3-4b5a84be39ad", // CachingDisabled
      originRequestPolicyId: "b689b0a8-53d0-40ab-baf2-68738e2966ac", // AllViewerExceptHostHeader
    };


// Static Site Settings
    new sst.aws.StaticSite("MyWeb", {
      domain: {
        name: "ドメイン名"
      },
      invalidation: {
        wait: true
      },
      // Nuxt 3の静的build用。Viteではこのオプションは不要
      build: {
        command: "npm run generate",
        output: ".output/public"
      },
      // transformでcomponentの構造を変換する
      transform: {
        cdn: (options: sst.aws.CdnArgs) => {
          options.origins = $resolve(options.origins).apply(val => [...val, origin]);

          options.orderedCacheBehaviors = $resolve(
            options.orderedCacheBehaviors || []
          ).apply(val => [...val, cacheBehavior]);
        },
      },
    });
```

1点補足で、こちらのidに関しては、AWSのドキュメントに記載がありますのでそこから値を引っ張ってきました。
CachingDisabledとAllViewerExceptHostHeaderはAPI Gatewayを使う場合に推奨される設定となっているため、準拠した形となります。

```sst.config.ts
cachePolicyId: "4135ea2d-6df8-44a3-9df3-4b5a84be39ad", // CachingDisabled
originRequestPolicyId: "b689b0a8-53d0-40ab-baf2-68738e2966ac", // AllViewerExceptHostHeader
```

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/using-managed-cache-policies.html



## 2. API Gateway v2（HTTP API）でLambda Authorizerを起動する

まずは、認証に使うLambda関数に対して、認証ロジックを記載します。

```src/authorizer.ts
import { APIGatewayEvent } from 'aws-lambda';

export const handler = async (
  event: APIGatewayEvent,
): Promise<{ isAuthorized: boolean; context?: any }> => {
  const originVerify = event.headers?.['x-origin-verify'];

  try {
    // リクエストのヘッダーに含まれるトークンが一致しているかチェックする
    if (originVerify === "トークン値") {
      return {
        isAuthorized: true,
        context: {
          user: 'user',
        },
      };
    }
  } catch (error) {
    console.error('Error retrieving secret:', error);
  }

  return {
    isAuthorized: false,
  };
};
```
:::note warn
実際はトークンの値はSecretsに保存してそれを参照する形としていますが、今回は説明を簡易的にするため、**"トークン値"** という形で記載しています。
:::

次に、**addAuthorizer** のメソッドを使い、API Gatewayに対してLambda Authorizerを設定します。

https://sst.dev/docs/component/aws/apigatewayv2#addauthorizer

```sst.config.ts
// Lambda Authorizer
    const lambdaAuthorizer = api.addAuthorizer({
      name: "lambdaAuthorizer",
      lambda: {
        function: {
          handler: "src/authorizer.handler",
        },
        identitySources: ["$request.header.x-origin-verify"], // リクエストヘッダーのどの値を認証に使うか指定する
      },
    });
```


最後に、API Gatewayの各RouteにLambda Authorizerを紐づけます。

https://sst.dev/docs/component/aws/apigatewayv2#auth-lambda

```sst.config.ts
// API Route
    api.route("GET /api/get",
      {
        handler: "src/get.handler",
      },
      {
        auth: {
          lambda: lambdaAuthorizer.id,
        },
      }
    );
```

これで設定は完了となります！

# さいごに
前回の[【セキュリティ】Lambda AuthorizerでHTTP API GatewayへのリクエストをCloudFrontだけに制限する](https://qiita.com/generokenken/items/b3d86f23351bae533f9a)の記事と比較して内容を省略した部分があるとはいえ、SSTの構成ファイルの方がより見やすく簡潔な記述で済んだと感じます。
ただ、transformのプロパティを弄る場合の癖は少し強いかなと思いますので、初めてキャッチアップする際は少し大変かもしれません。