CORS でのクッキーの取り扱い

作成者:カランカラン
💡

質問やフィードバックがありましたら、フォームからお願いします

本文は台湾華語で、ChatGPT で翻訳している記事なので、不確かな部分や間違いがあるかもしれません。ご了承ください

前言

CORS と cookie はフロントエンドにおいて非常に重要な問題ですが、開発中はフロントとバックエンドのドメインが同じであることが多いため、あまり気にされることはありません。あるいは、バックエンドに Access-Control-Allow-Origin: * を設定してもらえば済むと思われがちで、背後にある仕組みを理解することは少ないです。

この問題については、MDN に非常に詳しい説明がありますので、この記事では主に要点を整理し、実際の操作でよく発生する問題について取り上げます。

同源政策(same-origin policy)

JavaScript がウェブページ上で自由に操作するのを防ぐために、同源政策は特定のリソースやコードに対して同源の条件を満たす場合のみアクセスを許可します。

では、同源とは何でしょうか?document の出所は、プロトコル、ホスト、ポートで定義されます。つまり、もしドキュメント 1 が http://kalan.com から来ていて、ドキュメント 2 が https://kalan.com から来ている場合、これは同源とは見なされません。では、サブドメインの場合はどうでしょう?例えば https://api.foobar.comhttps://app.foobar.com は、ホストが異なるため、同じオリジンとは見なされません。

ただし、いくつかのリソースはもともとクロスオリジンで取得できるものもあります:

  • <img />
  • <video />, <audio />
  • <iframe />: ヘッダーを定義することで他者の埋め込みを防止できます
  • <link rel="stylesheet" href /> で読み込む CSS
  • <script src="" /> で読み込む JavaScript

ただし、プログラムコードから発信されるクロスオリジンリクエストは、同源政策の制約を受けます(例えば Fetch や XHR)。

明らかに、このような政策は厳しすぎます。すべてを同源政策に制限するなら、フロントエンドとバックエンドの開発は非常に困難になり、XHR を使って他の SDK の API を適用することもできなくなります。そのため、CORS(Cross-Origin Resource Sharing)の仕組みが登場しました。

CORS(Cross-Origin Resource Sharing)

CORS は多くの人がフロントエンドだけの知識だと思っていますが、実際にはバックエンドで関連するヘッダーを設定し、その背後にある意味を理解する必要があります。

では、クロスオリジンリクエストはどのように機能するのでしょうか?主に 2 つのヘッダー、OriginAccess-Control-Allow-Origin によってアクセス制御が行われます。

要求時の Origin と応答のヘッダー内の Access-Control-Allow-Origin の値が同じ、あるいは Access-Control-Allow-Origin: *(任意のドメインからのリソースアクセスを許可する)であれば、問題ありません。

CORS に合致しない場合、以下のメッセージが表示されます:

2019-01-18 10 12 54

もし返されたオブジェクトを読み取ろうとすると、警告が表示されます。

では...、提示された通りに fetch モードを no-cors に変更するとどうなるでしょうか?

確かに、厄介なエラーメッセージは処理されましたが、状況は改善されていないようです。

no-cors は万能薬ではありません。このモードを使用しても、CORS が解放されるわけではなく、リクエストが成功することはありません。その結果、SyntaxError: Unexpected end of input というエラーが発生します。このモードは通常、サービスワーカーと併用されます。

上記の実験から、CORS の制約を解除するにはただ一つの方法があることがわかります。それは、サーバー側に正しい Control-Access-Allow-Origin を追加することです(ホストはオリジンと同じであるか、あるいは * でなければなりません)。

また、CORS の仕組みは JavaScript が XHR または fetch を送信する際にのみ機能し、curl や Postman のような一般的なツールにはこの仕組みはありません。そのため、API エンドポイントのテスト時にこの点を見落とし、フロントエンドとバックエンドでテスト結果に食い違いが生じることがよくあります。

いくつかのクロスオリジンリクエストはプレフライトを発生させない一方で、他のリクエストは発生させます。MDN に記載されている条件は非常に明確です:

  1. GET, HEAD, POST のいずれかのメソッドである必要があります。
  2. ユーザーエージェントが自動的に設定するヘッダーと特定のヘッダーを除き、他のヘッダーは含まれないこと。許可される headers
  3. Content-Type がある場合(これはリクエストヘッダーであり、レスポンスヘッダーではない)、次の値のいずれかである必要があります:application/x-www-form-urlencoded, text/plain, multipart/form-data

つまり、上記の条件を満たさない場合、プレフライトリクエストが発生します。

Content-Typeapplication/json に変更してプレフライトの要件を満たすことを試みます(application/x-www-form-urlencoded, text/plain, multipart/form-data 以外)。

