Lambda無しでもいけます!!許可されたサイトにだけCORSを許可するためにAPI Gatewayのモックレスポンスを動的に設定してみた

マッピングテンプレートを使ってローカルマシン、CloudFront、S3だけCORSを許可してみました
2020.10.01

CX事業本部@大阪の岩田です。現在開発中の案件でAPI GatewayからのレスポンスヘッダAccess-Control-Allow-Headersを動的に設定したいという要件があり、API Gatewayのマッピングテンプレートを使って要件を実現しました。マッピングテンプレートを記述するためのVTLに関して情報があまり見つからなかったので、対応した内容についてブログにまとめてみました。

やりたかったこと

現在開発中の案件ではSPAからAPI Gateway × Lambdaで構築したREST APIを呼び出すというよくある構成で開発を進めています。開発中のSPAは

  • 開発者のローカルマシン
  • AWS上の開発環境(S3やCloudFront)

といった複数の環境で稼働するため、開発環境に関してはAPI GatewayAccess-Control-Allow-Origin: *を返却することでクロスドメインのアクセスを許可していたのですが(本番環境はちゃんと絞ります...)、特殊な要件から、あるAPIに関してはSet-Cookieを返却してブラウザにCookieを保存する必要が出てきました。JavaScriptからのAPI呼び出しでCookieをセットするには

  • SPAからAPIを呼び出す際に適切なオプションを付与する ※今回はReact×Amplifyの構成だったのでAPI呼び出し時に{withCredentials:true}を付与しました
  • サーバーサイドから返却するレスポンスヘッダを以下のように変更する
    • Set-CookieSameSite属性を適切に設定する ※Chrome80におけるデフォルト動作の変更対策
    • Access-Control-Allow-Credentials: Trueを返却する
    • Access-Control-Allow-Originに*以外を返却する

といった対応が必要になります。そうなんですAccess-Control-Allow-Origin: *は使えないんです。かといって決め打ちでhttp://localhost:3000のような指定もしたくありません。開発者ごとに環境は違うので、別PJとの兼ね合いでポート番号を変更したいケースも考えられますし、ループバックアドレスを使いたいかもしれません。AWS環境にデプロイしてテストする際にも困ります。

ということで...特定のオリジンに対してだけCORSを許可するよう動的にAccess-Control-Allow-Originを返却する必要が出てきました。オリジンがhttp://localhost:3000の場合はAccess-Control-Allow-Origin: http://localhost:3000を返却し、オリジンがhttp://example.comの場合はAccess-Control-Allow-Originを返却しない。といった具合です。

このような処理を実現するためにはアプリ側で許可されたオリジンの一覧を保持しておき、リクエストヘッダのoriginと突き合わせて動的にレスポンスを生成する手法が知られています。

API Gatewayのバックエンドで起動するLambdaにはこのような処理を組み込む想定ではいたのですが、API Gatewayの構成は以下のようになっていました。

/api
  |---OPTIONS...統合タイプ:モック
  |---GET...統合タイプ:Lambda関数

OPTIONSメソッドについては統合タイプがモックになっておりLambdaでロジックを書けないのです。OPTIONSメソッドの統合タイプをLambda関数に変更して、Access-Control-Allow-Originを動的に生成するだけのLambdaを用意することも考えたのですが、レスポンスヘッダ1個のためにLambdaを用意するのもちょっと大げさな気がします。できればLambda無しでOPTIONSメソッドのレスポンスを動的に生成したいところです。

そこでマッピングテンプレート!!

ここで登場するのがAPI Gatewayのマッピングテンプレートです。マッピングテンプレートを利用することでAPIのリクエスト/レスポンスパラメータ及びステータスコードをオーバーライドすることが可能です。さっそく設定してみましょう。まずはマネコンから「CORSの有効化」を選択し、OPTIONSメソッドの設定を用意します。

デフォルト設定のままCORSを有効化すると、以下のようにメソッドレスポンスが設定されます。レスポンスヘッダにAccess-Control-Allow-Originが存在することを確認しておきましょう。※後で削除します。

次に統合レスポンスの設定です。ここの設定でレスポンスヘッダAccess-Control-Allow-Originに実際にマッピングする値を定義します。マッピングテンプレートの定義に以下のように記述します。

#set($origin = $input.params().header.get('origin'))

