👏

LaravelのBreezeを使用したSPA開発で躓いたとこ 419エラー

2023/03/06に公開

LaravelのBreezeを使用したSPA開発で躓いたところ

今回、フロントをNext.js、バックをLaravelで開発を進めました
ちなみに、本番環境はフロントをVercel、バックをさくらレンタルサーバーです
 →最終的にここが問題だった
開発中に躓いたところがあったため、備忘録として残します
結構バカなことをしていたので、この時点でわかる方は分かると思いますww

LaravelのBreezeを使用したSPA認証をしようとした

CORSの設定をきちんとやって非同期通信がフロント側から無事にできるようになって、認証周りの開発すすめたところ壁が立ちはだかりました
まずローカルで開発を進め、認証周りの処理が問題なく動きました
新規登録もできるし、ログイン、ログアウトもできる!

「よし、本番環境でも動くか一応確認しとくか!」

フロントをVercelに、バックをさくらレンタルサーバーに置きました
そして認証周りの通信を行うとこのエラーが、、、

419エラー!?
初めて出会ったエラーでした。

419エラーとは・・・

400–499はクライアントエラー
つまり、クライアント側からのレスポンスの送信が何かおかしいよ?っていう場合に帰ってくるコード
Laravelではcsrf関連で何かしら正しくない挙動をした場合にこのエラーコードを返すようになっている
より正確には...
"VerifyCsrfToken.php"のメソッドにてthrowされている例外がそれに当たります

CSRF関連がおかしい

CSRF関連がおかしいとのことなので、いろいろ調べてやってみました

やってみたこと

調べたかんじではCookieのトークンの受け渡しができていない。CSRFの設定がおかしい。

Laravel側のconfig/session.phpをいじった。secureとhttp_onlyをtrueに設定した

.envのSESSION_SECURE_COOKIEをtrueで追記した(session.phpでenvファイルを参照しているから)
https://zenn.dev/funayamateppei/articles/497f25bbc8623d

Laravel側の.envファイルにSESSION_DOMAINとSANCTUM_STATEFUL_DOMAINSを追記してフロント側のドメインを指定した

https://独自のもの.vercel.app にフロント側をデプロイしているので、独自のもの.vercel.appを両方とも指定した

  • Laravelのセッションクッキーがフロントエンドのドメインに対して有効になる
  • Laravel Sanctumは、フロントエンドとバックエンドの両方を同じドメインでホストしている場合、自動的にCSRFトークンと認証を処理する。しかし、フロントエンドとバックエンドが異なるドメインでホストされている場合、SANCTUM_STATEFUL_DOMAINSを使用して、セッションの状態を維持するドメインを指定する必要がある
SESSION_DOMAIN=独自のもの.vercel.app
SANCTUM_STATEFUL_DOMAINS=独自のもの.vercel.app
SESSION_SECURE_COOKIE=true

config/cors.phpのsupports_credentialsをtrueにした

クロスオリジンリクエストの場合でもクライアントからサーバーに認証情報(CookieやHTTP認証)が自動的に付与されるように

フロント側もaxiosの設定でwithCredentialsをtrueにした

クロスオリジンリクエストの場合でもクライアントからサーバーに認証情報(CookieやHTTP認証)が自動的に付与されるように
lib/axios.js

import Axios from 'axios'

const axios = Axios.create({
    baseURL: process.env.NEXT_PUBLIC_BACKEND_URL,
    headers: {
        'X-Requested-With': 'XMLHttpRequest',
    },
    withCredentials: true,
})

export default axios

same_siteを一応noneに変更してみる

https://zenn.dev/funayamateppei/articles/bd21f6e0abc453

これで動くだろうと動かしてみる

変わらず419エラー ざけんな!!

コードを変更しまくったけど、なんで動かないの?
CORS対策も、CSRF対策もやったはずなのに、、、
そこからよく理解もせずに便利だからと使っていたBreezeについて調べてみた

まずCookieを発行しているところはどこやねん→Sanctum

Breezeをインストールしたときに一緒に入るSanctum
このSanctumには2種類の認証方法がある

  • APIトークン認証(Cookieは使用しない)
  • SPA認証(Cookieを使用する)
    今回使用していたのは、SPA開発をしていたのでもちろんSPA認証
    APIトークン認証はSPA開発ではセキュリティ面で非推奨みたいです

そして、このSPA認証に問題が、、、

SPA認証はトップドメインがフロントとバック同じじゃないとダメ

トップドメインとは?

今回デプロイした先はVercelさくらレンタルサーバー
Vercel
~~.vercel.app
さくらレンタルサーバー
~~.sakura.ne.jp

ここがまずおかしかった!!

Sanctumのドキュメントにも優しく書いていただいていました

これが原因でCookieが発行されず419エラーを吐いていたのがわかりました
Cookieの受け渡しの問題ではなかったみたいです

異なるオリジンへのリクエストの場合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トークンが付与されません。

まとめ

SPA認証をするときはトップレベルドメインは同じじゃないとダメ!!
Breezeを理解せずに便利だからといって使っていたのがバカでした。
同じようなバカをした人でこの記事に行き着いてしまった人に一言。

ちゃんと使うものは理解して使いましょう

Discussion