tohokuaikiのチラシの裏

技術的ネタとか。

Laravel Breeze(Inertia.jsは+React)でユーザーのパスワード変更をしたらTokenがマッチしなくなった件

環境

  • Laravel11 + Breeze + Inertia.js + React でユーザーログイン周りのScaffoldを作った。
  • 操作は全てReactからのAPIコール
  • AuthはStatefulAPIを使って、Sanctumで行う。RouteにミドルウェアRoute::middleware('auth:sanctum')って感じ。
  • ログイン後にTokenを取得してCOOKIEにセットしている。
    useEffect(() => {
        (async () => {
            await fetch(route('sanctum.csrf-cookie'))
        })();
    }, []);
  • axiosでAjaxする前に、COOKIEのTokenをHTTPヘッダに忍ばせている。
    const [cookies, setCookie, removeCookie] = useCookies(['XSRF-TOKEN']);
    axios.interceptors.request.use(config => {
        config.headers!['X-XSRF-TOKEN'] = cookies['XSRF-TOKEN']
        return config;
    }, error => {
        return Promise.reject(error)
    })

再現方法

  1. ログインする。
  2. sanctumで保護されているAPIを叩くページに移動しても問題ない。
  3. プロフィールページ http://localhost:8000/profileからパスワードを変更
  4. sanctumで保護されているAPIを叩くページに移動すると、401 unauthorizedを返される。

理由

セッションTokenとブラウザから送られるTokenが一致していない。

tokensMatchメソッドでセッションToken$request->session()->token() とRequestからのToken $this->getTokenFromRequest($request); が違うのでfalseを返す。

<?php
    protected function tokensMatch($request)
    {
        $token = $this->getTokenFromRequest($request);

        return is_string($request->session()->token()) &&
               is_string($token) &&
               hash_equals($request->session()->token(), $token);
    }

セッション側がプロフィールページでパスワードを変更した際に更新されているのにRequestトークンがそれに追いついていない。ちなみに、Requestトークンは X-CSRF-TOKEN または X-XSRF-TOKEN ヘッダを見ている。

理由その2

なぜ一致しないかというと、PasswordControllerが成功時に back() をレスポンスしている。…からじゃないかな?

<?php
class PasswordController extends Controller
{
    /**
     * Update the user's password.
     */
    public function update(Request $request): RedirectResponse
    {
        $validated = $request->validate([
            'current_password' => ['required', 'current_password'],
            'password' => ['required', Password::defaults(), 'confirmed'],
        ]);

        $request->user()->update([
            'password' => Hash::make($validated['password']),
        ]);

        return back();
    }
}

解決方法(失敗)

  • AjaxAPI的に使っているので、back()を使わないようにする。
  • Jsonを返して処理するだけにしたいけど、resources/js/Pages/Profile/Partials/UpdatePasswordForm.tsx は ControllerがInertia::render() を返してくるのを期待しているので使わないようにする。自前でaxiosで処理する。

…と思ってたら、やっぱり駄目だった。

どうも、passwordにUPDATEをかけると何かLaravelがセッションかTokenの処理をするっぽい。nameの更新だけだと問題なかった。

ということで、Passwordまわりで何か特別な処理をしていないかを探る。

src/vendor/laravel/framework/src/Illuminate/Session/Middleware/AuthenticateSession.phppublic function handle($request, Closure $next)なんかこのあたりでやってそうだなー。

もう面倒だから、「パスワード変更したら再ログイン必要」ってことにしちゃおう。