まとめ
2020/05/17 修正
fetch などを使った CORS リクエストにおいて、API サーバから SameSite 設定付きで Set-Cookie が返された場合、以降の CORS リクエストに Cookie は付くのかどうか → SameSite=none の場合のみ Cookie が付く。
ただし、サブドメイン部だけが異なるドメイン間での CORS の場合、lax/strict でも Cookie が付く → もうちょっと調べたトピック 参照
以下の通り、lax や strict を指定された Cookieは 別ドメインに対する CORS fetch リクエストには付かない。
SameSite | Cookie |
none | ○付く |
lax | ×付かない |
strict | ×付かない |
CORS で Cookie を使う場合、 SameSite=none にしつつ API の応答に Access-Control-Allow-Credentials: true を付けたり、 fetch 時のオプションに credentials: 'include' 付けたりと、CORSの仕様に沿う必要もある。
また、ブラウザの設定でサードパーティーCookieがブロックされている場合、 SameSite の指定に関係なく Cookie は付かない(Set-Cookieが無視されるのでブラウザに記憶されない)。
この挙動を考えると、Cookie認証なAPI サーバを別ドメインにする構成では SameSite=none の指定が必須になるわけですね。
以下打ち消し線部分は間違い
Cookie の挙動と Same Origin Policy を混同してました。
Cookie の場合、ドメイン名のみで識別されスキーマとポート番号は無視されるので、APIサーバを別ポートで動かしただけでは CORS が必要になるけれども Cookie の挙動は同一ドメイン時と同じ、という状態でのテストになっていた。という次第。
(そりゃ全部Cookie付くわなorz)
fetch などを使った CORS リクエストにおいて、API サーバから SameSite 設定付きで Set-Cookie が返された場合、以降の CORS リクエストに Cookie は付くのかどうか → 付く。
SameSite | Cookie |
none | ○ |
lax | ○ |
strict | ○ |
ただし、API サーバの応答に Access-Control-Allow-Credentials: true を付けたり、 fetch 時のオプションに credentials: 'include' 付けたりと、CORS 下で Cookie を使う仕様に沿う必要はある。
検証に使った適当なコード
簡易API サーバ
指定された条件で Set-Cookie を発行する API と、リクエスト中の Cookie を取得して返す API の簡易APIサーバ。
別ドメインに見せかけるため、 hosts ファイルに以下を追加して apitest.example.com でアクセスする(検証が終わったら削除する)。
127.0.0.1 apitest.example.com
/api/ に { 'msg': 'hoge', 'samesite': 0} みたいな JSON を POST すると Set-Cookie 入りのレスポンスを返す。
/api/cookie/ に POST したり GET したりすると、リクエスト中の Cookie の中身を JSON で返す。
// cors_api.js const express = require('express'); const cookieParser = require('cookie-parser'); const cors = require('cors'); const app = express(); app.use(cookieParser()); app.use(express.json()); // application/json // see https://expressjs.com/en/resources/middleware/cors.html#configuration-options const cors_options = { origin: true, // Access-Control-Allow-Origin: <Origin> credentials: true // Access-Control-Allow-Credentials: true } // set cookie to response app.options('/api/', cors(cors_options)); app.post('/api/', cors(cors_options), (req, res) => { console.log(req.cookies); const msg = req.body.msg; const samesite = req.body.samesite == 0 ? 'none' : req.body.samesite == 1 ? 'lax' : 'strict'; res.cookie('msg', msg, { httpOnly: true, sameSite: samesite }); res.json({ msg : `msg=${msg}; samesite=${samesite}`}); }); // get cookie from request const getCookie = (req, res) => { console.log(req.cookies); const msg = req.cookies['msg']; res.json({ msg : `msg=${msg}`}); }; app.options('/api/cookie/', cors(cors_options)); app.get('/api/cookie/', cors(cors_options), getCookie); app.post('/api/cookie/', cors(cors_options), getCookie); // start server app.listen(3000, 'localhost', () => console.log('Listening on port 3000'));
ドライバHTML
簡易APIサーバの API を叩くドライバHTMLを、VS Code の LiveServerなどの適当なWebサーバ介して localhost ドメインとしてブラウザで開く。
テキストボックスに Cookie に設定する文字列を入れ、その後ろのセレクトボックスは Cookie に設定する SameSite の種類を選ぶ。SET ボタンを押すと /api/ が叩かれて Set-Cookie 付きのレスポンスが得られる。
そして、GETボタンやPOSTボタンを押すと、それぞれのメソッドで /api/cookie/ が叩かれて、それらのリクエストに付いてくる Cookie の中身が表示される。
msg=undefined となったり、テキストボックスに入れた文字列と違うものが表示された場合はサーバ側で Cookie が取得できていないことを表す。
<!DOCTYPE html> <html> <body> <input type="text" id="msg"></input> <select id="samesite"> <option value="0">none</option> <option value="1">lax</option> <option value="2">strict</option> </select> <button id="s">SET</button> <button id="s_get">GET</button><button id="s_post">POST</button><span id="ans"></span> <script> document.getElementById('s').addEventListener('click', async (e) => { e.preventDefault(); const msg = document.getElementById('msg').value; const samesite = document.getElementById('samesite').value * 1; const req = { msg, samesite }; res = await fetch("http://apitest.example.com:3000/api/", { method: 'POST', body: JSON.stringify(req), mode: "cors", credentials: 'include', headers: { "Content-Type": "application/json; charset=utf-8", } }); json = await res.json(); document.getElementById('ans').innerText = json.msg; }); document.getElementById('s_get').addEventListener('click', async (e) => { e.preventDefault(); res = await fetch("http://apitest.example.com:3000/api/cookie/", { method: 'GET', mode: "cors", credentials: 'include'}); json = await res.json(); document.getElementById('ans').innerText = json.msg; }); document.getElementById('s_post').addEventListener('click', async (e) => { e.preventDefault(); res = await fetch("http://apitest.example.com:3000/api/cookie/", { method: 'POST', mode: "cors", credentials: 'include'}); json = await res.json(); document.getElementById('ans').innerText = json.msg; }); </script> </body> </html>