CORSって何

Honoでapi立ててそこにReactからfetchするという単純なコードでCORSエラーが出て詰まった
この際にちゃんとCORSの概念と対処法を理解しておきたい
知りたいのは
- なんのために存在するのか
- エラーになるの面倒だが意義があるから存在しているはず
- "CORS"とは概念なのかエラーなのか

まずmdn読む

なるほど
CORSは「別のオリジンに対してアクセス権を与えてあげるようブラウザに指示するための仕組み」か
オリジン間リソース共有 (Cross-Origin Resource Sharing, CORS) は、追加の HTTP ヘッダーを使用して、あるオリジンで動作しているウェブアプリケーションに、異なるオリジンにある選択されたリソースへのアクセス権を与えるようブラウザーに指示するための仕組みです。ウェブアプリケーションは、自分とは異なるオリジン (ドメイン、プロトコル、ポート番号) にあるリソースをリクエストするとき、オリジン間 HTTP リクエストを実行します。
オリジン間リクエストとは、例えば https://domain-a.com で提供されているウェブアプリケーションのフロントエンド JavaScript コードが fetch() を使用して https://domain-b.com/data.json へリクエストを行うような場合です。

そもそもオリジンとは何か
なるほど
ウェブコンテンツのオリジン (Origin) は、ウェブコンテンツにアクセスするために使われる URL の スキーム (プロトコル)、 ホスト (ドメイン)、 ポート番号 によって定義されます。スキーム、ホスト、ポート番号がすべて一致した場合のみ、 2 つのオブジェクトは同じオリジンであると言えます。
オリジンは3つの構成要素を持つ(これ全部一致して初めてその2つは「同じオリジンである」と言える)
- スキーム(プロトコル)
- ホスト(ドメイン)
- ポート番号
http://localhost:5000/
とhttp://localhost:5001/
ではポート番号が違うからオリジンが違うということになり、CORSエラーが発生したということか、なるほど~~~
操作によっては同じオリジンのコンテンツに限定されており、この制約は CORS を使用して緩和することができます。
例の部分がすっごいわかりやすい

一応関連も拾っておく
ok、元記事に戻る

"オリジン間HTTPリクエスト"と呼ぶのか
ウェブアプリケーションは、自分とは異なるオリジン (ドメイン、プロトコル、ポート番号) にあるリソースをリクエストするとき、オリジン間 HTTP リクエストを実行します。
オリジン間リクエストとは、例えば https://domain-a.com で提供されているウェブアプリケーションのフロントエンド JavaScript コードが fetch() を使用して https://domain-b.com/data.json へリクエストを行うような場合です。

あーやっぱ「セキュリティ上の理由」がCORSエラーの理由であり意義なんだなぁ
セキュリティ上の理由から、ブラウザーは、スクリプトによって開始されるオリジン間 HTTP リクエストを制限しています。例えば、fetch() や XMLHttpRequest は同一オリジンポリシー (same-origin policy) に従います。つまり、これらの API を使用するウェブアプリケーションは、そのアプリケーションが読み込まれたのと同じオリジンに対してのみリソースのリクエストを行うことができ、それ以外のオリジンからの場合は正しい CORS ヘッダーを含んでいることが必要です。
fetch()
などのAPIはデフォルトで同一オリジンポリシーに従うらしい

同一オリジンポリシーについて

あーそういうことか、なるほど~~~
これにより、悪意のある可能性のあるドキュメントを隔離し、起こりうる攻撃のベクターを減らすことができます。例えば、インターネット上の悪意のあるウェブサイトがブラウザー内で JS を実行して、 (ユーザーがサインインしている) サードパーティのウェブメールサービスや (公開 IP アドレスを持たないことで攻撃者の直接アクセスから保護されている) 企業のイントラネットからデータを読み取り、そのデータを攻撃者に中継することを防ぎます。
JSはブラウザで実行されちゃうからセッションの認証情報とか盗み見られたりしたらやばいという前提がまずあって、そういう不正リクエストは現在のオリジンからではなく他のオリジンから来るのは当然で、だからそれはデフォルトで排除しておいて、必要に応じて許可する形式にすれば良い、という流れか!!理解

