Lambda関数でOPTIONメソッドを作り「CORSの複数origin対応」を攻略する③(実装説明)

# この問題にぶつかるまでの経緯

- 単純でないリクエストでPreflightリクエストが送られるが、そこで特定の複数originからのリクエストのみパスさせたい
    - 本番環境では特に `Access-Control-Allow-Origin: "*"` はだめなので...
    - しかし、`Access-Control-Allow-Origin: "https:hoge.com,https:huga.com"` のような形式で複数のorigin を指定することはApiGatewayの仕様上できないらしい
        - ではどうするか？次のように考えた
            - PreflightリクエストはOPTIONS
            - OPTIONSメソッドを自分で作ったLambda関数に統合して、定義したホワイトリストに合致するoriginからのリクエストだった場合パスさせるようにする

# 記事の構成
-  [[AWS SAM] Lambda関数でOPTIONメソッドを作り「CORSの複数origin対応」を攻略する①(概要説明)](https://qiita.com/sawa-akabee/items/1f3ab7886ffe0688a4e3) 
    - 全体的な説明、CORSについてなど
- [[AWS SAM] Lambda関数でOPTIONメソッドを作り「CORSの複数origin対応」を攻略する②(シンプルな代案の説明)](https://qiita.com/sawa-akabee/items/a6cda41d9a937893b2a6) 
    - originが「*」や特定の1originで良い場合の説明
- [AWS SAM] Lambda関数でOPTIONメソッドを作り「CORSの複数origin対応」を攻略する③(実装説明)
    - 本記事

# 対応方法まとめ

- 複数originのCORS許可対応をさせたいメソッドについて
    - (下記手順内の例ではExampleFunctionというGETメソッドを指す)
    - **レスポンスヘッダーの `Access-Control-Allow-Origin` は「*」とし全てのoriginをパスさせる**
- **ExampleFunctionと同一エンドポイントのOPTIONSメソッドを定義**
    - **レスポンスヘッダーとして、リクエスト元のフロント側originを `Access-Control-Allow-Origin` にセットする処理とする**

# 環境

- macOS: Big Sur v11.6.1(Intel)
- バックエンド(Lambda関数を呼び出すAPIをSAMで構築する)
    - SAM CLI: 1.42.0
        - Runtime: node.js 14.x (Typescript)
- フロントエンド
    - Ionic: @ionic/angular 5.8.0

# 手順

## 普通のGET, POSTメソッドなどを作るのと同様にOPTIONSメソッドを作る

### ローカルからLambdaに渡す環境変数としてoriginのホワイトリストを定義

- ここで定義されたものが許可するoriginとなる
- samconfig.tomlはGit管理されていないものとしている

```samconfig.toml
[default.deploy.parameters]
parameter_overrides = "ORIGINWHITELIST=\"ionic://localhost,http://localhost:8100\""
```

### OPTIONSを作成

```template.yml
   # samconfig.tomlで設定したパラメータを環境変数として設定する
   Parameters:
++   ORIGINWHITELIST:
++     Type: String
   
   Globals:
     Function:
       Environment:
++       Variables:
++         ORIGINWHITELIST: !Ref ORIGINWHITELIST

   # OPTIONSを定義する
   Resources:
      ExampleFunction:
        Type: AWS::Serverless::Function
        Properties:
          Handler: dist/handlers/example.exampleHandler # TypescriptをJavascriptにコンパイルしているのが/dist配下。Typescriptを直接実行はできないのでそこを参照させる。tsconfig.json: compilerOptions.outDirを参照。
          Events:
            Api:
              Type: Api
              Properties:
                Path: /
                Method: GET
                RestApiId: !Ref ApiGateway
++    OptionFunction:
++      Type: AWS::Serverless::Function
++      Properties:
++        Handler: dist/handlers/preflight.preflightHandler # TypescriptをJavascriptにコンパイルしているのが/dist配下。Typescriptを直接実行はできないのでそこを参照させる。tsconfig.json: compilerOptions.outDirを参照。
++        Events:
++          Api:
++            Type: Api
++            Properties:
++              Path: /
++              Method: OPTIONS
++              RestApiId: !Ref ApiGateway
```

- レスポンスヘッダーとして、リクエスト元のフロント側originを `Access-Control-Allow-Origin` にセットする処理とする

```src/handlers/preflight.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import 'source-map-support/register';

/**
 * OPTION(preflight)用
 */
export const preflightHandler = async (
  event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
  console.debug('event', event);
  console.debug('process', process);

  const originWhiteList = process.env.ORIGINWHITELIST;
  const origin = event.headers['origin'];

  const responseHeaders = {
    'Access-Control-Allow-Origin': '',
    'Access-Control-Allow-Credentials': true,
    'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept, Authorization',
  };

  if (!originWhiteList) {
    // Origin環境変数の取得失敗の場合
    console.error('Cannot get env[ORIGINWHITELIST]');
    console.error('event.headers: ', event.headers);
    return {
      headers: responseHeaders,
      statusCode: 403,
      body: JSON.stringify({
        message: 'Missing white list of origin!',
      }),
    };
  }

  if (!origin) {
    // リクエストヘッダーにoriginが含まれていない場合
    console.error('headers.origin has not passed.');
    console.error('event.headers: ', event.headers);
    return {
      headers: responseHeaders,
      statusCode: 403,
      body: JSON.stringify({
        message: 'Missing passed origin!',
      }),
    };
  }

  if (!originWhiteList.split(',').includes(origin)) {
    // ホワイトリストに渡されたoriginが含まれていない場合
    console.error('Origin whitelist not include passed origin.');
    console.error('originWhiteList.split(', '): ', originWhiteList.split(','));
    console.error('origin: ', origin);
    return {
      headers: responseHeaders,
      statusCode: 403,
      body: JSON.stringify({
        message: 'Not allowed origin!',
      }),
    };
  }

  // 全チェックをパスしたので、ここでフロント側originをAccess-Control-Allow-Originにセットしている
  // これでAccess-Control-Allow-Originには1つのoriginしか記載できないという制限をクリアしつつ、複数origin(Lambda環境変数にて指定したoriginのホワイトリストに載っているorigin)に対応できた
  responseHeaders['Access-Control-Allow-Origin'] = origin; 
  console.debug('Origin check is passed!');
  console.debug('responseHeaders:', responseHeaders);
  return {
    headers: responseHeaders,
    statusCode: 200,
    body: JSON.stringify({
      message: 'OK',
    }),
  };
};
```

### CORS対応させたいメソッドの設定

- レスポンスヘッダーの `Access-Control-Allow-Origin` は「*」とし全てのoriginをパスさせる

```example.ts
    import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
    import 'source-map-support/register';
    
    /**
     * A simple example includes a HTTP get method.
     */
    export const exampleHandler = async (
      event: APIGatewayProxyEvent
    ): Promise<APIGatewayProxyResult> => {
      // All log statements are written to CloudWatch
      console.debug('Received event:', event);
    
      return {
        statusCode: 200,
++    headers: {
++      'Access-Control-Allow-Origin': '*',
++      'Access-Control-Allow-Methods': 'GET,POST,OPTIONS',
++      'Access-Control-Allow-Headers': 'Content-Type, Authorization, Accept, timeout',
++    },
        body: JSON.stringify({
          message: 'Hello world!',
        }),
      };
    };
```

### デプロイ

![Untitled.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490917/bfe3fee8-77d7-1475-8bca-b46478b962dd.png)


- 無事、Lambda関数に統合された

## iOSアプリから呼び出してみる

- 今回はホワイトリストとして下記をsamconfig.tomlで設定しています
    - http://localhost:8100 (エミュレータ用)
    - ionic://localhost (実機用)

### エミュレータから

- NGパターン(ポート番号が不一致)
    - Preflightでエラーとなった！

![Untitled 1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490917/935b8542-371b-b241-7f7f-5722811c0509.png)


![Untitled 2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490917/39334a7e-fc4f-8e2e-1f11-8a2e88def781.png)


- OKパターン
    - 無事APIが通った！
    
![Untitled 3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490917/aae6fa17-fa75-92cb-e636-84d862f4cb5b.png)

    

![Untitled 4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490917/8832765d-77b4-2c2d-35ad-2f1f52b30613.png)


### 実機から

- 実機はhost nameを変更しなければ `ionic://localhost` でoriginは固定
    - 無事APIが通った！

![Untitled 5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490917/f73dad43-34e3-40c2-cfd7-b17e524b3625.png)


# 結果まとめ

- 下記の通り、想定通りの挙動を実現できた
    - ホワイトリストに登録された2つのoriginがPreflightをパス
    - 未登録のoriginが弾かれた

# (参考)エラーの場合の原因調査方法

- CloudWatch
    - Preflightが走っていればログが出力されているはず
        - ログが出力されてないならPreflightが走ってないかも？
        - 単純リクエストになってたりする可能性あり

- Lambda関数のテスト

![Untitled 6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490917/ef7d9c62-7237-3940-12fd-ebabdad62e67.png)

- 開発者ツールのネットワークタブ