Open13

CORS - Cloud Front + API Gateway + Lambda 周りの設定

ピン留めされたアイテム
mocknmockn

対応案1

色々試したが、結果以下対応で解決した。他にもっと良い方法があればぜひ知りたい。

背景

  • あるアプリの画面からCloudFrontを経由して異なるオリジンのAPIにリクエストを行った際にCORSエラーが発生し、対応に苦しんだ。
  • APIはPublicであるが、Cognitoユーザープールにアカウントを持つユーザーのみリクエストを許可するため、リクエストヘッダにはIDトークン(JWT)の設定を行っている。

構成

  • Cloud Front - API Gateway - Lambda - DynamoDB
  • オンプレミス上の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

rewrite-response-header
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 など)、ブラウザーは指定されたオリジンからのレスポンスへのアクセスを許可します。

他の対応案

https://dev.classmethod.jp/articles/spa-cloudfront-and-api-gateway-voiding-cors/

  • API Gatewayのカスタムドメインを利用して、Js側と同一ドメインにすることでそもそもクロスドメインアクセス自体を回避する。

参考

https://developer.mozilla.org/ja/docs/Web/HTTP/CORS

https://qiita.com/yana1316/items/34ee351adb6091ea7fc1

https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/how-to-cors.html#:~:text=があります。-,プロキシ統合の CORS サポートを有効にする,-Lambda プロキシ

https://github.com/aws-samples/amazon-cloudfront-functions/blob/main/add-cors-header/index.js

https://zenn.dev/syo_yamamoto/articles/445ce152f05b02

mocknmockn

ゴール

  • Cloud Frontから配信される静的コンテンツ(画面)からJWTによる認可を必要とするAPIとの通信を行えること

背景・前提

  • 外部から叩けるAPI(Lambda)に認可処理を実装しセキュリティを強化
    • Cognito AuthorizerでAPIリクエストのAuthorizationヘッダのJWTを検証(認可)
    • Cognito認証済みユーザーからのリクエストを想定
  • クライアントのJavascript内でXMLHttpRequestを利用してAPIにリクエスト

環境

技術

  • Cloud Front
  • API Gateway
  • Lambda
    • Python 3.10
  • Cognito

問題

  • APIの単体テスト(Talend API Tester)ではAPI Gateway + Lambdaの処理は正常に動作
    • HTTP 200、想定通りのレスポンス、ログ出力OK
  • 画面との結合テスト(画面操作によってAPI呼び出し)ではHTTP 401 (Unauthorized)エラーになる
    • 画面資源はCloud Frontから配信された静的ファイル
    • 画面のDevツールのConsoleからfetch関数を利用してリクエスト
mocknmockn

今回のアプリの要件は以下の通り

  • シンプルでないリクエスト(プリフライトリクエスト)
  • 資格情報付のCORS要求
  • API GatewayとLambdaのプロキシ統合を行う

https://developer.mozilla.org/ja/docs/Web/HTTP/CORS

  • シンプルなリクエストとシンプルでないリクエストがある
  • シンプルでないリクエストは実行したいリクエストに先立ってプリフライトリクエストが飛ぶ
  • CORS要求に資格情報を含める場合は別途設定が必要
mocknmockn
  • 既定ではブラウザ側から資格情報が送信されない
    • クライアントからのリクエストでフラグ設定を適切に行う必要がある
  • プリフライトリクエストの場合、サーバ側でレスポンスのヘッダ設定を適切に行う必要がある
    • 設定が行われていない場合、ブラウザは対象リソースへのアクセスが拒否される

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 など)、ブラウザーは指定されたオリジンからのレスポンスへのアクセスを許可します。

参考

https://qiita.com/yana1316/items/34ee351adb6091ea7fc1

mocknmockn

検証

以下設定の見直しを行う

  1. サーバ(APIGateway, Lambda)のレスポンス設定
  2. ブラウザ(JavascriptのXMLHttpRequest)からのリクエスト設定
mocknmockn

1. サーバ(APIGateway, Lambda)のレスポンス設定

API Gateway