なんか日本語おかしいような
最近のブラウザーは CORS を fetch() や XMLHttpRequest などの API で使用して、オリジン間 HTTP リクエストのリスクの緩和に役立てています。
CORSというのは前述の通り「許可してあげる仕組み」なので、この文脈だとfetch()
とかが使ってるのはCORSというより"同一オリジンポリシー"では?
「CORS」と「同一オリジンポリシー」は対の位置にある概念な気がする
「許可してあげる仕組み」と「デフォルトでは許可しない仕組み」なので
まぁCORSを有効化することができるようになってるよ、ということなのかな

実態はHTTPヘッダーか
オリジン間リソース共有の仕様は、ウェブブラウザーから情報を読み取ることを許可されているオリジンをサーバーが記述することができる、新たな HTTP ヘッダーを追加することで作用します。
プリフライト...ほう
事前に認証しちゃって、okの場合のみリクエスト受け付けるみたいなことができるってことかな?凄そう
加えて仕様書では、サーバーの情報に副作用を引き起こすことがある HTTP のリクエストメソッド (特に GET 以外の HTTP メソッドや、特定の MIME タイプを伴う POST) のために、ブラウザーが HTTP の OPTIONS リクエストメソッドを用いて、あらかじめリクエストの「プリフライト」 (サーバーから対応するメソッドの一覧を収集すること) を行い、サーバーの「認可」のもとに実際のリクエストを送信することを指示しています。サーバーはリクエスト時に「資格情報」 (Cookie や HTTP 認証など) を送信するべきかをクライアントに伝えることもできます。

JSではわからないけどコンソール見れば詳細まで表示されるようになってるのか
CORS は様々なエラーで失敗することがありますが、セキュリティ上の理由から、エラーについて JavaScript から知ることができないよう定められています。コードからはエラーが発生したということしか分かりません。何が悪かったのかを具体的に知ることができる唯一の方法は、ブラウザーのコンソールで詳細を見ることです。

絶対プリフライト起きる訳ではなさそう
リクエストによっては CORS プリフライトを発生させません。これをこの記事では古い CORS 仕様書に倣って単純リクエストと呼んでいますが、 (現在 CORS を定義している) Fetch 仕様書 ではこの用語を使用していません。

あーそういうことか、プリフライトはリクエスト側が善意でやるやつってことか
単純リクエストとは異なり、「プリフライト」リクエストは始めに OPTIONS メソッドによる HTTP リクエストを他のドメインにあるリソースに向けて送り、実際のリクエストを送信しても安全かどうかを確かめます。オリジン間リクエストがユーザーデータに影響を与える可能性があるような場合に、プリフライトを行います。
じゃあサーバーによるpreLoadの事前認証みたいなことではないな

ん?プリフライトって明示的に善意で書くみたいなことではないっぽい
プリフライトが行われるリクエストの例です。
const fetchPromise = fetch("https://bar.other/doc", { method: "POST", mode: "cors", headers: { "Content-Type": "text/xml", "X-PINGOTHER": "pingpong", }, body: "<person><name>Arun</name></person>", }); fetchPromise.then((response) => { console.log(response.status); });
上記の例では、 POST で送信する XML の本体を作成しています。また、標準外の X-PINGOTHER HTTP リクエストヘッダーを設定しています。このようなヘッダーは HTTP/1.1 プロトコルに含まれていませんが、ウェブアプリケーションでは一般的に便利なものです。リクエストで Content-Type に text/xml を使用しており、かつ独自のヘッダーを設定しているため、このリクエストではプリフライトを行います。
じゃあ標準外のリクエストヘッダーだったりしたら自動で裏でプリフライトが走るようになってるってことかな?であれば良さげ

