一覧WB-12-2 · オリジン境界とCORS

プリフライトと認証情報付きリクエスト

文書はhttp://127.0.0.1:4322、APIは4323番です。PATCHとJSON本文・X-Labヘッダーの事前確認を調べ、次にcredentialsの値とサーバーの許可設定を変えます。同じホストのCookieはポート別ではなく、ここではlab=blueが保存されています。

説明

別オリジンへの要求では、メソッドやヘッダーによって本要求の前にOPTIONSの確認が行われます。このプリフライトが成立しなければ、本要求は送信されません。Cookieなどの資格情報を含む場合は、応答公開にも追加の条件があります。

文書はhttp://127.0.0.1:4322、APIは4323番です。PATCHとJSON本文・X-Labヘッダーの事前確認を調べ、次にcredentialsの値とサーバーの許可設定を変えます。同じホストのCookieはポート別ではなく、ここではlab=blueが保存されています。

1事前確認が必要な要求

PATCHとJSON本文を送る要求を、許可なしのAPIへ出します。

(globalThis.api="http://127.0.0.1:4323",await fetch(api+"/api?policy=deny&tag=patch-deny",{method:"PATCH"事前確認が必要なメソッド,headers:{"Content-Type":"application/json"JSONの要求形式,"X-Lab":"one"独自の要求ヘッダー},body:'{"qty":2}'}).then(r=>({status:r.status}),e=>({error:e.name})))

この要求はプリフライトが必要で、許可がないため失敗します。次に、本要求が送られたかを記録で確認します。

2OPTIONSだけが届く

patch-denyの受信記録を表示します。

await(await fetch("/records?tag=patch-deny事前確認と本要求の記録")).json()

OPTIONSだけが届き、Access-Control-Request-MethodにPATCH、Request-Headersにcontent-type,x-labが入ります。これは送りたい本要求の条件です。許可が成立しなかったためPATCH本文は届いていません。

3メソッドとヘッダーを許可する

指定オリジン・PATCH・Content-Type・X-Labを許可したAPIへ同じ内容を送ります。

await(await fetch(api+"/api?policy=exactオリジン・メソッド・ヘッダーを許可&tag=patch-allow",{method:"PATCH"許可後の本要求,headers:{"Content-Type":"application/json","X-Lab":"one"},body:'{"qty":2}'})).json()

本要求が成功し、PATCHとJSON本文が返ります。Allow-Originだけでなく、必要なメソッドと要求ヘッダーもプリフライトで許可されています。

4確認の後に本要求が続く

patch-allowの二つの要求を表示します。

await(await fetch("/records?tag=patch-allow二つの要求を順に取得")).json()

OPTIONSの後にPATCHが記録されています。事前確認にはJSON本文はなく、許可された本要求に本文が入ります。この演習ではMax-Age=0と別のURLを使います。実際にはプリフライト結果をキャッシュできるため、毎回OPTIONSが見えるとは限りません。

5既定では別オリジンへCookieを含めない

通常のFetchで、応答の公開を許可したAPIへGETします。

await(await fetch(api+別オリジンへ既定設定で取得"/api?policy=exact&tag=default-cookie")).json()

受信Cookieは空です。Fetchの既定はcredentials:same-originなので、ホストが同じでもオリジンの異なる4323番への要求には含めません。Cookie自体の対象範囲とFetchの資格情報設定は別の条件です。

6includeとAllow-Originの*

Cookieを含め、ワイルドカードの公開許可を返すAPIへGETします。

await fetch(api+"/api?policy=wildAllow-Originは*&tag=wild-cookie",{credentials:"include"資格情報を含める}).then(r=>({status:r.status}),e=>({error:e.name}))

読取りは失敗します。credentials:includeの応答公開にAllow-Origin:*は使えません。Cookieが送られたかは次の記録で確認します。

7Cookieは送信済みである

wild-cookieの要求を取得します。

(await(await fetch("/records?tag=wild-cookie受信したCookieを確認")).json()).map(r=>({method:r.method,cookie:r.cookie}))

GETにlab=blueが含まれています。このGETでは応答を読めないこととCookieを送らないことは一致しません。資格情報付き要求でも、事前確認を要しない場合があります。

8オリジン一致だけでは足りない

Allow-Originを一致させ、Allow-Credentialsを返さないAPIへ要求します。

await fetch(api+"/api?policy=exactオリジンだけを一致&tag=no-credentials-header",{credentials:"include"資格情報付き要求}).then(r=>({status:r.status}),e=>({error:e.name}))

これも読取りに失敗します。資格情報付きの応答公開には、具体的なオリジンの一致に加えてAccess-Control-Allow-Credentials:trueが必要です。

9二つの公開条件を満たす

具体的なAllow-OriginとAllow-Credentials:trueのAPIへ要求します。

await(await fetch(api+"/api?policy=credsオリジンと資格情報の公開を許可&tag=credentials-ok",{credentials:"include"Cookieを含める})).json()

応答が読め、lab=blueの受信も確認できます。これは同じホストでの例です。異なるサイトの場合にはSameSiteやブラウザのCookie制限も関係します。CORSの許可だけであらゆるCookie送信が可能になるわけではありません。