前提

  • API Gatewayはプロキシ統合を行っているため、OPTIONSが存在する
  • API Gatewayに到達したリクエストは、メソッドリクエストのCognito Authorizerによって認可処理が行われる
  • 認可成功した場合、指定のヘッダ情報を含むレスポンスを返却する
    • コンソール上でAPI Gatewayの統合レスポンスとメソッドレスポンスにヘッダの設定を行う

ヘッダ設定方針

HTTP レスポンスヘッダー

  • レスポンスヘッダに以下を設定する
    • Access-Control-Allow-Origin
      • リソースへのアクセス許可するオリジン(リクエスト元)
    • Access-Control-Allow-Credentials
      • 資格情報を送信する場合は' true
    • Access-Control-Allow-Methods
      • 実際のリクエストメソッドを指定
      • 基本的に POST, GET, OPTIONS を指定しておけばよさそう(必要に応じて追加)
    • Access-Control-Allow-Headers
      • 実際のリクエストを行う際に使用される HTTP ヘッダー
      • リクエストのAccess-Control-Request-Headers ヘッダーと一致するはず

シンプルではないリクエストの CORS を有効にする

メソッドレスポンス

OPTIONSのメソッドレスポンス(HTTP 200)に以下のレスポンスヘッダーを追加

  • Access-Control-Allow-Headers
  • Access-Control-Allow-Methods
  • Access-Control-Allow-Origin
  • Access-Control-Allow-Credentials

統合レスポンス

OPTIONSの統合レスポンスに以下のヘッダー値を使用します。

  • Access-Control-Allow-Headers: 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'
    • 値は例。最低限'Content-Type', 'Content-Type'は必要か
    • Access-Control-Allow-Methods: 'POST, GET, OPTIONS,'
    • 実際のリクエストのメソッドを指定
  • Access-Control-Allow-Origin: 'https://www.example.com'
    • アクセス元のドメイン
  • Access-Control-Allow-Credentials: 'true'

https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/http-api-cors.html

参考

設定 説明
1.メソッドリクエスト リクエストに認証が必要か、どのようなクエリパラメータを受け付けるかといったAPI Gatewayの受付設定を行う
2.統合リクエスト アップストリームの指定、リクエストボディの変換といったAPI Gatewayとアップストリーム間の統合設定を行う
3.統合レスポンス ステータスレコードのマッピング、レスポンス内容の変換といったアップストリームとAPIGateway間の統合設定を行う【2.統合リクエスト】で「統合プロキシ」を指定した場合は設定不可
4.メソッドレスポンス ステータスレコードごとのレスポンスヘッダーやレスポンスボディの設定といったAPI Gatewayからクライアントへのレスポンス設定を行う

https://qiita.com/leomaro7/items/66ea3233097fa410fef9

プロキシ統合の CORS サポートを有効にする

import json

def lambda_handler(event, context):
    return {
        'statusCode': 200,
        'headers': {
            'Access-Control-Allow-Headers': 'Content-Type',
            'Access-Control-Allow-Origin': 'https://www.example.com',
            'Access-Control-Allow-Methods': 'OPTIONS,POST,GET'
        },
        'body': json.dumps('Hello from Lambda!')
    }
mocknmockn

現状のAPIGateway設定(OPTIONS)

メソッドレスポンス

  • Access-Control-Allow-Headers
  • Access-Control-Allow-Methods
  • Access-Control-Allow-Origin

統合レスポンス

  • Access-Control-Allow-Headers: 'Content-Type'
  • Access-Control-Allow-Methods: 'OPTIONS'
  • Access-Control-Allow-Origin: '*'

検証

  • 以下が両方成功するか検証
    • Talend API Testerでリクエスト送信
    • 画面のコンソール上でfetchスクリプト実行
    • どちらもAPI Gatewayのエンドポイントを直叩きする
  1. Access-Control-Allow-Credentials: 'true' 追加
  • 失敗。CORSエラー。
  1. Access-Control-Allow-Origin: '<対象リクエスト元>' 設定
  • 失敗。プリフライトレスポンスのAccess-Control-Allow-Headers にAuthorizationが設定されていないというエラー。
  1. Access-Control-Allow-Headers: 'Content-Type,Authorization' を設定
  • 失敗。資格情報を含むCORS要求の場合はAccess-Control-Allow-Credentialsに'true'が指定されているべきだが '' (空)になっている
  • APIGatewayのOPTIONS設定は問題なさそうなので、プリフライトリクエストは正常に通っているが、POSTで失敗しているか(POST側の設定は変更していないため)

