🍔
【Laravel】userのemailがuniqueでない場合はPasswordファサードによるパスワードリセットはできないッ!!
環境
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.php
のsendResetLink()
やreset()
を追ってみてください)
Passwordファサードを使えないならどうすれば良いの?
- まずはトークン用のテーブルを作り直します。
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 | |
+------------+-----------------+------+-----+---------+----------------+
- 続いてモデルにリレーションを貼ります。
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);
}
- それからパスワードリセットの処理を自作します。
/**
* パスワードリセットメールの送信処理
*
* @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
を実行すれば削除できます。
参考
Discussion