Password adalah satu-satunya rahasia yang pengguna kita percayakan berulang kali setiap hari — dan database yang bocor tidak memberi kesempatan kedua. Mekanisme Password Hashing pada section Authentication sudah menjelaskan cara Laravel menyimpan password sebagai hash bcrypt alih-alih teks polos. Chapter ini mengangkat sisi keamanannya: mengapa work factor bcrypt layak diperhatikan lewat pengukuran nyata, bagaimana aturan kompleksitas dinyatakan sebagai rule object yang bisa diuji, mengapa percobaan login wajib dibatasi, dan bagaimana bentuk pesan error login menentukan bocor-tidaknya informasi.

Work Factor Bcrypt: Kompromi yang Terukur #

Bcrypt sengaja lambat. Parameter rounds (cost factor) menentukan berapa kali fungsi hash diulang — setiap kenaikan satu berarti waktu berlipat dua. Kita ukur langsung dengan Hash::make():

php
use Illuminate\Support\Facades\Hash;

foreach ([4, 10, 12] as $rounds) {
    $mulai = microtime(true);
    $hash = Hash::make('rahasia-rahasia', ['rounds' => $rounds]);
    $ms = (microtime(true) - $mulai) * 1000;
    echo 'rounds='.str_pad($rounds, 2, ' ', STR_PAD_LEFT).' : '.number_format($ms, 1)
        .' ms — '.substr($hash, 0, 7)."\n";
}

Output yang Dihasilkan:

text
rounds= 4 : 1.3 ms — $2y$04$
rounds=10 : 51.9 ms — $2y$10$
rounds=12 : 207.6 ms — $2y$12$

Angka ini bervariasi tergantung perangkat, tetapi polanya tetap: rounds 12 sekitar 160 kali lebih lambat dari rounds 4. Bagi pengguna, 200 ms sekali login tidak terasa; bagi penyerang yang mencoba miliaran tebakan dari database curian, selisih itu berarti berabad-abad kerja. Nilai ini juga tersimpan di hash itu sendiri — $2y$12$ menyatakan cost factor yang dipakai — sehingga bisa dibaca ulang di masa depan. Default Laravel adalah 12 rounds, bisa diatur lewat BCRYPT_ROUNDS di .env. Ketika hardware zaman berubah dan kita menaikkan nilai itu, hash lama terdeteksi otomatis:

text
needsRehash(hash rounds=4) : true
needsRehash(hash default)  : false

Hash::needsRehash() mengembalikan true untuk hash ber-cost rendah — aplikasi bisa memanfaatkan momen login sukses untuk mengganti hash lama tanpa mengganggu pengguna.

Kebijakan Kompleksitas sebagai Rule Object #

Aturan password Laravel adalah object Password yang dirantai — bukan sekadar string rule. Hasilnya bisa langsung diuji lewat validator:

php
use Illuminate\Validation\Rules\Password;

$rule = Password::min(8)->letters()->numbers();

foreach (['pendek1', 'tanpaangka', 'kur4ng', 'rahasia-kuat-123'] as $uji) {
    $v = validator(['pw' => $uji], ['pw' => $rule]);
    $hasil = $v->fails() ? implode(' ', $v->errors()->get('pw')) : 'LULUS';
    echo str_pad($uji, 16).' : '.$hasil."\n";
}

Output yang Dihasilkan:

text
pendek1      : The pw field must be at least 8 characters.
tanpaangka   : The pw field must contain at least one number.
kur4ng       : The pw field must be at least 8 characters.
rahasia-kuat-123 : LULUS

Method yang tersedia mencakup letters(), mixedCase(), numbers(), symbols(), dan uncompromised(). Yang terakhir ini istimewa: Laravel memeriksa apakah password pernah muncul di kebocoran data publik via layanan haveibeenpwned — dengan teknik k-anonymity, hanya 5 karakter pertama hash SHA-1 yang dikirim, jadi password kita tidak pernah utuh meninggalkan server:

text
password                       : GAGAL — The given pw has appeared in a data leak. Please choose a different pw.
rahasia-sangat-panjang-92xz    : LULUS

Untuk kata-kata yang kita larang sendiri (nama brand, “administrator”, dsb.), rule not_in berperan sebagai daftar hitam sederhana: uji not_in:administrator,password menolak administrator dengan pesan “The selected pw is invalid.” sementara rahasia-budi-2025 lolos. Kebijakan untuk seluruh aplikasi didaftarkan sekali di AppServiceProvider lewat Password::defaults(fn () => Password::min(8)->letters()), lalu dipakai cukup dengan Password::default() di setiap form request.

Rate Limit Percobaan Login #

Kebijakan kompleksitas melindungi dari tebakan offline; percobaan login online harus dibatasi sendiri. Laravel menyediakan RateLimiter yang didaftarkan per skenario — untuk login, kunci yang tepat adalah email+IP agar penyerang tak bisa mengunci seluruh aplikasi hanya dengan berganti email:

php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;

// di AppServiceProvider::boot()
RateLimiter::for('login', function (Request $request) {
    return Limit::perMinute(3)->by($request->input('email').'|'.$request->ip());
});

Route login kemudian memakai middleware throttle:login. Empat percobaan beruntun dengan password salah menghasilkan:

text
percobaan #1 : HTTP 422 — These credentials do not match our records.
percobaan #2 : HTTP 422 — These credentials do not match our records.
percobaan #3 : HTTP 422 — These credentials do not match our records.
percobaan #4 : HTTP 429 —  (Retry-After: 59 detik)

Tiga percobaan pertama ditolak sebagai kredensial salah; percobaan keempat dibatasi dengan 429 Too Many Requests beserta header Retry-After yang memberi tahu kapan jendela pembuka (nilai ini berkurang tiap detik sejak pertama kali limit tersentuh). Brute force menjadi tidak ekonomis — tiga tebakan per menit per email.

Pesan Error yang Tidak Bocor #

Detail kecil yang sering terlewat: penyerang memanfaatkan perbedaan pesan error untuk menebak apakah sebuah email terdaftar (user enumeration). Laravel menegakkannya dengan satu pesan untuk semua kegagalan login. Kita bandingkan email terdaftar + password salah melawan email yang tidak terdaftar:

text
email terdaftar    : HTTP 422 — {"message":"These credentials do not match our records."}
email tak terdaftar: HTTP 422 — {"message":"These credentials do not match our records."}
respon identik     : true

Body respons sama persis — penyerang tidak bisa membedakan mana email yang ada di database. Pertahankan perilaku ini dengan tidak pernah menulis pesan khusus seperti “email tidak dikenal”. Referensi lengkap rate limiter ada di dokumentasi Rate Limiting dan aturan password di dokumentasi Validation .

Password pengguna kini terlindungi dari tiga arah: hashing yang mahal, kebijakan yang tegas, dan batas percobaan. Arah berikutnya menyangkut berkas yang diunggah pengguna — dari ekstensi ganda hingga SVG beracun — di chapter Secure File Uploads .