APIGateway側の設定は以上。次にLambda(POSTレスポンス)の設定を行う。

mocknmockn

現状のLambdaの設定

  • プロキシ統合を行っているため、APIGatewayのメソッドレスポンス、統合レスポンス設定は不要で、代わりにLambda側でレスポンス設定を行っている

レスポンス設定(POST)

  • 200
    • Access-Control-Allow-Headers: 'Content-Type'
    • Access-Control-Allow-Methods: 'POST'
    • Access-Control-Allow-Origin: ''<対象リクエスト元>'

検証

  • 以下が両方成功するか検証
    • Talend API Testerでリクエスト送信
    • 画面のコンソール上でfetchスクリプト実行
    • どちらもAPI Gatewayのエンドポイントを直叩きする

1. Access-Control-Allow-Credentials: 'true' 追加

  • 成功。HTTPステータス200、想定通りのレスポンス(メッセージ)が返却された

参考

https://qiita.com/ritya/items/d19f9379bc96fb63ad34

mocknmockn

Cloud Frontの設定不正

  • Cloud Frontのエンドポイント経由でAPIにリクエストを投げるとCORSエラーで失敗
    • Access-Control-Allow-Origin が無い
  • Cloud FrontからヘッダーがAPI Gatewayに渡っていないよう

対応方針

適切なヘッダーをオリジンサーバーに転送するように CloudFront ディストリビューションを設定する
https://repost.aws/ja/knowledge-center/no-access-control-allow-origin-error

Cloud Frontの設定

キャッシュポリシーを設定する(管理オリジンリクエストポリシー)

  1. 定義済みのポリシーをディストリビューションに追加する
  • Access-Control-Allow-Headers
  • Access-Control-Allow-Methods
  • Access-Control-Allow-Origin
  • Access-Control-Allow-Credentials
  • [CORS-S3Origin] または [CORS-CustomOrigin] を選択
  1. キャッシュポリシーを使用してヘッダーを転送する場合
  1. HTTP リクエストに対して OPTIONS メソッドを許可するように CloudFront ディストリビューションのキャッシュ動作を設定する
  • エラーにならなければ対応不要
  1. 必要な Access-Control-Allow-Origin ヘッダーを返すように CloudFront レスポンスポリシーを設定する
  • 基本的に不要
mocknmockn

資格情報付きCORSリクエストに対するレスポンス設定(マスタ)

OPTIONS

  • APIGatewayで設定
    • 統合レスポンス
Access-Control-Allow-Credentials: 'true'
Access-Control-Allow-Headers: 'Content-Type, Authorization'	
Access-Control-Allow-Methods: 'OPTIONS'
Access-Control-Allow-Origin: 'https:/example.com'
  • メソッドレスポンス
Access-Control-Allow-Credentials
Access-Control-Allow-Headers
Access-Control-Allow-Methods
Access-Control-Allow-Origin

POST

  • Lambdaのコードで実装
    • レスポンス
Hidden comment
mocknmockn

CloudFrontの設定

https://zenn.dev/bun913/articles/cloudfront-cors-policies

  • a

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/add-origin-custom-headers.html#add-origin-custom-headers-forward-authorization
https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/header-caching.html

  • a

https://github.com/aws-samples/amazon-cloudfront-functions/blob/main/add-cors-header/index.js

  • 動的にレスポンスヘッダを設定する場合はCF2(Cloud Front Functions)でリクエストのOriginヘッダーを取得し、レスポンスのOriginヘッダを上書きする処理を実装する
    • レスポンスヘッダーポリシーは静的レスポンスヘッダーのみ
  • Originが意図したものでなくても認証が通ってしまう?