AWSサーバレスアプリのCORSエラーに苦しんだ件
背景
AWSでサーバレスWebアプリケーションを構築した際にCORSエラーに苦しんだため対応内容を備忘として記載しました。同じ事象で躓いた方の助けになれば幸いです。
構成
使用サービス
- Cloud Front - API Gateway - Lambda - DynamoDB
概要
- アプリの画面からCloudFrontを経由して異なるオリジンのAPIにリクエストを行った際にCORSエラーが発生。
- APIはPublicであるが、Cognitoユーザープールにアカウントを持つユーザーのみリクエストを許可するため、リクエストヘッダにはIDトークン(JWT)の設定を行っている。
- オンプレミス上のWebサイトからS3に配置されているJsを読み込み、Js上の関数からAPIをコールする。
- PC用、SP(Smart Phone)用のサイトが別ドメインで存在する。
対応
- APIへの通信は資格情報を含む単純でないCORSリクエストであることがポイント。
- プリフライトリクエストが成功するよう、CORSポリシーに準拠するようなレスポンスヘッダの設定を行うことで解決した。
API Gatewayの設定
- PublicなAPIのためCognito Authorizerでリクエスト元のユーザーをJWT認証を行い、認証に成功した場合のみバックエンドのLambdaで処理を行う。(単純でないリクエスト)
- 単純でないリクエストに対してプリフライトリクエスト(OPTIONSメソッド)が成功するように以下設定を行う。
メソッド構成
OPTIONS、POST
OPTIONSメソッドの設定
- API GatewayのLambdaプロキシ統合を設定したうえで以下設定を行う。
- Cognito Athorizerは追加しない。
メソッドレスポンス
Access-Control-Allow-Credentials
Access-Control-Allow-Headers
Access-Control-Allow-Methods
Access-Control-Allow-Origin
統合レスポンス
Access-Control-Allow-Credentials: 'true' // 資格情報付(JWT)のリクエストのため必要
Access-Control-Allow-Headers: 'Content-Type, Authorization' // AuthorizationヘッダにJWTを設定しているため必要
Access-Control-Allow-Methods: 'OPTIONS'
Access-Control-Allow-Origin: 'https:/example.com' // 資格情報付リクエストの場合は"*"は設定できないため仮の値を一つ設定
- Content-Type: application/json
POSTメソッドの設定
- API GatewayのLambdaプロキシ統合を設定したうえで以下設定を行う。
- Cognito Athorizerの追加。
Lambdaの設定
- レスポンスは必要最低限のステータスコードとボディのみを設定する。
- 他のレスポンスヘッダは統合レスポンスとCloudFront Functionsが追加してくれる。
def lambda_handler(event, context) {
// 処理
return {
"statusCode": 200,
"body": json.dump("success")
}
}
Cloud Frontの設定
- リクエストのOriginにはPCサイトとSPサイトがあり、統合レスポンスの設定で一つに指定できないためFunctionsで動的にOriginの値を更新する。
Behavior
- Path pattern: XXX/v1/*
- Origin: APIGateway
- Viewer protocol: HTTPS only
- Allowed HTTP methods: GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE
- Cache HTTP methods: なし
- Restrict viewer access: いいえ
- Cache policy and origin request policy: はい
- Cache policy: Managed-CachingDisable
- Origin request policy: なし
- Response header policy: なし
- CloudFront Functions: rewrite-response-header
CloudFront Functions
function handler(event) {
var request = event.request;
var response = event.response;
// Access-Control-Allow-Origin
// レスポンスヘッダにOriginヘッダがあるかどうかに関わらずリクエストヘッダのOriginの値で追加または上書きする
if (request.headers['origin']) {
response.headers['access-control-allow-origin'] = {value: request.headers['origin'].value};
console.log("Access-Control-Allow-Origin adding or overwriting now.");
}
return response;
}
補足
- 既定ではブラウザ側から資格情報が送信されない
- クライアントからのリクエストでフラグ設定を適切に行う必要がある
- プリフライトリクエストの場合、サーバ側でレスポンスのヘッダ設定を適切に行う必要がある
- 設定が行われていない場合、ブラウザは対象リソースへのアクセスが拒否される
MDN: 資格情報を含むリクエスト
既定では、オリジン間の XMLHttpRequest または Fetch の呼び出しにおいて、ブラウザーは資格情報を送信しません。 XMLHttpRequest オブジェクトまたは Request のコンストラクターの呼び出し時に、特定のフラグを設定する必要があります。
MDN: プリフライトリクエストと資格情報
CORS のプリフライトリクエストに資格情報を含めてはいけません。プリフライトリスクエストへのレスポンスには Access-Control-Allow-Credentials: true を指定して、実際のリクエストを資格情報付きで行うことができることを示す必要があります。
MDN: 資格情報付きリクエストとワイルドカード
資格情報付きリクエストに返答する場合、
サーバーは Access-Control-Allow-Origin ヘッダーで "" ワイルドカードを指定してはならず、 Access-Control-Allow-Origin: https://example.com のように、明示的にオリジンを指定しなければなりません。
サーバーは Access-Control-Allow-Headers ヘッダーで "" ワイルドカードを指定してはならず、 Access-Control-Allow-Headers: X-PINGOTHER, Content-Type のように、明示的にヘッダー名を指定しなければなりません。
サーバーは Access-Control-Allow-Methods ヘッダーで "*" ワイルドカードを指定してはならず、 Access-Control-Allow-Methods: POST, GET のように、明示的にメソッド名を指定しなければなりません。
リクエストが資格情報 (多くの場合は Cookie ヘッダー) を含んでおり、そのレスポンスが Access-Control-Allow-Origin: * ヘッダー (つまりワイルドカード) を含んでいる場合、ブラウザーはレスポンスへのアクセスをブロックし、開発ツールのコンソールに CORS エラーを報告します。
ただし、リクエストが (Cookie ヘッダーのような) 資格情報を含んで行われ、そのレスポンスがワイルドカードではない実際のオリジンを含んでいる場合 (例えば Access-Control-Allow-Origin: https://example.com など)、ブラウザーは指定されたオリジンからのレスポンスへのアクセスを許可します。
他の対応案
API Gatewayのカスタムドメインを利用して、Js側と同一ドメインにすることでそもそもクロスドメインアクセス自体を回避する方法があるようです。
こちらは試していないですが、適用可能か調べてみたいと思います。
まとめ
CORSエラーには結構悩まされましたが、おかげでCORSポリシーについて仕組みをちゃんと理解するきっかけになりました。
対応方法はいくつか考えられそうですが、他の対応方法をご存じの方がいらっしゃればシェアしていただけると幸いです。
CORSエラーに苦しむ人が生まれない平和な世界になることを祈っています。合掌🙏
参考
Discussion