プレフライト

プレフライトとは、リクエストが最初に HTTP OPTIONS の方法で他のドメインに送信され、問題がないことを確認してから本当のリクエストが送信されることを指します。一度この条件がトリガーされると、やるべきことはかなり面倒になります。

  1. OPTIONS の同じ API エンドポイントを追加し、CORS の要件を満たすように Access-Control-Allow-Origin を設定する必要があります
  2. Access-Control-Allow-Headers を追加し、条件に含まれないすべてのヘッダーを含める必要があります。そうしないと通過できません。

プレフライトチェックを通過しなかった場合、以下のようなエラーメッセージが表示されます:

Access to fetch at 'http://localhost:3001/trigger-preflight' from origin 'http://localhost:3000' has been blocked by CORS policy:
Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response.

あるいは、OPTIONS の応答ヘッダーに Access-Control-Allow-Origin を追加していない場合:

Access to fetch at 'http://localhost:3001/trigger-preflight' from origin 'http://localhost:3000' has been blocked by CORS policy: 
Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. 
If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

もし成功すると、ネットワークには 2 つのリクエストが表示されます。一つは OPTIONS、もう一つは本当のリクエストです。

自作のヘッダーを追加した場合はどうでしょうか?MDN に基づいて、プレフライトリクエストをトリガーするはずです。X-Access-Token を追加して何が起こるか見てみましょう。

fetch("http://localhost:3001/trigger-preflight", {
  headers: { "X-Access-Token": "dontbeserious" },
})
  .then(res => res.json())
  .then(log)
2019-01-18 11 20 07

確かにプレフライトを通過できません。通過するためには、X-Access-TokenAccess-Control-Allow-Headers に追加する必要があります。

附帯認証のあるリクエスト

cookie はクロスオリジンで送信できないため、異なるオリジン間で cookie が相互に送信またはアクセスされることはありません。そうでなければ、混乱が生じます。しかし、A ドメインから B ドメインにリクエストを送信し、B ドメインが cookie の情報を返した場合、A ドメインには B ドメイン形式の cookie が保存されます。ただし、withCredentials または credentials: 'include' を設定しなければ、サーバーが Set-Cookie を返しても書き込まれません。以下のように:

2019-01-18 1 10 06 2019-01-18 1 10 18

通常、B ドメインの API を使用する際、cookie は自動的に送信されません。この場合、XHRwithCredentials を設定するか、fetch のオプションに { credentials: 'include' } を指定する必要があります。これはクロスオリジンリクエストであるため、CORS の要件に従って Access-Control-Allow-Origin を追加する必要があります。

fetch(`${hostname}/cookie`, {
  method: "POST",
  credentials: "include",
})
Access to fetch at 'http://localhost:3001/cookie' from origin 'http://localhost:3000' has been blocked by CORS policy: 
The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.

セキュリティの問題を避けるために、ブラウザは Access-Control-Allow-Origin* にすることを許可していません。

Access to fetch at 'http://localhost:3001/cookie' from origin 'http://localhost:3000' has been blocked by CORS policy: 
The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'.

しかし、これだけでは不十分で、ブラウザは Access-Control-Allow-Credentials がない応答を自動的に拒否します。そのため、クロスオリジンサーバーに認証情報を送信できるようにするには、Access-Control-Allow-Credentials: true を追加する必要があります。すべてが正しく設定されていれば、以下のようにリクエストクッキーに cookie が正常に送信されたことが表示されます。

2019-01-18 1 11 37

さて、これらの設定に成功しても、cookie をサーバーに送信できない可能性があります。この点については、以下のような状況が考えられます:

ユーザーがあなたをブラックリストに登録したため、cookie が正常に送信されない可能性があります。

解決策:

  • ドメインを変更する
  • なぜユーザーにブロックされたかを反省する

Safari では時々この設定が有効になり、デバッグ中に多くの苦労を味わいました。

2019-01-18 1 13 21

後記

CORS の処理は手間のかかる面倒な作業です。特に、Access-Control-Allow-Origin や Access-Control-Allow-Credentials を忘れると、CI/CD を実行し、デプロイするまでに一日かかることもあります。今回はいくつかの一般的な問題を整理しましたので、今後同様の状況に直面した際には対処法がわかることを願っています。

今では AWS API Gateway があり、主なコードに手を加えずに必要なヘッダーを追加できるようになりました。または、同じドメイン内にプロキシを設置することも一つの手です。

参考記事

この記事が役に立ったと思ったら、下のリンクからコーヒーを奢ってくれると嬉しいです ☕ 私の普通の一日が輝かしいものになります ✨

Buy me a coffee