Laravel は PHP のフレームワークで web サイトの構築によく用いられます。
web サイトにはしばしば管理者、会員などの認証機能が必要な機能が現れます。Laravel はこの認証機能をすぐに作れるにしており、用意されているものの中にはパスワードリセット機能もあります。
このパスワードリセット機能は便利なのですが、ままある無効なパスワードリセットリンクは 404 エラーにしたい、トークンが無効であることをすぐに通知したい、というった要望に対応するのが難しいです。これはデータベースの流出時の被害を抑えるための Laravel の用意したハッシュ化の仕組みが原因です。
パスワードリセットリンクを構築するトークンレコードはデータベース内では次の様に保存されています。
email,token,created_at account@example.com,$2y$10$IVA16XOYn2HXDot02ZwUk.hNsAYhH62KzYeIRilMgcZmgRBtQw5y.,2022-03-15 18:10:02
そしてパスワードリセットリンクは次の様にハッシュ化される前の値を用いる前提となっています。
http://localhost/password/reset/0a314119a5b6506d84f175489f1941f83608151d87bdfa46f1a2d3bc08b8b933
必然的にそのままではデータベース内で該当レコードがあるかないかのクエリを作れないため 404 NotFound を適切に返すのが難しくなっています。
\DB::query()->from('password_reset_tokens') ->where('token', '=', /* ハッシュ化済みの値が必要 */)
これの解決方法は少なくとも二つあります。
一つはリンクにハッシュ化済みの値も加える方法です。次の様にデータベース中で保存されている値をリンクに含めると
http://localhost/password/reset/0a314119a5b6506d84f175489f1941f83608151d87bdfa46f1a2d3bc08b8b933?hash=$2y$10$IVA16XOYn2HXDot02ZwUk.hNsAYhH62KzYeIRilMgcZmgRBtQw5y.
次の様に where に入れる値を得られるためレコードの有無がわかり、適切にエラーを返せます。
Route::get('/reset-password/{token}', function ($token) { $hashed = \Request::get('hash'); $exists = \DB::query()->select() ->from('password_reset_tokens') ->where('token', '=', $hashed) ->exists(); if(!$exists){ abort(404);// トークンが存在しない場合 404 エラー } return view('auth.reset-password', ['token' => $token]); })->middleware('guest')->name('password.reset');
この方法の問題点としてパスワードリセットページへの URL が極端に長くなる点があります。
もう一つの方法はテーブル内を総ざらいして一つ一つハッシュをチェックする方法です。これはパスワードリセットページへの URL が短く済みますが、パスワードリセット用のトークンテーブル内のレコード数に比例して実行時間が伸びる問題があります。利用者数が少なく済むことが確約されるサービス(社内ツールなど)であれば問題になりませんが、そうでない場合、実行時間が長くなりがちです。
Route::get('/reset-password/{token}', function ($token) { $exists = \DB::query()->select() ->from('password_reset_tokens') // 寿命期限内に生成されているレコードを全て取得 ->where('created_at', '>=', now()->subMinutes(config('auth.passwords.users.expire'))) ->get() // パスワードリセットトークンのハッシャーの check メソッドを使用 // 設定を変えている場合は設定箇所から呼び出す。デフォルトは \Hash::check で大丈夫 ->filter(fn ($record) => \Hash::check($token, $record->token)) ->isNotEmpty(); if(!$exists){ abort(404);// トークンが存在しない場合 404 エラー } return view('auth.reset-password', ['token' => $token]); })->middleware('guest')->name('password.reset');
一長一短ですがこういった方法でパスワードリセットページを開いた瞬間にエラーを表示できます。