Laravel SanctumのSPA認証でつまづいたところのメモ
Laravel SanctumのSPA認証(セッションベースの認証)の実装でかなりつまづいたので、メモを残しておこうと思います。おかげで認証とセキュリティ周りがちょっと分かるようになりました。
結論
- クロスオリジンのリクエストの場合、axiosはCSRFトークンをCookieから取り出してリクエストヘッダにつける処理を行わない
- フロントエンドとAPIを同一サイトに置いてSameSiteクッキーで認証すれば、トークンを使わずに(ほぼ)CSRF対策ができる
正しいかどうか自信がないので、間違っているところがあれば教えていただけるとうれしいです。
つまづいたところ
ドキュメントや記事を参考にして、ローカルではSPA認証を実装することができたのですが、検証環境にデプロイするとログインができなくなりました。原因は2つありました。
- セッションIDがSameSiteクッキーで返却されるため、APIとフロントが別サイトだとセッションIDを受け取れない
- APIはVPS、フロントはS3にデプロイしていたため別サイトでした
- 異なるオリジンへのリクエストの場合、axiosはCSRFトークンを付与する処理を行わない
セッションIDがSameSiteクッキーで返却される
LaravelのデフォルトはSameSite属性がlaxになっているため、クッキーを受け取ることができていませんでした。Networkのレスポンスヘッダを見るとブロックされていることが分かりました。
「SameSite属性がLaxで、クロスサイトのレスポンスなのでブロックされました」と書かれています。クッキーのSameSite属性とは、異なるサイトへのクッキーの送受信を制限するためのものです。
SameSite属性はLax、Strict、Noneという3つの値をとります。Strictの場合はクロスサイトのクッキーの送信が完全に制限されます。Noneの場合は制限がかからず、Laxは安全なリクエストのみ送信されます。
異なるオリジンへのリクエストの場合axiosはCSRFトークンを付与しない
Sanctumのドキュメントには、SPA認証の手順について以下のように書かれています。
- 最初に
/sanctum/csrf-cookie
にリクエストを送る - レスポンスのクッキーで受け取った
XSRF-TOKEN
を、リクエストヘッダのX-XSRF-TOKEN
に付与してリクエストを送る- axiosはこの処理を自動で行ってくれるので、特別な処理を書かなくてもよい
しかし、CSRFトークンの自動付与はAPIとSPAが同一オリジンの場合しか行ってくれません。なぜなら、axiosのCSRFトークンの設定箇所に以下のような条件分岐があるためです。
if (utils.isStandardBrowserEnv()) {
// withCredentialsオプションがtrueで、
// リクエスト元とリクエスト先が同一オリジンの場合のみ、クッキーからCSRFトークンを取得する
var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
cookies.read(config.xsrfCookieName) :
undefined;
if (xsrfValue) {
requestHeaders[config.xsrfHeaderName] = xsrfValue;
}
}
つまり、Laravel Mixを使っていてSPAとAPIを同じサーバーで動かす場合はCSRFトークンは送信されますが、別オリジンにデプロイする場合はCSRFトークンが付与されません。
どうすればよいか
フロントエンドとAPIのドメインを同一サイトにし、SameSiteクッキーで認証すればよいです。例えば、フロントエンドとAPIのドメインを以下のようにします。
サービス | ドメイン |
---|---|
フロントエンド | example.com |
API | api.example.com |
CSRFトークンを使った制限は行わないため、/sanctum/csrf-cookie
へのリクエストは不要になります。また、APIのCSRFトークンを使った保護も無効化します。
class VerifyCsrfToken extends Middleware
{
protected $except = [
'api/*'
];
}
本当にこれで大丈夫なのか?
考えてみれば当たり前だったのですが、SameSiteクッキーを使うとCSRFはほぼ防ぐことができます。なぜなら偽造サイトからAPIにリクエストを送る際に、(偽造サイトが別サイトであれば)セッションIDは送信されないからです。
問題は、例えばテナントごとにドメインが割り振られるようなSaaSの場合です。この場合だと、悪意のある人がevil.example.com
というサイトを作ると、CSRF攻撃が可能になりそうです。ただ今回はこれには当てはまらなかったので、問題ないと判断しました。
感想
分からない単語がたくさん出てきて難しかったですが、今まで避けてきたことが少し分かってうれしかったです。
参考
-
これで完璧!今さら振り返る CSRF 対策と同一オリジンポリシーの基礎 - Qiita
- CSRF対策について体系的にまとめられています。ここから分からない用語を拾って理解していきました。
-
SPAのログイン認証のベストプラクティスがわからなかったのでわりと網羅的に研究してみた〜JWT or Session どっち?〜 - Qiita
- 一番最後の「Sessionを用い、SessionのCookieはSameSiteCookie」に該当します
- SameSite 属性を使った Cookie のセキュアな運用を考える - 30歳からのプログラミング
- same-site/cross-site, same-origin/cross-originをちゃんと理解する
Discussion