[Set-Cookie] Set-CookieでCookieファイルに値を格納できない問題の原因と解決方法

## この記事を読んでできること
サーバーサイド側から`Set-Cookie`でクライアントのCookieファイルにトークンを格納できない問題を解決できる

## なぜこの記事を書くことにしたか
サーバーサイド側から`Set-Cookie`でクライアントにCookieの値を格納しようとしたが、格納されず、解決にかなりの時間を割くことになったため

## やりたいこと
サーバーサイド側から`Set-Cookie`でクライアントのデベロッパーツールのApplicationのCookieファイルにCSRFトークンの値を格納したい

## 記述しないこと
- FastAPIの実装について
- Cookieについて

## 使用技術
### バックエンド
- 言語
    - Python 3.10.4
- フレームワーク
    - Fast API 0.78.0
- データベース
    - MySQL 8.0.31
- ライブラリ
    - starlette-csrf==1.4.4

※ちなみに
- トークンを発行するシステムは[starlette-csrf](https://github.com/frankie567/starlette-csrf)というライブラリに任せます
    - ライブラリの仕様については最後尾に記載します
- ローカル環境で検証しています


## 現状
トークンを発行するAPIをフロントエンドに叩いてもらい、Networkにはレスポンスヘッダに`Set-Cookie`が渡されていることが確認できる
しかし、Cookieファイルにはトークンが格納されていない

```main.py
import os

from fastapi import FastAPI
from starlette_csrf import CSRFMiddleware

from routers import router


app = FastAPI()

app.add_middleware(
    CSRFMiddleware,
    secret=os.environ["SECRET_KEY"],
)

app.include_router(router.router)
```
```routers/router.py
@router.get("/api/get_csrf_token", tags=["token"])
def get_csrf_token():
    return "トークンを発行しました。"
```
Set-Cookieは返ってきているが、cookieファイルにはトークンがない

<img width="884" alt="スクリーンショット 2022-12-16 10.53.01.png (207.1 kB)" src="https://img.esa.io/uploads/production/attachments/14274/2022/12/16/126296/87d889c8-cbae-40b9-aa3b-0d295890aaa1.png">

<img width="1199" alt="スクリーンショット 2022-12-16 10.50.07.png (164.0 kB)" src="https://img.esa.io/uploads/production/attachments/14274/2022/12/16/126296/91e3c50e-f559-4609-985b-3ff66e4bfed2.png">


## 原因
**Cookieの属性について把握をしていなかったため**
Response HeaderのSet-Cookieの横にある小さなアテンションマークにカーソルを合わせることで表記される

以下がその要因である
1. このSet-Cookieヘッダーによるクッキーの設定は、「SameSite=None」属性は持っているものの、「SameSite=None」を使用するために必要な「Secure」属性を持っていなかったため、ブロックされる
1. このSet-Cookieヘッダーによるクッキーの設定の試みは、「Same-Site=Lax」属性を持っているものの、トップレベルナビゲーションに対する応答ではないクロスサイトの応答から来るため、ブロックされる

今回であれば、starlette-csrfライブラリのデフォルトが`Same-Site=Lax`のため、2番である
SameSite、Secure属性については以下の記事を参照
[SameSite cookies](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Set-Cookie/SameSite)
### トップレベルナビゲーションとは
そのリクエストによってアドレスバーに記載されているURLが変化するものであるナビゲーションのこと
以下の記事を参照
[SameSite 属性を使った Cookie のセキュアな運用を考える](https://numb86-tech.hatenablog.com/entry/2020/01/26/112607)

## 該当箇所を修正
今回は`http://localhost:3000`から`http://127.0.0.1:8000/api/get_csrf_token`へリクエストしており、全く別物のオリジンからくるレスポンスのため、Cookieの属性をNone+Secureにする
starlette-csrfライブラリのREADMEを参照し、以下のように引数を渡す
これによって、CookieにSameSite=None, Secure=True属性を付与することができる
```main.py
app.add_middleware(
    CSRFMiddleware,
    secret=os.environ["SECRET_KEY"],
    cookie_samesite=None,
    cookie_secure=True,
)
```

Cookieの属性がSameSite=None、Secureであることが確認できる

<img width="874" alt="スクリーンショット 2022-12-16 10.54.23.png (206.0 kB)" src="https://img.esa.io/uploads/production/attachments/14274/2022/12/16/126296/de6d2650-1a42-42ac-bc2b-758560bb2189.png">
無事、 Cookieが格納されていることを確認できる

<img width="1200" alt="スクリーンショット 2022-12-16 10.44.48.png (114.7 kB)" src="https://img.esa.io/uploads/production/attachments/14274/2022/12/16/126296/b6f27c13-639f-439d-9b1e-f97ab34f2a02.png">

## starlette-csrfについての詳細
### starlette-csrfの仕様
1. 安全なhttpメソッド({“GET”, “HEAD”, “OPTIONS”, “TRACE”})でリクエストされたとき、set-cookieでクライアントのcookieにcsrftokenというkeyで格納する
1. クライアントからPOSTリクエストされたとき、x-csrftokenというkeyでcookie headerが送信されることを期待する
1. cookieとheaderのtokenの値を比較検証する
1. それらが一致した場合、リクエストは処理される
1. 一致しない場合は、403 Forbiddenエラー応答が返される

## 処理の検証結果

- 現状、単純なapi/get_csrf_tokenというエンドポイントを作成し、tokenをセットする
- POSTMANで
  - ヘッダにx-csrftokenを用意しない場合は、403エラー
  - ヘッダにx-csrftokenを用意したが、valueが一致しない場合も403エラー
  - ヘッダにx-csrftokenを用意し、valueが一致した場合、リクエストを処理する

## 参考
- [HTTP Cookieの使用](https://developer.mozilla.org/ja/docs/Web/HTTP/Cookies)
- [SameSite cookies](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Set-Cookie/SameSite)
- [starlette-csrf](https://github.com/frankie567/starlette-csrf)