Pengguna lupa password adalah kejadian yang pasti datang di aplikasi apa pun. Kita tidak bisa mengubah password secara manual satu per satu lewat database, jadi kita butuh alur otomatis: pengguna memasukkan email, sistem mengirim link berisi token rahasia, lalu pengguna memilih password baru melalui halaman khusus. Laravel membungkus seluruh alur ini dalam password broker — fasad Password — yang menangani pembuatan token, pengiriman email, validasi, sampai pembersihan token setelah dipakai. Kita tinggal menyediakan route, controller, dan callback penyimpanan password baru.
Tabel password_reset_tokens #
Broker password menyimpan token di tabel password_reset_tokens. Kabar baiknya, tabel ini sudah dibuatkan secara bawaan oleh migrasi awal Laravel di database/migrations/0001_01_01_000000_create_users_table.php:
Schema::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});Kolom email menjadi primary key, artinya satu alamat email hanya boleh punya satu token aktif — permintaan reset baru akan menimpa token lama. Kolom token menyimpan versi ter-hash dari token (kita bahas sebentar lagi), dan created_at dipakai untuk menandai kedaluwarsa. Masa berlaku token diatur di config/auth.php pada entri passwords.users.expire yang bernilai 60 menit secara bawaan.
Mengirim Link Reset dengan Password Broker #
Kita mulai dari dua route: formulir lupa password dan aksi pengiriman link. Perhatikan bahwa route diarahkan ke controller terpisah agar logika reset tidak menumpuk di controller login:
use App\Http\Controllers\PasswordResetController;
Route::get('/lupa-password', [PasswordResetController::class, 'formLupa'])
->name('password.request')->middleware('guest');
Route::post('/lupa-password', [PasswordResetController::class, 'kirimLink'])
->name('password.email')->middleware('guest');Method kirimLink() tinggal membungkus satu pemanggilan broker:
public function kirimLink(Request $request)
{
$request->validate([
'email' => ['required', 'email'],
]);
$status = Password::sendResetLink(
$request->only('email')
);
return $status === Password::RESET_LINK_SENT
? back()->with(['status' => __($status)])
: back()->withErrors(['email' => __($status)]);
}Password::sendResetLink() mengembalikan string status, bukan boolean. Kita membandingkan hasilnya dengan konstanta Password::RESET_LINK_SENT untuk memutuskan response-nya. Berikut konstanta yang paling sering kita jumpai:
| Konstanta | Kondisi | Pesan bawaan (lang/en/passwords.php) |
|---|---|---|
RESET_LINK_SENT | Link berhasil dikirim | We have emailed your password reset link. |
PASSWORD_RESET | Password berhasil diganti | Your password has been reset. |
INVALID_TOKEN | Token salah atau kedaluwarsa | This password reset token is invalid. |
INVALID_USER | Email tidak terdaftar | We can't find a user with that email address. |
Di balik layar, broker membuat token acak 64 karakter, menyimpan versi ter-hash-nya ke tabel password_reset_tokens, lalu memanggil sendPasswordResetNotification() pada model User — yang secara bawaan mengirim notifikasi Illuminate\Auth\Notifications\ResetPassword berisi link ke route bernama password.reset. Sandbox latihan memakai MAIL_MAILER=log, jadi email sungguhan ditulis ke file log; di feature test kita pakai Notification::fake(). Inilah isi email yang benar-benar dihasilkan notifikasi tersebut (token kita bekukan agar tampilannya deterministik):
Hello!
You are receiving this email because we received a password reset request for your account.
Reset Password
This password reset link will expire in 60 minutes.
If you did not request a password reset, no further action is required.
Regards,
Laravel
If you're having trouble clicking the "Reset Password" button, copy and paste the URL below
into your web browser: http://localhost:8000/reset-password/token-demo-tetap-1234567890?email=budi%40example.comMenyelesaikan Reset dengan Password::reset() #
Halaman reset menerima token dari URL, lalu formulir mengirim token bersama password baru:
Route::get('/reset-password/{token}', [PasswordResetController::class, 'formReset'])
->name('password.reset')->middleware('guest');
Route::post('/reset-password', [PasswordResetController::class, 'reset'])
->name('password.update')->middleware('guest');Method reset() memvalidasi input lalu memanggil Password::reset() dengan dua argumen: kredensial dari request dan callback yang mengerjakan pembaruan:
public function reset(Request $request)
{
$request->validate([
'token' => ['required'],
'email' => ['required', 'email'],
'password' => ['required', 'confirmed', 'min:8'],
]);
$status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function (User $user, string $password) {
$user->forceFill([
'password' => Hash::make($password),
'remember_token' => Str::random(60),
])->save();
event(new PasswordReset($user));
}
);
return $status === Password::PASSWORD_RESET
? redirect()->route('login')->with('status', __($status))
: back()->withErrors(['email' => [__($status)]]);
}Callback dijalankan hanya bila token valid dan belum kedaluwarsa. Di dalamnya kita melakukan tiga hal: meng-hash password baru dengan Hash::make(), mengganti remember_token agar cookie remember me di perangkat lain tidak lagi berlaku, dan melepas event PasswordReset (misalnya untuk mengirim notifikasi ke pengguna). Setelah callback sukses, broker otomatis menghapus baris token dari database.
Feature Test Alur Lengkap #
Berikut test yang menuntaskan ketiga tahap — minta link, reset, login — dalam satu method. Notification::fake() mencegat email, dan kita menangkap token mentah langsung dari notifikasi yang terkirim:
public function test_alur_lengkap_reset_password(): void
{
$user = User::factory()->create([
'name' => 'Budi',
'email' => '[email protected]',
'password' => 'rahasia-lama-123',
]);
Notification::fake();
// Langkah 1: minta link reset
$this->post('/lupa-password', ['email' => '[email protected]']);
Notification::assertSentTo(
[$user],
ResetPasswordNotification::class,
function (ResetPasswordNotification $notification) use (&$rawToken) {
$rawToken = $notification->token;
return true;
}
);
// Langkah 2: reset dengan token mentah dari email
$this->post('/reset-password', [
'token' => $rawToken,
'email' => '[email protected]',
'password' => 'rahasia-baru-456',
'password_confirmation' => 'rahasia-baru-456',
]);
$user->refresh();
// Langkah 3: login dengan password baru
$this->post('/login', [
'email' => '[email protected]',
'password' => 'rahasia-baru-456',
]);
$this->assertAuthenticatedAs($user);
}Output yang Dihasilkan:
langkah1: redirect=302, notifikasi=kirim, panjang_token=64, token_db_panjang=60
langkah2: redirect=302, password_baru_valid=ya, password_lama_valid=tidak, token_terhapus=ya
langkah3: login_baru=suksesTest penolakan juga layak ada. Token palsu menghasilkan error validasi pada field email tanpa menyentuh password siapa pun:
token_salah: password_terubah=tidak
email_hantu: status=302, error=["We can't find a user with that email address."]Dengan alur reset berjalan, masalah berikutnya muncul: bagaimana memastikan email yang didaftarkan benar-benar milik pengguna? Itu pekerjaan verifikasi email, yang akan kita bangun di chapter berikutnya. Selengkapnya bisa dipelajari di dokumentasi Password Reset .