Banyak aplikasi bisnis yang berjalan bertahun-tahun ternyata masih menyimpan password dengan MD5, atau lebih buruk lagi, dalam bentuk teks biasa. Selama database tidak bocor, semuanya terasa baik-baik saja. Begitu ada backup yang tertinggal di hosting atau celah SQL injection, seluruh password pengguna bisa dibaca dalam hitungan menit. Karena banyak orang memakai password yang sama di beberapa tempat, kebocoran itu juga membuka akun email dan media sosial mereka.
Artikel ini membahas cara menyimpan password dengan benar, memigrasikan password lama tanpa memaksa semua pengguna reset, serta mengamankan alur login, logout, dan reset password di Laravel 11.
Kenapa MD5 dan SHA1 Tidak Cocok untuk Password
MD5 dan SHA1 dirancang untuk cepat. Untuk checksum file, itu kelebihan. Untuk password, itu kelemahan, karena kartu grafis biasa bisa mencoba miliaran tebakan MD5 per detik. Ditambah lagi, hash MD5 untuk password umum seperti 123456 sudah tersedia di tabel siap pakai.
| Cara menyimpan | Penilaian | Alasan |
|---|---|---|
| Teks biasa | Tidak boleh | Siapa pun yang membaca database langsung tahu password |
| MD5 / SHA1 / SHA256 tanpa salt | Tidak boleh | Terlalu cepat dan rentan tabel hash siap pakai |
| Enkripsi (bisa didekripsi) | Tidak boleh | Jika kunci bocor, semua password terbuka |
| bcrypt / Argon2id | Disarankan | Sengaja lambat, memakai salt acak per password |
Langkah 1: Hashing dengan Benar di Laravel
Laravel memakai bcrypt secara default. Di Laravel 11, model User bawaan sudah memiliki cast 'password' => 'hashed', sehingga password polos yang diisi ke model otomatis di-hash:
// app/Models/User.php (bawaan Laravel 11)
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
// Saat registrasi — cukup isi password polos
User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => $data['password'],
]);
// Saat memeriksa password secara manual
if (Hash::check($request->password, $user->password)) {
// cocok
}
Tingkat kerumitan bcrypt diatur lewat BCRYPT_ROUNDS di .env (default 12). Laravel 11 juga otomatis memperbarui hash saat pengguna login bila nilai rounds dinaikkan, jadi Anda tidak perlu memigrasikannya manual.
Langkah 2: Migrasi Password MD5 Lama Tanpa Reset Massal
Jika Anda mewarisi aplikasi lama yang menyimpan MD5, memaksa semua pengguna reset password biasanya memicu banyak keluhan. Cara yang lebih halus adalah memigrasikan hash saat pengguna berhasil login:
public function store(Request $request)
{
$credentials = $request->validate([
'email' => ['required', 'email'],
'password' => ['required', 'string'],
]);
$user = User::where('email', $credentials['email'])->first();
// Hash MD5 selalu 32 karakter heksadesimal; hash bcrypt diawali $2y$
if ($user && preg_match('/^[a-f0-9]{32}$/', $user->password)
&& hash_equals($user->password, md5($credentials['password']))) {
$user->forceFill(['password' => $credentials['password']])->save(); // di-hash ulang oleh cast
}
if (! Auth::attempt($credentials, $request->boolean('remember'))) {
return back()->withErrors(['email' => 'Email atau password salah.'])->onlyInput('email');
}
$request->session()->regenerate();
return redirect()->intended('/dashboard');
}
Setelah beberapa bulan, cek berapa akun yang masih memakai MD5 dengan query SELECT COUNT(*) FROM users WHERE password NOT LIKE '$2y$%'. Akun yang tersisa biasanya sudah tidak aktif; kirimkan email reset password kepada mereka dan hapus kode migrasi di atas.
Langkah 3: Aturan Password yang Masuk Akal
Aturan yang terlalu rumit membuat pengguna menulis password di kertas, sedangkan aturan yang terlalu longgar mengizinkan 12345678. Laravel menyediakan rule Password yang bisa didefinisikan sekali untuk seluruh aplikasi:
// app/Providers/AppServiceProvider.php
use Illuminate\Validation\Rules\Password;
public function boot(): void
{
Password::defaults(function () {
return $this->app->isProduction()
? Password::min(10)->letters()->numbers()->uncompromised()
: Password::min(8);
});
}
// Di form registrasi atau ganti password
'password' => ['required', 'confirmed', Password::defaults()],
Aturan uncompromised() mengecek apakah password pernah muncul di kebocoran data publik lewat layanan Have I Been Pwned. Yang dikirim hanya potongan awal hash-nya (metode k-anonymity), bukan password itu sendiri. Karena butuh koneksi internet, aktifkan hanya di produksi seperti contoh di atas.
Langkah 4: Amankan Proses Login
- Pesan error yang sama. Tampilkan "Email atau password salah" untuk kedua kasus, jangan "Email tidak terdaftar". Pesan yang berbeda memberi tahu penyerang email mana yang terdaftar.
- Regenerasi session setelah login. Panggilan
$request->session()->regenerate()pada contoh di atas mencegah session fixation, yaitu penyerang yang sudah menyiapkan ID session sebelum korban login. - Batasi percobaan login. Gunakan rate limiter seperti dijelaskan di checklist keamanan Laravel, misalnya lima percobaan per menit per kombinasi email dan IP.
Langkah 5: Logout yang Benar-Benar Mengakhiri Session
public function destroy(Request $request)
{
Auth::guard('web')->logout();
$request->session()->invalidate(); // hapus data session lama
$request->session()->regenerateToken(); // token CSRF baru
return redirect('/');
}
Untuk fitur "keluar dari semua perangkat lain" setelah ganti password, aktifkan middleware AuthenticateSession pada route yang login, lalu panggil Auth::logoutOtherDevices($request->password).
Langkah 6: Reset Password yang Aman
Fitur reset bawaan Laravel (dipakai oleh Breeze dan Fortify) sudah menangani hal-hal penting: token acak sekali pakai, masa berlaku token, dan penyimpanan token dalam bentuk hash. Pastikan konfigurasinya masuk akal:
// config/auth.php
'passwords' => [
'users' => [
'provider' => 'users',
'table' => 'password_reset_tokens',
'expire' => 60, // menit; jangan dibuat berhari-hari
'throttle' => 60, // detik jeda sebelum boleh meminta tautan baru
],
],
Pada halaman "lupa password", tampilkan pesan yang sama apa pun hasilnya, misalnya "Jika email terdaftar, tautan reset sudah kami kirim". Setelah password berhasil diganti, kirim email pemberitahuan ke pemilik akun agar ia sadar bila perubahan itu bukan dilakukannya.
Langkah 7: Pertimbangkan Verifikasi Dua Langkah (2FA)
Untuk akun admin, kasir kepala, atau siapa pun yang bisa melihat data keuangan, password saja tidak cukup. Laravel Fortify menyediakan 2FA berbasis aplikasi authenticator (TOTP) seperti Google Authenticator, lengkap dengan kode pemulihan. Jika belum memungkinkan, minimal wajibkan 2FA untuk akun dengan peran tertinggi.
Checklist Autentikasi
| Item | Cara mengecek |
|---|---|
| Password disimpan dengan bcrypt/Argon2 | Kolom password diawali $2y$ atau $argon2id$ |
| Tidak ada sisa hash MD5 | SELECT COUNT(*) FROM users WHERE password NOT LIKE '$2%' AND password NOT LIKE '$argon%' |
| Aturan password diterapkan | Coba daftar dengan 12345678: harus ditolak |
| Pesan login tidak membedakan email dan password | Login dengan email tidak terdaftar dan password salah: pesannya sama |
| Session diregenerasi saat login | Bandingkan cookie session sebelum dan sesudah login: harus berubah |
| Rate limit login aktif | Enam kali salah dalam semenit: harus muncul 429 |
| Token reset kedaluwarsa | Nilai expire di config/auth.php tidak lebih dari 60 menit |
Keamanan login adalah satu bagian dari gambaran yang lebih besar. Untuk aplikasi yang juga menyediakan API, lanjutkan ke panduan keamanan API yang baik, dan untuk pengaturan peran pengguna lihat role dan permission dengan Spatie.