#if ($origin.matches("^http(s?)://(localhost|127\.0\.0\.1|.*\.cloudfront\.net|s3-\.*-\d?\.amazonaws\.com)(:?\d+)?"))
    #set($context.responseOverride.header.Access-Control-Allow-Origin = $origin)
#end

やっていることはすごくシンプルで、以下の通りです。

  • まず#set($origin = $input.params().header.get('origin'))の部分でリクエストヘッダのoriginを取得して$originという変数にセットします。
  • 続いて#if ($origin.matches(...の部分で$originにセットされた文字列が許可されたオリジンかどうかを判定しています。とりあえずlocalhostと127.0.0.1、CloudFront全般とS3全般を許可しています。もう少し厳密にやるならCloudFrontやS3のドメインを絞ると良いでしょう。
  • if文の評価にマッチした場合はリクエストヘッダの$context.responseOverride.header.Access-Control-Allow-Originに変数$originをセットします。$context.responseOverride.header.<ヘッダ名>という変数をセットすることでレスポンスヘッダ内の指定したヘッダの値を上書くことが可能です。

マッピングテンプレートを使用して、API のリクエストおよびレスポンスパラメータとステータスコードをオーバーライドする

マネコン上の表示はこんな感じになります。

最後にメソッドレスポンスからAccess-Control-Allow-Originを消しておきましょう。これをやっておかないと、許可されたオリジン以外からのアクセスがあった場合(マッピングテンプレートのIF文にマッチしなかった場合)にAccess-Control-Allow-Origin: *が返却されてしまいます。最初から消しておけば良いのですが、そうするとマネコンからマッピングテンプレートを保存する際にInvalid mapping expression specified: Validation Result: warnings : [], errors : [Invalid mapping expression parameter specified: method.response.header.Access-Control-Allow-Origin]というエラーが出てしまうので、マッピングテンプレートの保存後に削除しています。

やってみる

ここまでで準備ができたので、動作を確認してみます。

まずオリジンを指定しない場合...

$curl -XOPTIONS https://<API GWのID>.execute-api.ap-northeast-1.amazonaws.com/dev -i
HTTP/2 200
date: Thu, 01 Oct 2020 08:54:15 GMT
content-type: application/json
content-length: 1
x-amzn-requestid: 08927537-4f43-40ac-a952-28ab101627cf
access-control-allow-headers: Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token
x-amz-apigw-id: TuTgnHdqNjMFa1g=
access-control-allow-methods: OPTIONS

Access-Control-Allow-Originは返ってきません。

続いて許可されたオリジンを指定した場合...

$curl -XOPTIONS https://<API GWのID>.execute-api.ap-northeast-1.amazonaws.com/dev -i -H "origin: http://localhost:3000"
HTTP/2 200
date: Thu, 01 Oct 2020 08:55:23 GMT
content-type: application/json
content-length: 1
x-amzn-requestid: e8aa81dd-37c4-43ca-acfa-c8f103f0c782
access-control-allow-origin: http://localhost:3000
access-control-allow-headers: Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token
x-amz-apigw-id: TuTrXFSntjMFpgA=
access-control-allow-methods: OPTIONS

今度はAccess-Control-Allow-Origin: http://localhost:3000が返却されてきました。

最後に許可されていないオリジンhttps://example.comを指定した場合です

$ curl -XOPTIONS https://<API GWのID>.execute-api.ap-northeast-1.amazonaws.com/dev -i -H "origin: https://example.com"
HTTP/2 200
date: Thu, 01 Oct 2020 08:57:13 GMT
content-type: application/json
content-length: 1
x-amzn-requestid: dda0e4a0-4583-466a-9625-51759811b36f
access-control-allow-headers: Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token
x-amz-apigw-id: TuT8bHHAtjMFVlQ=
access-control-allow-methods: OPTIONS

こちらもAccess-Control-Allow-Originは返ってきません。意図通り動作していそうです。

まとめ

API Gatewayのマッピングテンプレートを使って動的にレスポンスヘッダーを調整する例をご紹介しました。マッピングテンプレートで利用するVelocity Template Language (VTL)に関してネット上にもあまり情報が転がっておらず、今回利用した正規表現でのチェックmatchesも「多分できるけんだろうけど書き方が分からない...」という状態になってしまったので、同じような境遇の人のためになればと思いブログ化してみました。VTLはAppSyncでも利用する記法なので、この機会にちゃんと勉強してみようと思います。

参考