10資格情報付きPATCH

Cookieを含むPATCHを、必要な許可を返すAPIへ出します。

await(await fetch(api+"/api?policy=all&tag=credential-patch",{method:"PATCH"事前確認が必要な操作,credentials:"include"本要求に資格情報を含める,headers:{"Content-Type":"application/json","X-Lab":"two"},body:'{"qty":3}'})).json()

今度はプリフライトを経て、Cookieと本文を含むPATCHが届きます。事前確認自身と本要求とで、Cookieの有無を比較します。

11プリフライトにはCookieを含めない

二つの要求のmethod・Cookie・本文を抜き出します。

(await(await fetch("/records?tag=credential-patch資格情報付き操作の記録")).json()).map(r=>({method:r.method,cookie:r.cookie二段階のCookieを比較,body:r.body本文の送信段階,requestMethod:r.requestMethod,requestHeaders:r.requestHeaders}))

OPTIONSのCookieと本文は空で、本要求のPATCHにはlab=blueとqty=3が入ります。事前確認の許可と本要求の認証・認可は役割が異なります。サーバーではOPTIONSにCookieがないことを前提に確認条件を返し、本要求で操作の権限を検査します。

まとめ

プリフライトは本要求のmethodとヘッダーを提示し、許可された場合に本要求が続きます。

credentials:includeでは具体的なAllow-OriginとAllow-Credentials:trueが必要です。

プリフライトのCookieなしと、本要求の資格情報付き処理を分けて確認します。

操作履歴と解説を振り返れます。

ブラウザ / オリジン・通信記録

1
(globalThis.api="http://127.0.0.1:4323",await fetch(api+"/api?policy=deny&tag=patch-deny",{method:"PATCH",headers:{"Content-Type":"application/json","X-Lab":"one"},body:'{"qty":2}'}).then(r=>({status:r.status}),e=>({error:e.name})))
{
  "error": "TypeError"
}
2
await(await fetch("/records?tag=patch-deny")).json()
[
  {
    "tag": "patch-deny",
    "method": "OPTIONS",
    "origin": "http://127.0.0.1:4322",
    "requestMethod": "PATCH",
    "requestHeaders": "content-type,x-lab",
    "cookie": "",
    "body": ""
  }
]
3
await(await fetch(api+"/api?policy=exact&tag=patch-allow",{method:"PATCH",headers:{"Content-Type":"application/json","X-Lab":"one"},body:'{"qty":2}'})).json()
{
  "accepted": true,
  "method": "PATCH",
  "cookie": "",
  "body": "{\"qty\":2}"
}
4
await(await fetch("/records?tag=patch-allow")).json()
[
  {
    "tag": "patch-allow",
    "method": "OPTIONS",
    "origin": "http://127.0.0.1:4322",
    "requestMethod": "PATCH",
    "requestHeaders": "content-type,x-lab",
    "cookie": "",
    "body": ""
  },
  {
    "tag": "patch-allow",
    "method": "PATCH",
    "origin": "http://127.0.0.1:4322",
    "requestMethod": "",
    "requestHeaders": "",
    "cookie": "",
    "body": "{\"qty\":2}"
  }
]
5
await(await fetch(api+"/api?policy=exact&tag=default-cookie")).json()
{
  "accepted": true,
  "method": "GET",
  "cookie": "",
  "body": ""
}
6
await fetch(api+"/api?policy=wild&tag=wild-cookie",{credentials:"include"}).then(r=>({status:r.status}),e=>({error:e.name}))
{
  "error": "TypeError"
}
7
(await(await fetch("/records?tag=wild-cookie")).json()).map(r=>({method:r.method,cookie:r.cookie}))
[
  {
    "method": "GET",
    "cookie": "lab=blue"
  }
]
8
await fetch(api+"/api?policy=exact&tag=no-credentials-header",{credentials:"include"}).then(r=>({status:r.status}),e=>({error:e.name}))
{
  "error": "TypeError"
}
9
await(await fetch(api+"/api?policy=creds&tag=credentials-ok",{credentials:"include"})).json()
{
  "accepted": true,
  "method": "GET",
  "cookie": "lab=blue",
  "body": ""
}
10
await(await fetch(api+"/api?policy=all&tag=credential-patch",{method:"PATCH",credentials:"include",headers:{"Content-Type":"application/json","X-Lab":"two"},body:'{"qty":3}'})).json()
{
  "accepted": true,
  "method": "PATCH",
  "cookie": "lab=blue",
  "body": "{\"qty\":3}"
}
11
(await(await fetch("/records?tag=credential-patch")).json()).map(r=>({method:r.method,cookie:r.cookie,body:r.body,requestMethod:r.requestMethod,requestHeaders:r.requestHeaders}))
[
  {
    "method": "OPTIONS",
    "cookie": "",
    "body": "",
    "requestMethod": "PATCH",
    "requestHeaders": "content-type,x-lab"
  },
  {
    "method": "PATCH",
    "cookie": "lab=blue",
    "body": "{\"qty\":3}",
    "requestMethod": "",
    "requestHeaders": ""
  }
]

演習完了