プリフライトリクエストを表すOPTIONSメソッドなるものがあるらしい
最初のブロックは、OPTIONS メソッドによるプリフライトリクエストを表します。ブラウザーは上記の JavaScript コードで使用していていたリクエストの引数に基づいて、プリフライトの送信が必要であることを判断します。これによりサーバーは実際のリクエストの引数によって送られるリクエストを受け入れ可能かを応答することができます。 OPTIONS は HTTP/1.1 のメソッドであり、サーバーから付加的な情報を得るために用いるもので、また安全なメソッド、つまりリソースを変更するためには使用できないメソッドです。

こちらも参考になる
インターネット技術がまだ新しかった時代では、クロスサイトリクエストフォージェリ (CSRF) の問題がありました。これらの問題では、被害者のブラウザから別のアプリケーションに偽のクライアントリクエストが送信されていました。
たとえば、被害者が銀行のアプリケーションにログインしたとします。その後、だまされて新しいブラウザタブに外部ウェブサイトを読み込まされたとします。そして、その外部ウェブサイトは被害者のクッキー認証情報を使用して、被害者になりすましてデータを銀行のアプリケーションに中継するのです。結果として、権限のないユーザーが銀行のアプリケーションに意図せずアクセスするようになったのです。
このような CSRF 問題を防ぐことを目的として、すべてのブラウザが同一オリジンポリシーを実装するようになりました。
同一オリジンのポリシーは非常に安全ですが、実際のユースケースでは柔軟性がありません。
クロスオリジンリソース共有 (CORS) は同一オリジンポリシーの拡張です。これは、外部の第三者とリソースを正式に共有するために必要です。たとえば、公開または承認されている外部 API からデータを取得する場合は、CORS が必要です。また、権限のある第三者に自分のサーバーリソースへのアクセスを許可したい場合も、CORS が必要とされています。
ただし、一部の HTTP リクエストは複雑と見なされ、実際のリクエストを送信する前にサーバーの確認が必要です。事前承認プロセスはプリフライトリクエストと呼ばれます。

最後にこちらを見ていく

自分のエラーを一応貼っとく
2番目のやつに関しては、200 ok なのにnet: ERR FAILEDと両立してるのはなんか不思議
リクエストが成功してレスポンスは成功裏に返却されてるけどそれを受け取る権限がないためCORS特有エラーが出てるということなのかな
エンドポイント名変えたら200 ok出なくなった、謎だ

これ、多分やらないでも大丈夫そう
※ XHR は特にやることはないですが、Fetch API では mode cors を設定する必要があります。
fetch('https://trusted-api.co.jp', { mode: 'cors' });
mdnいわく規定で設定されてるらしいので
既定では mode は cors に設定され、

ほー
サーバーサイドでは、以下のレスポンスヘッダーを追加する必要があります。
Access-Control-Allow-Origin: https://trusted-one.co.jp // 特定のサイトを許可する Access-Control-Allow-Origin: * // 全てのサイトを許可する(危険なのでプロダクトでは基本的には使わない) Access-Control-Allow-Headers "X-Requested-With, Origin, X-Csrftoken, Content-Type, Accept" // この辺は使うフレームワークにより異なるが許可するヘッダーを定義しておく。
originやMethodsは簡単だけどHeadersが難しそうだ、知らん単語だらけなので

なるほど、正規表現使ったりenvファイルに分離したりするのか
実際にプロダクトで設定するときは、脆弱性を考慮し特定のオリジンからのリクエストのみ受け付けるようにします。
// OK: https://front-end.com or https://abc.front-end.com...
// NG: https://evilsite.com...
const origin = req.headers.origin;
if (
origin === process.env.FRONTEND_ORIGIN // https://front-end.com
|| /^https:\/\/.+\.front-end\.com$/.test(origin)
) {
res.header('Access-Control-Allow-Origin', origin);
res.header('Access-Control-Allow-Headers', 'X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept');
res.header('Access-Control-Allow-Methods', 'PUT, DELETE, OPTIONS');
...
}
...

ほう
単純リクエスト (Simple Request) と言われているのは以下のメソッドです。
- GET
- POST
- HEAD
ここに補足含め書いてありそうだ
単純リクエストは、以下のすべての条件を満たすものです。

