<AWS SAM> 複数originからのCORSを許可する

## はじめに
業務でAWS SAM（lambdaプロキシ統合）にて作成されたAPIに対し、複数originからのアクセスを許可するという作業があったためメモとして残します。
また今回の状況は開発環境と本番環境、両方からアクセスしたい場合などにも役立つ内容かと思います。

## 状況再現
AWSの[公式チュートリアル](https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-getting-started-hello-world.html)よりSAMの簡単なAPIを作成します。

諸々作業を終え、生成されたURLにcurlすると以下のような結果になると思います。
```bash
$ curl https://xxxxxxx.execute-api.us-west-2.amazonaws.com/Prod/hello/

{"message": "hello world"}
```

ではブラウザからアクセスしてみます。
今回はlocalhost(`http://localhost:3000`)とs3,cloudfrontを用いてホスティングしているCDNのVueの環境(cloudfrontより生成されたURL)からアクセスしてみます。


「Access to XMLHttpRequest at 'https://xxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.」

上記のようなエラーがアクセスしたブラウザすべてで発生するかと思います。
> No 'Access-Control-Allow-Origin' header is present on the requested resource.


上記のエラーはバックエンド側(今回の場合APIGatewayまたはLambda)でレスポンスヘッダーにて`Access-Control-Allow-Origin`を設定しアクセスできるオリジン(URL)を指定しなさいと注意されているエラーになります。

## 実装
Lambdaのコードとtemplate.yamlのコードを改修していきます。

```template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  sam-app

  Sample SAM Template for sam-app

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 3
    MemorySize: 128

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.9
      Environment:  # 環境変数を定義
        Variables:
          ALLOWED_ORIGINS: 'http://localhost:3000,https://xxxxxxxx.cloudfront.net'
      Architectures:
        - x86_64
      Events:
        HelloWorld:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            Path: /hello
            Method: get

Outputs:
  # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
  # Find out more about other implicit resources you can reference within SAM
  # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
  HelloWorldApi:
    Description: "API Gateway endpoint URL for Prod stage for Hello World function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
  HelloWorldFunction:
    Description: "Hello World Lambda Function ARN"
    Value: !GetAtt HelloWorldFunction.Arn
  HelloWorldFunctionIamRole:
    Description: "Implicit IAM Role created for Hello World function"
    Value: !GetAtt HelloWorldFunctionRole.Arn

```

チュートリアルのコードから修正した部分は環境変数を設定した点です。(上記コードから抜粋)
許可したいURLを下記のように定義します。
```yaml
Environment:  # 環境変数を定義
        Variables:
          ALLOWED_ORIGINS: 'http://localhost:3000,https://xxxxxxxx.cloudfront.net'
```

続いてLambdaのコードです。

```app.py
import json
import os

def get_cors_headers(origin):
    return {
        'Access-Control-Allow-Origin': origin,
        'Access-Control-Allow-Headers': 'Content-Type',
        'Access-Control-Allow-Methods': 'OPTIONS,POST,GET'
    }

def lambda_handler(event, context):

    origin = event["headers"].get("origin")
    allowed_origins = os.environ.get("ALLOWED_ORIGINS", "").split(',')

    if origin in allowed_origins:
        cors_headers = get_cors_headers(origin)

        return {
            "statusCode": 200,
            "body": json.dumps({
                "item": "hello lambda!",
            }),
            "headers": cors_headers
        }
    else:
        return {
            "statusCode": 403,
            "body": json.dumps({
                "message": "Origin not allowed.",
            }),
        }
```

ヘッダー情報を`def get_cors_headers`にて設定します。(引数にはリクエストしてきたオリジンのURLが入ります。)

下記コードでは`event`からoriginのURL情報の抽出と先ほどtemplate.yamlにて設定した環境変数のURLを配列に変換します。

```python
origin = event["headers"].get("origin")
allowed_origins = os.environ.get("ALLOWED_ORIGINS", "").split(',')
```

続いて条件分岐について環境変数にて設定した許可するオリジンとリクエストしてきたオリジン情報が一致しているかどうか確かめます。一致している場合は`get_cors_headers`を呼び出し、200と本来レスポンスする情報を返します。一致しない場合は403と`message`を返します。

```python
if origin in allowed_origins:
        cors_headers = get_cors_headers(origin)

        return {
            "statusCode": 200,
            "body": json.dumps({
                "item": "hello World",
            }),
            "headers": cors_headers
        }
    else:
        return {
            "statusCode": 403,
            "body": json.dumps({
                "message": "Origin not allowed.",
            }),
        }
```

### 各オリジンからアクセスする

無事レスポンスが返ってきました。
![スクリーンショット 2023-10-01 16.23.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1620925/76158246-f962-d4a0-6db9-46b89319aac6.png)

無事`Access-Control-Allow-Origin`も設定できています。
![スクリーンショット 2023-10-01 16.25.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1620925/f0e8bfdc-3178-031c-7485-981d4c08af5d.png)

![スクリーンショット 2023-10-01 16.26.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1620925/339c6a64-157f-6b79-4f1b-ee85924db748.png)

## 終わりに
思ったよりもサクッと実装できました。
CORSについて触れるいい機会になったかなと思います。この記事がどなたかにお役に立てば幸いです。

## 参考
https://www.tdi.co.jp/miso/aws-whitelist-api

https://dev.classmethod.jp/articles/apigw-dynamic-response-header-with-vtl/