🍔

【Laravel】userのemailがuniqueでない場合はPasswordファサードによるパスワードリセットはできないッ!!

2024/03/31に公開

環境

Laravel 10.42

なぜPasswordファサードでパスワードリセットできないのか

パスワードリセットに使用されるpassword_reset_tokensテーブルを見てみましょう。

mysql> SHOW COLUMNS FROM password_reset_tokens;
+------------+--------------+------+-----+---------+-------+
| Field      | Type         | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+-------+
| email      | varchar(255) | NO   | PRI | NULL    |       |
| token      | varchar(255) | NO   |     | NULL    |       |
| created_at | timestamp    | YES  |     | NULL    |       |
+------------+--------------+------+-----+---------+-------+

emailをprimaryとしており、emailの重複に耐える作りになっていません。

ではユーザーAとユーザーBとユーザーCのemailが重複している状態で、パスワードリセットを行った場合どうなるのでしょう。

  • ユーザーAのパスワードリセット → ユーザーAのパスワードが更新される
  • ユーザーBのパスワードリセット → ユーザーAのパスワードが更新される
  • ユーザーCのパスワードリセット → ユーザーAのパスワードが更新される

idが若いユーザーのパスワードが常に更新されてしまいます。

(内部的な処理はここでは解説しませんので、気になった方は vendor/laravel/framework/src/Illuminate/Auth/Passwords/PasswordBroker.phpsendResetLink()reset()を追ってみてください)

Passwordファサードを使えないならどうすれば良いの?

  1. まずはトークン用のテーブルを作り直します。
database/migrations/2014_10_12_100000_create_password_reset_tokens_table.php
public function up(): void
{
    Schema::create('password_reset_tokens', function (Blueprint $table) {
        $table->id();
        $table->foreignIdFor(User::class)->unique();
        $table->string('token');
        $table->timestamp('created_at')->nullable();
    });
}
mysql> SHOW COLUMNS FROM password_reset_tokens;
+------------+-----------------+------+-----+---------+----------------+
| Field      | Type            | Null | Key | Default | Extra          |
+------------+-----------------+------+-----+---------+----------------+
| id         | bigint unsigned | NO   | PRI | NULL    | auto_increment |
| user_id    | bigint unsigned | NO   | UNI | NULL    |                |
| token      | varchar(255)    | NO   |     | NULL    |                |
| created_at | timestamp       | YES  |     | NULL    |                |
+------------+-----------------+------+-----+---------+----------------+
  1. 続いてモデルにリレーションを貼ります。
app/Models/User.php
public function password_reset_token(): HasOne
{
    return $this->hasOne(PasswordResetToken::class);
}
app/Models/PasswordResetToken.php
public function user(): BelongsTo
{
    return $this->belongsTo(User::class);
}
  1. それからパスワードリセットの処理を自作します。
/**
 * パスワードリセットメールの送信処理
 *
 * @param ResetPasswordEmailRequest $request
 * @return RedirectResponse
 */
public function email(ResetPasswordEmailRequest $request): RedirectResponse
{
    $loginId = $request->getLoginId();
    $token = generateToken();

    $user = User::whereLoginId($loginId)->firstOrFail();
    PasswordResetToken::updateOrCreate(['user_id' => $user->id], [
        'token' => $token,
        'created_at' => Carbon::now(),
    ]);

    $user->notify(new ResetPasswordNotification($token, $user->id));

    return to_route('password.request')->with(['success_message' => __('passwords.sent')]);
}
/**
 * パスワードのリセット処理
 *
 * @param ResetPasswordUpdateRequest $request
 * @return RedirectResponse
 */
public function update(ResetPasswordUpdateRequest $request): RedirectResponse
{
    $userId = $request->getUserId();
    $token = $request->getToken();
    $password = $request->getPassword();

    $user = User::findOrFail($userId);
    $passwordResetToken = $user->password_reset_token;

    if ($passwordResetToken === null || $passwordResetToken->token !== $token) {
        return back()->withErrors(['error_message' => __('passwords.token')]);
    }

    $createdAt = $passwordResetToken->created_at;
    if ($createdAt->addMinutes(config('auth.passwords.users.expire'))->isPast()) {
        return back()->withErrors(['error_message' => __('passwords.expired')]);
    }

    User::whereId($user->id)->update([
        'password' => Hash::make($password),
    ]);

    PasswordResetToken::destroy($passwordResetToken->id);

    return to_route('login')->with(['success_message' => __('passwords.reset')]);
}

以上です。

おまけ

有効期限が切れたトークンは php artisan auth:clear-resets を実行すれば削除できます。

参考

https://readouble.com/laravel/10.x/ja/passwords.html

GitHubで編集を提案

Discussion