ふむ
単純リクエストとは異なり、プリフライトリクエスト (Preflight Request) は、リクエストの始めに OPTIONS メソッドで対象の異なるオリジンにリクエストを送り、実際のリクエストを送っても問題ないか確認します。
該当するリクエストは以下になります。
- PUT
- DELETE
- CONNECT
- OPTIONS
- TRACE
- PATCH
プリフライトリクエストのレスポンスとして、アクセスを許可するメソッドをレスポンスヘッダーに含める必要があります。
Access-Control-Allow-Methods: PUT, DELETE, PATCH
これらのメソッドは自動的にプリフライトリクエストとなるということかな
んでそのレスポンスにちゃんとアクセス許可するメソッドをヘッダーとして付与する必要があると

ほー、cookieもsame origin policy(SOP)適用されるのか
a.com という js のページを開いた状態で b.com へ XMLHttpRequest を送る際に b.com の Cookie も含めてリクエストを送りたいという場合、デフォルトでは異なる Origin に対して Cookie は送信されません。 (自分はこれにハマってしまったのでお気をつけて... 😖 )
Origin をまたいだ XMLHttpRequest で Cookie を送りたい場合、
Cookie の送受信を許可するために、クライアントサイド・サーバーサイドに実装が必要になります。
でもJSによる不正行為を防止するのがsame origin policyの意義であって、cookieなんかただの文字列だからJSではないのでなぜわざわざ規制するのかわからん

これは確かにやる必要がありそうだ
same-origin (既定値): 同一オリジンのリクエストに対してのみ資格情報を送信し、含めます。
fetch('https://trusted-api.co.jp', { mode: 'cors', credentials: 'include' // ここを追加。 });
規定値はincludeではなくsame-originになってるので
same-origin (既定値): 同一オリジンのリクエストに対してのみ資格情報を送信し、含めます。

へぇーワイルドカードでもダメな場合があるのか
credentials mode (withCredentials パラメータを着けている場合) では Access-Control-Allow-Origin は *(ワイルドカード) だとダメとのこと。
なので、このように Origin を明示的に指定する必要があります。

なるほど~~~すごく勉強になる
これだけ色々書いておいて、クライアントサイドもサーバーサイドも実装したし大丈夫!と意気込んでテストを行った結果、以下のようなエラーに遭遇しました...
API の CORS 許可設定はしっかりしていたはずなのに...
Access to XMLHttpRequest at 'https://authentication.com/auth?client_id=12345&scope=openid&profile&email&response_type=code&redirect_uri=http://api.com/auth/cb&state=abcdefg'
(redirected from 'http://api.com') from origin 'http://front-end.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
エラーの内容をよーく見てみると...
(redirected from 'http://api.com') from origin 'http://front-end.com' has been blocked by CORS policy
これですね... API の CORS の許可設定していたので問題ないかと思っていたのですが、
ブラウザが最初にアクセスする URL は Front End (front-end.com) であり、API でリダイレクトされて認証サーバにリクエストが飛ばされた結果、認証サーバでは Front End (front-end.com) の Origin の許可設定がされていないためエラーを返されてしまったわけです...。
たとえ API に CORS の許可設定を入れていたとしても、リダイレクトされてしまうとその先のサーバが CORS の許可設定を入れていないとエラーになるという観点が抜け漏れていました... :sob:
- 教訓
CORS エラーに遭遇した時は冷静にリクエストの流れを辿り、どのサーバーで CORS の許可設定がされていないのかを確認するようにしましょう。エラー内容から読み解くことができるはずです。
この記事でもリダイレクトで困ったとか言ってたな

へー、エラーにはならないだけで帰ってくるのは空のレスポンスなのか。使いどころが気になる
'no-cors': クロスオリジンリソース共有ができない場合に、エラーとはならず空のレスポンスが返却される。

追記
CSRF攻撃を防ぐための仕組みこそCORS/SOPか!と思い至った