Laravel Password Reset Aman tidak cukup hanya membuat fitur “lupa password” berfungsi. Alur ini sering menjadi target user enumeration, brute force, penyalahgunaan email flood, kebocoran token, dan replay token yang sudah pernah dipakai.
Tujuan implementasi yang aman adalah: selalu memberi respons generik, membatasi request, menggunakan token reset yang kedaluwarsa dan tidak bisa dipakai ulang, serta mengirim email secara andal tanpa membebani request utama. Di Laravel modern, sebagian fondasi ini sudah tersedia, tetapi tetap perlu dikonfigurasi dan dibungkus dengan pola yang benar.
Threat model singkat: apa yang harus dicegah?
1. User enumeration
Penyerang mencoba mengetahui apakah suatu email terdaftar dengan melihat perbedaan respons, waktu respons, atau perilaku sistem. Contoh buruk:
- “Email tidak ditemukan” untuk email yang tidak ada.
- “Link reset telah dikirim” hanya untuk email yang valid.
- Perbedaan waktu respons yang mencolok antara email valid dan tidak valid.
Solusinya adalah respons generik yang konsisten. Pengguna selalu menerima pesan yang sama, misalnya: “Jika email terdaftar, kami akan mengirimkan tautan reset password.”
2. Brute force dan flood request
Endpoint reset password bisa disalahgunakan untuk:
- Membanjiri inbox korban dengan email reset.
- Mencoba token reset berulang kali.
- Menghabiskan resource aplikasi.
Solusinya adalah rate limiting di endpoint permintaan reset dan endpoint submit password baru, idealnya berbasis kombinasi IP dan email yang dinormalisasi.
3. Token leakage
Token dapat bocor melalui:
- Log yang menyimpan query string mentah.
- Referer header jika halaman reset memuat resource pihak ketiga.
- Screenshot, histori browser, atau email forwarding.
Karena itu, token harus berumur pendek, disimpan dengan aman, dan segera diinvalidasi setelah dipakai.
4. Replay token
Jika token yang sama masih valid setelah reset berhasil, penyerang yang sempat memperoleh token bisa mengganti password lagi. Solusinya adalah sekali pakai dan hapus/invalidate token lama saat token baru dibuat atau reset berhasil.
Prinsip implementasi aman di Laravel
- Jangan bocorkan status akun lewat isi respons.
- Terapkan throttle pada endpoint request reset dan submit reset.
- Gunakan broker password Laravel untuk token reset dan expiry.
- Pastikan token kedaluwarsa dalam waktu wajar.
- Invalidasi token lama saat membuat token baru.
- Jangan log token mentah.
- Queue email agar request tetap cepat dan stabil.
- Catat audit log minimal tanpa menyimpan data sensitif berlebihan.
Konfigurasi broker password
Laravel umumnya menggunakan broker password untuk mengatur penyimpanan token reset dan waktu kedaluwarsanya. Pastikan konfigurasi broker Anda mengarah ke tabel token reset yang benar dan memiliki masa berlaku yang masuk akal.
// config/auth.php (contoh struktur umum)
'passwords' => [
'users' => [
'provider' => 'users',
'table' => 'password_reset_tokens',
'expire' => 60,
'throttle' => 60,
],
],Makna praktisnya:
- expire: token reset hanya valid dalam beberapa menit tertentu.
- throttle: mencegah pembuatan token reset berulang terlalu cepat untuk akun yang sama.
Nilai yang tepat tergantung kebutuhan aplikasi, tetapi umumnya tidak perlu terlalu lama. Semakin lama token hidup, semakin besar jendela risiko jika token bocor.
Catatan: detail internal penyimpanan token dapat berbeda antar versi Laravel, tetapi praktik amannya tetap sama: gunakan fasilitas broker bawaan, jangan menyimpan token mentah sembarangan, dan pastikan ada expiry.
Route dan throttle yang relevan
Pisahkan endpoint untuk meminta link reset dan untuk mengirim password baru. Terapkan throttle pada keduanya.
use App\Http\Controllers\Auth\ForgotPasswordController;
use App\Http\Controllers\Auth\ResetPasswordController;
use Illuminate\Support\Facades\Route;
Route::middleware('guest')->group(function () {
Route::post('/forgot-password', [ForgotPasswordController::class, 'store'])
->middleware('throttle:password-reset-request');
Route::post('/reset-password', [ResetPasswordController::class, 'store'])
->middleware('throttle:password-reset-submit');
});Daripada memakai angka throttle generik langsung di route, lebih baik definisikan limiter bernama agar logikanya jelas dan mudah diubah.
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
public function boot(): void
{
RateLimiter::for('password-reset-request', function (Request $request) {
$email = mb_strtolower((string) $request->input('email'));
return [
Limit::perMinute(5)->by($request->ip()),
Limit::perHour(3)->by('pwd-reset:email:' . sha1($email)),
];
});
RateLimiter::for('password-reset-submit', function (Request $request) {
$email = mb_strtolower((string) $request->input('email'));
return [
Limit::perMinute(10)->by($request->ip()),
Limit::perMinute(5)->by('pwd-submit:' . sha1($email)),
];
});
}Pendekatan ini membantu dalam dua arah:
- Membatasi serangan dari satu IP.
- Membatasi spam ke satu email meskipun IP berubah-ubah.
Jika aplikasi berjalan di banyak instance, gunakan backend cache terpusat seperti Redis agar rate limit konsisten di semua node.
Form request: validasi tanpa bocor
Validasi tetap diperlukan, tetapi jangan menjadikan error sebagai kanal enumeration. Untuk endpoint request reset, validasi cukup memeriksa format email, bukan keberadaan email di database.
namespace App\Http\Requests\Auth;
use Illuminate\Foundation\Http\FormRequest;
class ForgotPasswordRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'email' => ['required', 'email:rfc'],
];
}
}
Hindari aturan seperti exists:users,email pada form ini, karena itu bisa memperjelas perbedaan antara akun yang ada dan tidak ada.
Untuk submit reset password, validasi fokus pada data yang memang diperlukan:
namespace App\Http\Requests\Auth;
use Illuminate\Foundation\Http\FormRequest;
class ResetPasswordRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'token' => ['required', 'string'],
'email' => ['required', 'email:rfc'],
'password' => ['required', 'string', 'confirmed', 'min:12'],
];
}
}Jika aplikasi memiliki kebijakan password yang lebih ketat, gunakan aturan password yang sesuai. Yang penting, jangan mengorbankan keamanan alur reset demi pesan error yang terlalu spesifik.
Controller request reset dengan respons generik
Bagian paling penting untuk anti-enumeration adalah selalu mengembalikan respons yang sama, terlepas dari email tersebut ada atau tidak.
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\ForgotPasswordRequest;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Facades\Log;
class ForgotPasswordController extends Controller
{
public function store(ForgotPasswordRequest $request)
{
$email = mb_strtolower($request->string('email')->toString());
$status = Password::broker()->sendResetLink([
'email' => $email,
]);
Log::info('password_reset_requested', [
'email_hash' => sha1($email),
'ip' => $request->ip(),
'status' => $status,
]);
return response()->json([
'message' => 'Jika email terdaftar, kami akan mengirimkan tautan reset password.',
]);
}
}Beberapa catatan penting:
- Normalisasi email membantu konsistensi lookup dan rate limit.
- Jangan kembalikan status broker ke client, karena itu bisa membuka enumeration.
- Audit log cukup hash email, bukan email mentah, jika memang tidak perlu.
Apakah logging status broker aman? Umumnya aman untuk internal observability selama akses log dibatasi. Namun jangan log token reset, URL reset lengkap, atau password baru.
Controller submit reset password
Pada tahap ini, kita perlu memvalidasi token, mengubah password, memutar remember token, dan memastikan token tidak bisa digunakan ulang.
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\ResetPasswordRequest;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
class ResetPasswordController extends Controller
{
public function store(ResetPasswordRequest $request)
{
$status = Password::broker()->reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function ($user, string $password) {
$user->forceFill([
'password' => Hash::make($password),
'remember_token' => Str::random(60),
])->save();
event(new PasswordReset($user));
}
);
if ($status !== Password::PASSWORD_RESET) {
return response()->json([
'message' => 'Token reset password tidak valid atau sudah kedaluwarsa.',
], 422);
}
return response()->json([
'message' => 'Password berhasil direset. Silakan login kembali.',
]);
}
}Mengapa remember_token perlu diputar? Untuk mengurangi risiko sesi atau mekanisme “remember me” lama tetap relevan setelah perubahan password. Jika aplikasi Anda juga memakai session backend, pertimbangkan invalidasi sesi aktif sesuai kebutuhan kebijakan keamanan.
Token yang aman: expiry, invalidasi, dan penyimpanan
Expiry token
Token reset harus memiliki masa hidup pendek. Tidak ada angka universal untuk semua sistem, tetapi masa hidup yang terlalu panjang memperbesar dampak token leakage. Jika pengguna sering terlambat membuka email, lebih baik optimalkan pengiriman email atau UI untuk meminta token baru, bukan memperpanjang umur token berlebihan.
Invalidasi token lama
Praktik aman adalah hanya mengizinkan token terbaru atau memastikan token lama tidak relevan lagi ketika token baru diterbitkan. Broker password Laravel dirancang untuk mengelola token reset dengan lifecycle yang aman; gunakan mekanisme bawaan alih-alih membuat tabel token kustom tanpa kebutuhan jelas.
Penyimpanan token
Prinsip yang benar adalah jangan memperlakukan token reset seperti data biasa. Token harus disimpan dengan aman dan diverifikasi tanpa mengekspos token mentah ke log atau sistem lain. Jika Anda tetap membuat implementasi kustom, pastikan token:
- Dihasilkan dengan sumber acak yang kuat.
- Disimpan dalam bentuk yang aman, bukan plaintext yang mudah dibaca operator atau dump database.
- Dibandingkan secara aman saat verifikasi.
- Dihapus setelah dipakai atau saat kedaluwarsa.
Untuk kebanyakan kasus, lebih aman memakai broker bawaan Laravel daripada membangun sendiri.
Email notifikasi dan queue
Pengiriman email reset sebaiknya tidak memblokir request utama. Jika SMTP lambat atau provider email sedang bermasalah, endpoint forgot password bisa ikut melambat dan memunculkan perbedaan waktu respons yang tidak diinginkan.
Gunakan queue untuk email reset
Konfigurasikan queue driver yang andal, misalnya database, Redis, atau layanan queue terkelola. Untuk beban nyata, Redis umumnya lebih cocok daripada driver sinkron.
// .env (contoh umum)
QUEUE_CONNECTION=redisPastikan worker queue berjalan stabil:
php artisan queue:workManfaat praktis queue:
- Request API lebih cepat dan konsisten.
- Retry lebih mudah jika provider email gagal sementara.
- Lonjakan trafik tidak langsung membebani proses web.
Jika memakai queue, tetap perhatikan:
- Jangan log payload sensitif.
- Setel retry dan timeout yang masuk akal.
- Monitor job gagal agar email reset tidak diam-diam hilang.
Untuk notifikasi email, gunakan mailer dan template yang sederhana. Hindari menyisipkan data sensitif selain tautan reset yang memang diperlukan. Jangan tampilkan apakah akun aktif, tidak aktif, atau tidak ditemukan di isi respons aplikasi web.
Audit log seperlunya, bukan berlebihan
Audit log berguna untuk mendeteksi abuse, tetapi logging yang terlalu detail bisa menjadi risiko baru. Simpan informasi yang cukup untuk investigasi, misalnya:
- Waktu request reset.
- Hash email atau identifier internal.
- IP atau fingerprint ringan.
- Status sukses/gagal internal.
- Waktu reset berhasil.
Hindari menyimpan:
- Token reset mentah.
- URL reset lengkap.
- Password baru atau turunannya.
- Isi email lengkap jika tidak benar-benar dibutuhkan.
Audit log adalah alat deteksi, bukan tempat menyimpan rahasia. Jika log bocor, dampaknya harus tetap terbatas.
Kesalahan umum yang sering terjadi
- Memakai validasi exists pada email forgot password, sehingga akun yang tidak ada langsung ketahuan.
- Mengembalikan pesan berbeda antara email valid dan tidak valid.
- Tidak memberi rate limit pada endpoint forgot/reset password.
- Melogging token di access log atau exception log.
- Masa berlaku token terlalu lama.
- Tidak menghapus atau menginvalidasi token setelah dipakai.
- Mengirim email secara sinkron sehingga endpoint lambat dan mudah timeout.
Checklist pengujian yang wajib dilakukan
1. Token kedaluwarsa
- Minta link reset.
- Tunggu melewati masa berlaku token.
- Coba reset dengan token tersebut.
- Pastikan hasilnya ditolak dengan pesan yang sesuai, tanpa error internal.
2. Token dipakai ulang
- Minta link reset.
- Gunakan token untuk reset password.
- Coba gunakan token yang sama lagi.
- Pastikan token kedua ditolak.
3. Flood request
- Kirim permintaan reset berkali-kali dari IP yang sama.
- Kirim ke email target yang sama berulang.
- Pastikan throttle aktif dan sistem tetap stabil.
- Verifikasi bahwa respons throttle tidak membocorkan status akun.
4. Konsistensi pesan error
- Uji email terdaftar dan tidak terdaftar.
- Bandingkan status code, body respons, dan pola waktu respons.
- Pastikan keduanya tidak mengungkap keberadaan akun.
5. Email dan queue
- Matikan sementara koneksi email atau buat provider lambat.
- Pastikan request forgot password tetap cepat jika memakai queue.
- Pastikan job gagal tercatat dan bisa di-retry.
6. Logging
- Periksa access log, application log, dan failed job log.
- Pastikan token reset tidak muncul dalam plaintext.
Tips debugging jika alur reset tidak berjalan
- Email tidak terkirim: cek konfigurasi mailer, queue worker, dan log job gagal.
- Token selalu invalid: cek broker yang dipakai, tabel token reset, expiry, dan sinkronisasi environment.
- Throttle terasa terlalu agresif: evaluasi key limiter berdasarkan IP/email dan kebutuhan pengguna nyata.
- Respons masih bocor: audit pesan validasi, status code, serta cabang logika controller.
Rekomendasi implementasi minimum yang siap diterapkan
- Pakai Password broker bawaan Laravel.
- Atur expiry token yang pendek dan masuk akal.
- Terapkan rate limiter berbasis IP dan email ter-normalisasi.
- Gunakan respons generik pada forgot password.
- Pastikan token sekali pakai dan tidak bisa direplay.
- Putar remember token saat reset berhasil.
- Kirim email lewat queue.
- Simpan audit log minimal tanpa token mentah.
Jika tujuan Anda adalah mengamankan alur reset password tanpa membocorkan apakah email terdaftar, kombinasi respons generik, throttle yang tepat, broker token yang aman, dan queue email adalah fondasi yang paling relevan di Laravel modern. Jangan berhenti di implementasi; pastikan juga ada pengujian eksplisit untuk token kedaluwarsa, replay, flood request, dan konsistensi pesan error.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!