[AWS CDK] API Gateway REST API で Preflight request のレスポンスヘッダに Access-Control-Max-Age を設定する

2023.09.29

こんにちは、CX事業本部 Delivery部の若槻です。

Web ブラウザなどのクライアントから CORS(Cross-Origin Resource Sharing) リクエストが行われる際には、Preflight request によりサーバー側が CORS リクエストを受け付けるかどうかの判断が行われます。

この Preflight request は、通常は本来行いたいリクエストの前に毎回行われるのですが、サーバー側で Access-Control-Max-Age レスポンスヘッダを設定することで、Preflight request の結果をブラウザ側にキャッシュさせることができます。

今回は、Amazon API Gateway Rest API で preflight request のレスポンスヘッダに Access-Control-Max-Age を AWS CDK で設定する方法を確認してみました。

試してみた

Access-Control-Max-Age を設定しない場合

AWS CDK で REST API に CORS Preflight を構成する場合は LambdaRestApi(RestApi のextends)の defaultCorsPreflightOptions を設定します。

lib/constructs/api.ts

import {
  aws_apigateway,
} from "aws-cdk-lib";

// 中略

export class ApiConstruct extends Construct {
  constructor(scope: Construct, id: string, props: ApiConstructProps) {
    super(scope, id);

    // 中略

    new aws_apigateway.LambdaRestApi(this, "RestApi", {
      handler: restApiFunc,
      defaultCorsPreflightOptions: {
        allowOrigins: ["https://example.com"],
        allowMethods: ["POST"],
        allowHeaders: ["authorization", "content-type"],
      },
      deployOptions: {
        stageName: "v1",
      },
      defaultMethodOptions: { authorizer: cognitoUserPoolsAuthorizer },
    });
  }
}

CDK コード全文はこちら

lib/constructs/api.ts

import {
  aws_lambda,
  aws_lambda_nodejs,
  aws_dynamodb,
  aws_apigateway,
  aws_cognito,
} from "aws-cdk-lib";
import { Construct } from "constructs";

interface ApiConstructProps {
  companiesTable: aws_dynamodb.Table;
  cognitoUserPool: aws_cognito.UserPool;
}

export class ApiConstruct extends Construct {
  constructor(scope: Construct, id: string, props: ApiConstructProps) {
    super(scope, id);

    const { companiesTable, cognitoUserPool } = props;

    const restApiFunc = new aws_lambda_nodejs.NodejsFunction(
      this,
      "RestApiFunc",
      {
        architecture: aws_lambda.Architecture.ARM_64,
        entry: "../server/src/lambda/handlers/api-gateway/rest-api-router.ts",
        environment: {
          COMPANIES_TABLE_NAME: companiesTable.tableName,
        },
      },
    );

    companiesTable.grantReadWriteData(restApiFunc);

    const cognitoUserPoolsAuthorizer =
      new aws_apigateway.CognitoUserPoolsAuthorizer(
        this,
        "CognitoUserPoolsAuthorizer",
        {
          cognitoUserPools: [cognitoUserPool],
        },
      );

    new aws_apigateway.LambdaRestApi(this, "RestApi", {
      handler: restApiFunc,
      defaultCorsPreflightOptions: {
        allowOrigins: ["https://example.com"],
        allowMethods: ["POST"],
        allowHeaders: ["authorization", "content-type"],
      },
      deployOptions: {
        stageName: "v1",
      },
      defaultMethodOptions: { authorizer: cognitoUserPoolsAuthorizer },
    });
  }
}

前述の CDK コードをデプロイして構築した API に対して、次のようなテストコードで Preflight request を確認をしみます。CORS 用のレスポンスヘッダーのうち Access-Control-Max-Age のみ 設定されていない ことを確認しています。

companies_api.test.ts

import Axios, { AxiosResponse, AxiosError } from "axios";
import { test, describe, expect, beforeAll } from "vitest";

const REST_API_ENDPOINT = process.env.REST_API_ENDPOINT || "";

describe("/companies", (): void => {
  describe("OPTIONS", (): void => {
    let response: AxiosResponse | undefined = undefined;

    beforeAll(async (): Promise<void> => {
      response = await Axios({
        url: `${REST_API_ENDPOINT}/companies`,
        method: "OPTIONS",
        headers: {
          "Access-Control-Request-Method": "POST",
          "Access-Control-Request-Headers": "authorization",
          Origin: "https://example.com",
        },
      }).catch((err: AxiosError) => err.response);
    });

    test("Preflight Request が成功すること", (): void => {
      expect(response!.status).toBe(204);
      expect(response!.headers).toMatchObject({
        "access-control-allow-headers": "authorization,content-type",
        "access-control-allow-methods": "POST",
        "access-control-allow-origin": "https://example.com",
      });
      expect(response!.headers["access-control-max-age"]).toBeUndefined();
    });
  });
});

テストを実行すると、Access-Control-Max-Age が未設定であることが確認できました。

$ npx vitest run packages/e2e/api/companies_api.test.ts

 RUN  v0.34.3 /Users/wakatsuki.ryuta/projects/cm-cxlabs/cdk-lambda-dynamodb-sample

 ✓ packages/e2e/api/companies_api.test.ts (1) 975ms
   ✓ /companies (1) 974ms
     ✓ OPTIONS (1) 974ms
       ✓ Preflight Request が成功すること

 Test Files  1 passed (1)
      Tests  1 passed (1)
   Start at  02:47:46
   Duration  2.08s (transform 130ms, setup 0ms, collect 177ms, tests 975ms, environment 0ms, prepare 144ms)

