SPAの場合のセッション（Cookie）認証におけるCSRF対策

## TL;DR
* 書くこと：SPAの場合のセッション（Cookie）認証における、私なりのCSRF対策の解
* 書かないこと：
    * JWTとの比較
    * 一般的なセキュリティの話（CSRFとは何か、など）
    * Cookieの詳しい仕組み

## モチベーション
* 自社でAPIの認証周りの話が出た時に、自分の考えを持てていなかったので歯痒かった
* CSRFに関する外部の記事を読み漁ったけど、「何故そうなのか」を理解できなかった
* 知ってたら自分のサービス作る時に良さそう

## 結論
まずは結論から述べます。
SPAでセッション認証を行う場合、以下を実施しておけばCSRFを防げます。

1. APIのドメインが認証用Cookieのドメインと同じ場合：Cookieの`SameSite`属性を`Lax`に指定
2. APIのドメインが認証用Cookieのドメインと異なる場合：
    * Cookieの`SameSite`属性を`None`に指定し、`Secure`属性を付与する
    * CSRF tokenを利用する
    * 事前にPreflight requestを投げる

通常は`SameSite=None`と`Secure`の設定に加えてPreflight requestを投げるだけでいい筈ですが、tokenを利用するようにしておくと完璧だと思います。
理由は後述します。

## 前提：CSRFが何故起きるのか
端的に、以下の2つが要因。

* Cookieは「ドメインをまたぐリクエスト」にも自動で付与されるから
* サーバ側は基本、「どこから送られたか」を見ていないから

要はサイトAの認証情報（Cookie）を持って悪質なサイトBに行ったと仮定した場合、サイトBからサイトAへのPOSTリクエストを投げられると、サイトAはCookieが正しいからリクエストを受け付けちゃうんだぜ、というお話。

加えてCORSで弾こうにもリクエスト自体は到達してしまうので、CORSだけでは意味を成しません。
CORSで異なるドメインを弾くのはブラウザであって、APIサーバではないからです。

## APIのドメインが認証用Cookieのドメインと同じ場合の対策
前述したとおり、Cookieの`SameSite`属性を`Lax`に指定しすればOK。
`SameSite`属性は値によって以下のように挙動が変わります。

* `Strict`：異なるドメインへのリクエストにCookieを付与しない
* `Lax`：モダンブラウザのデフォルト値。異なるドメインへのPOST・画像リクエスト、XHR、iframe経由のリクエストにCookieを付与しない
* `None`：以前のデフォルト値。異なるドメインへのリクエストであってもCookieを付与する

## APIのドメインが認証用Cookieのドメインと異なる場合の対策
### 1. Cookieの`SameSite`属性を`None`に指定し、`Secure`属性を付与する
前述したように、`SameSite=None`だと今までと変わりません。
最低限`Secure`属性を使ってHTTPS通信することを心がけましょう。

そもそもモダンブラウザでは`SameSite=None`を設定する場合には`Secure`属性も付与しないとリクエストできませんのでご注意下さい。

### 2. CSRF tokenを利用する
セッション情報以外にtokenを発行して認証に利用する仕組みです。
サーバ側で発行したtokenをブラウザに返し、VuexのStoreやCookieなどに仕込んでリクエスト時に利用します。
サーバ側はセッション情報とこのtokenを用いて認証する、というわけです。

この仕組みは以下のポイントによってCSRFを防ぎます。

- 独自ヘッダーを付与して投げる形となるため、独自ヘッダーを付与できないフォームのリクエストから利用できない
- 仮にtokenをCookieに仕込んだとしても、悪意のあるサイトからはSOP（Same Origin Policy）の制限によってJSからCookieにアクセスできない

### 3. 事前にPreflight requestを投げる
Preflight requestはリクエスト前にOPTIONSメソッドを用いたHTTPリクエストを送り、リクエストが安全かを事前に確かめるブラウザの仕組みです。
この仕組みが発生しないリクエストを「単純リクエスト」と呼びます。

以下の全てを満たすと「単純リクエスト」になります。

1. HTTPメソッドが HEAD、GET、POSTのいずれか
2. ユーザーエージェントによって自動的に設定されたヘッダーしか含まない
3. `Content-Type`が以下のいずれか
    * `application/x-www-form-urlencoded`
    * `multipart/form-data`
    * `text/plain`

逆に上記以外の条件だとPreflight requestが発生します。
この時レスポンスヘッダのオリジンとリクエスト送信元のオリジンが一致しないとPreflight requestが失敗し、後続する実際のリクエストが送信されません。
XSSと違ってCSRFは異なるドメインから送信されるため、ここで攻撃が失敗します。

ただし以下の点で注意が必要です。

* Preflight requestはあくまでブラウザの仕組みのため、ブラウザを介さず送信されると機能しない
* うっかり単純リクエストを受け付けられるようにしていると抜けられる
    * 例） `Content-type`の`application/x-www-form-urlencoded`などを許可してしまっている
        * → 異なるドメインからでも通常のPOSTリクエストは受け付けてしまいます…

Preflight requestだけに依存するとリスクがありますよ、というお話。

## まとめ
とりあえずSPA × セッション（Cookie）認証を利用するなら以下をやっておきましょう。

1. APIのドメインが認証用Cookieのドメインと同じ場合：Cookieの`SameSite`属性を`Lax`に指定
2. APIのドメインが認証用Cookieのドメインと異なる場合：
    * Cookieの`SameSite`属性を`None`に指定し、`Secure`属性を付与する
    * CSRF tokenを利用する
    * 事前にPreflight requestを投げる

これでSPAでもCSRFから守られるね！やったね！
まぁ何をしてもXSSとかくらったらCookie送信されて終わるので頑張っていきましょう。

※ 内容に相違などあれば是非コメントください！修正します！

## 補足
* `Referrer`属性見ればいいじゃん！と思うかもしれませんが、HTTPSで通信すると`Referrer`が見えませんので使えません
* それなら`Origin`属性を見たらいいじゃん？！と思うかもしれません。それはアリなんですが、ホワイトリスト方式でオリジンを許可するようにしてしまうとローカルで検証しにくくなるなぁ…と思って省きました
* `HttpOnly`属性を設定できれば安心と思いきや、これを設定して返してしまうと認証されたいサーバでセッション情報にアクセスできなくなってしまうので無理です

## 参考資料
- [MDN web docs - SameSite cookies](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Set-Cookie/SameSite)
- [blog.jxck.io - Cookie の性質を利用した攻撃と Same Site Cookie の効果](https://blog.jxck.io/entries/2018-10-26/same-site-cookie.html)
- [Qiita - Web API の CSRF 対策まとめ【追記あり】](https://qiita.com/okamoai/items/044c03680766f0609d41)
- [くろのて - CORS とか Preflight とかよくわかんないよな](https://note.crohaco.net/2019/http-cors-preflight/)
- [親バカエンジニアのナレッジ帳 - SPAサイトのCSRF対策](https://ti-tomo-knowledge.hatenablog.com/entry/2018/08/28/122825)