Access-Control-Max-Age を設定してみる

Access-Control-Max-Age を設定するには、LambdaRestApi の defaultCorsPreflightOptionsmaxAge を設定します。下記では 5 分(300 秒)で設定しています。

lib/constructs/api.ts

import {
  aws_apigateway,
  Duration,
} from "aws-cdk-lib";

// 中略

export class ApiConstruct extends Construct {
  constructor(scope: Construct, id: string, props: ApiConstructProps) {
    super(scope, id);

    // 中略

    new aws_apigateway.LambdaRestApi(this, "RestApi", {
      handler: restApiFunc,
      defaultCorsPreflightOptions: {
        allowOrigins: ["https://example.com"],
        allowMethods: ["POST"],
        allowHeaders: ["authorization", "content-type"],
        maxAge: Duration.minutes(5),
      },
      deployOptions: {
        stageName: "v1",
      },
      defaultMethodOptions: { authorizer: cognitoUserPoolsAuthorizer },
    });
  }
}

CDK コード全文はこちら

lib/constructs/api.ts

import {
  aws_lambda,
  aws_lambda_nodejs,
  aws_dynamodb,
  aws_apigateway,
  aws_cognito,
  Duration,
} from "aws-cdk-lib";
import { Construct } from "constructs";

interface ApiConstructProps {
  companiesTable: aws_dynamodb.Table;
  cognitoUserPool: aws_cognito.UserPool;
}

export class ApiConstruct extends Construct {
  constructor(scope: Construct, id: string, props: ApiConstructProps) {
    super(scope, id);

    const { companiesTable, cognitoUserPool } = props;

    const restApiFunc = new aws_lambda_nodejs.NodejsFunction(
      this,
      "RestApiFunc",
      {
        architecture: aws_lambda.Architecture.ARM_64,
        entry: "../server/src/lambda/handlers/api-gateway/rest-api-router.ts",
        environment: {
          COMPANIES_TABLE_NAME: companiesTable.tableName,
        },
      },
    );

    companiesTable.grantReadWriteData(restApiFunc);

    const cognitoUserPoolsAuthorizer =
      new aws_apigateway.CognitoUserPoolsAuthorizer(
        this,
        "CognitoUserPoolsAuthorizer",
        {
          cognitoUserPools: [cognitoUserPool],
        },
      );

    new aws_apigateway.LambdaRestApi(this, "RestApi", {
      handler: restApiFunc,
      defaultCorsPreflightOptions: {
        allowOrigins: ["https://example.com"],
        allowMethods: ["POST"],
        allowHeaders: ["authorization", "content-type"],
        maxAge: Duration.seconds(300),
      },
      deployOptions: {
        stageName: "v1",
      },
      defaultMethodOptions: { authorizer: cognitoUserPoolsAuthorizer },
    });
  }
}

前述の CDK コードをデプロイしたら、次のようなテストコードで Preflight request を確認をしみます。今度はレスポンスヘッダーに Access-Control-Max-Age が 設定されている ことを確認しています。

companies_api.test.ts

import Axios, { AxiosResponse, AxiosError } from "axios";
import { test, describe, expect, beforeAll } from "vitest";

const REST_API_ENDPOINT = process.env.REST_API_ENDPOINT || "";

describe("/companies", (): void => {
  describe("OPTIONS", (): void => {
    let response: AxiosResponse | undefined = undefined;

    beforeAll(async (): Promise<void> => {
      response = await Axios({
        url: `${REST_API_ENDPOINT}/companies`,
        method: "OPTIONS",
        headers: {
          "Access-Control-Request-Method": "POST",
          "Access-Control-Request-Headers": "authorization",
          Origin: "https://example.com",
        },
      }).catch((err: AxiosError) => err.response);
    });

    test("Preflight Request が成功すること", (): void => {
      expect(response!.status).toBe(204);
      expect(response!.headers).toMatchObject({
        "access-control-allow-headers": "authorization,content-type",
        "access-control-allow-methods": "POST",
        "access-control-allow-origin": "https://example.com",
        "access-control-max-age": "300",
      });
    });
  });
});

テストを実行すると、Access-Control-Max-Age が設定されていることが確認できました。

$ npx vitest run packages/e2e/api/companies_api.test.ts

 RUN  v0.34.3 /Users/wakatsuki.ryuta/projects/cm-cxlabs/cdk-lambda-dynamodb-sample

 ✓ packages/e2e/api/companies_api.test.ts (1)
   ✓ /companies (1)
     ✓ OPTIONS (1)
       ✓ Preflight Request が成功すること

 Test Files  1 passed (1)
      Tests  1 passed (1)
   Start at  02:30:30
   Duration  525ms (transform 74ms, setup 0ms, collect 93ms, tests 184ms, environment 0ms, prepare 68ms)

おわりに

今回は、AWS CDK で REST API に CORS Preflight を構成する際に、Access-Control-Max-Age を設定する方法を確認しました。

このレスポンスヘッダーが無くても CORS は動作するので忘れがちですが、パフォーマンス向上のためのチューニングの一環として是非設定しておきましょう。

以上