Auth best practices bukan satu fitur baru, tapi daftar cek: hal-hal kecil yang sudah kamu pelajari satu per satu, dan semuanya harus benar sekaligus. Mirip checklist pilot sebelum terbang. Pilotnya jago, tapi tetap mencentang tiap baris, karena satu baris yang terlewat sudah cukup bikin celaka. Di lesson ini kamu mengaudit login Portal Sekolah versi pertama dengan tujuh cek, memperbaiki yang gagal, lalu menulis aturan password untuk form daftar.
🧪 Tebakan cepat: Siti login di laptop, HP, dan komputer lab sekolah, jadi dia punya tiga sesi. Di laptop dia menekan "Keluar dari semua perangkat", tapi Portal Sekolah versi pertama cuma menghapus sesi yang sedang dipakai. Berapa sesi Siti yang masih hidup? Tebak dulu, jawabannya ada di bagian "Tujuh cek, satu per satu".
Setup yang kita audit
Ini login Portal Sekolah versi pertama, persis data yang dipakai Lab. Tiap baris punya cek yang bisa dijalankan, bukan sekadar perasaan "kayaknya aman":
| # | Yang dicek | Versi pertama | Hasil | Diajarkan di |
|---|---|---|---|---|
| 1 | Password di-hash lambat, dengan salt | md5($password) |
gagal | Password Hashing |
| 2 | Cookie sesi HttpOnly, Secure, SameSite |
sid=...; Path=/; HttpOnly |
gagal | Session-based Authentication |
| 3 | Login gagal selalu dijawab sama | "Email tidak terdaftar" dan "Password salah" | gagal | Password Hashing |
| 4 | Percobaan login dibatasi | throttle:5,1 |
lolos | Rate Limiting untuk Autentikasi |
| 5 | Session ID baru setelah login | session()->regenerate() |
lolos | Session-based Authentication |
| 6 | Keluar dari semua perangkat | hapus sesi yang sedang dipakai saja | gagal | Device & Session Management |
| 7 | Secret di luar repo | .env di .gitignore, env('APP_KEY') |
lolos | Environment Variables & Secrets (track Security) |
Tiga lolos, empat gagal. Lesson ini gak mengulang isi lesson-lesson itu. Yang baru di sini: cara memeriksa tiap baris dan memperbaikinya.
Tujuh cek, satu per satu
- Hash. Isi kolom
passwordSiti dan Pak Andi sama persis:7f95b733f4210c71482904eb422143f8. Itu MD5 darirahasia123: cepat, tanpa salt, jadi password sama kelihatan sama. Lolos kalau isinya bcrypt dengan cost 12 ke atas ($2y$12$...) atau argon2id, dan tiap user beda. - Cookie. Pecah header
Set-Cookieper titik koma. Harus adaHttpOnly,Secure, danSameSite=LaxatauStrict. Versi pertama kurang dua. - Pesan error. Kirim dua login gagal: email yang gak terdaftar, dan email Siti dengan password salah. Status dan pesannya harus sama:
401dan "Email atau password salah". Kalau beda, form login jadi alat untuk mengecek email mana yang terdaftar. - Rate limit. Kirim enam login salah dalam satu menit. Dengan
throttle:5,1, lima pertama dijawab401, yang keenam429 Too Many Requestsdengan headerRetry-After. - Regenerate. Bandingkan session ID sebelum dan sesudah login. Harus beda.
- Logout di semua perangkat. Siti punya tiga sesi. Versi pertama menghapus sesi laptop saja, jadi tersisa dua: HP dan komputer lab masih login. Itu jawaban tebakannya. Lolos kalau sisanya nol.
- Secret.
.envtercantum di.gitignore,git ls-filesgak memuat.env, dan kode membacaAPP_KEYlewat config, bukan menulisnya langsung.
Perbaikan untuk empat yang gagal, di Laravel:
// 1. Hash: bcrypt cost 12 (BCRYPT_ROUNDS=12), bukan md5
$user->password = Hash::make($request->password);
// 2. .env production: config/session.php sudah http_only true dan same_site lax
// SESSION_SECURE_COOKIE=true
// 3. Satu pesan untuk semua login yang gagal
if (! Auth::attempt($credentials)) {
return response()->json(['message' => 'Email atau password salah'], 401);
}
// 6. Keluar dari semua perangkat (session driver: database)
DB::table('sessions')->where('user_id', $request->user()->id)->delete();
Auth::logout();
$request->session()->invalidate();
Hash::makememberi string 60 karakter dengan salt di dalamnya. Hash MD5 lama gak bisa diubah langsung: saat user login berikutnya, cocokkan sekali dengan cara lama, lalu simpan hasilHash::make-nya (idenya sama dengan rehash di lesson Password Hashing).Auth::attemptmengembalikanfalseuntuk email tak terdaftar dan password salah, jadi satu cabang menjawab keduanya dengan status dan pesan yang sama.- Tabel
sessionsmenyimpanuser_idtiap sesi, jadi satudeletemematikan semua perangkat. Laravel juga punyaAuth::logoutOtherDevices($password)(butuh middlewareauth.session).
Rate limit: membatasi percobaan login
Perlindungan brute force berarti membatasi jumlah login gagal, lalu menambah jeda setelah beberapa kali gagal:
Route::post('/login', [LoginController::class, 'store'])->middleware('throttle:5,1');
throttle:5,1 artinya 5 percobaan per 1 menit. Kunci yang bagus: email ditambah IP, supaya satu akun gak bisa dicoba terus dari mana pun, dan satu IP gak bisa mencoba banyak akun. Setelah berkali-kali gagal, jedanya bisa dibuat makin panjang. Mengunci akun dan memberi tahu pemiliknya dibahas di lesson Account Recovery & Lockout.
Aturan password saat daftar
Form daftar Portal Sekolah menolak password yang lemah sebelum di-hash. Aturan yang dipakai mengikuti panduan NIST SP 800-63B, dan itu juga yang dinilai di latihan lesson ini: tiga aturan, diperiksa berurutan, dan tiap aturan yang gagal menambah satu pesan:
| Urutan | Aturan | Pesan kalau gagal |
|---|---|---|
| 1 | panjang minimal 8 | Minimal 8 karakter |
| 2 | panjang maksimal 64 | Maksimal 64 karakter |
| 3 | gak ada di daftar password bocor (dibandingkan dalam huruf kecil) | Password ini ada di daftar bocor |
const errors = [];
if (password.length < 8) errors.push("Minimal 8 karakter");
if (password.length > 64) errors.push("Maksimal 64 karakter");
if (DAFTAR_BOCOR.includes(password.toLowerCase())) errors.push("Password ini ada di daftar bocor");
return { valid: errors.length === 0, errors };
- Tiap
ifberdiri sendiri, jadi satu password bisa kena lebih dari satu pesan, sesuai urutan tabel.errors.join("|")menggabungkannya dengan pemisah|. validbernilaitruekalauerrorskosong.
| Password | valid |
errors |
|---|---|---|
Ab1! |
false |
Minimal 8 karakter |
qwerty |
false |
Minimal 8 karakter|Password ini ada di daftar bocor |
Password |
false |
Password ini ada di daftar bocor |
kopi susu gula aren |
true |
kosong |
Aturan campuran seperti "harus ada huruf besar, angka, dan simbol" sengaja gak dipakai. NIST SP 800-63B menyarankan untuk gak memaksa campuran jenis karakter: orang jadi menulis Password1!, yang memenuhi semua aturan tapi ada di setiap daftar bocor. Yang benar-benar menolong: utamakan panjang (15 karakter kalau password jadi satu-satunya faktor), izinkan password panjang sampai minimal 64 karakter, dan tolak password yang ada di daftar bocor. Daftar di latihan cuma contoh kecil; server sungguhan memeriksa jutaan password, misalnya lewat Have I Been Pwned. Di Laravel: Password::min(15)->uncompromised().
Token acak, bukan rakitan
Token untuk login, reset password, atau API harus acak. md5($user->id . time()) itu rakitan: id-nya kecil, time() cuma detik saat ini, dan MD5 itu deterministik dan cepat, jadi siapa pun bisa menghitung ulang. Token itu juga gak punya masa berlaku.
$token = bin2hex(random_bytes(32)); // 64 karakter hex, 256 bit acak
DB::table('api_tokens')->insert([
'user_id' => $user->id,
'token_hash' => hash('sha256', $token), // yang disimpan cuma hash-nya
'expires_at' => now()->addMinutes(15),
]);
📌 Yang belum masuk tujuh cek
Checklist lengkapnya lebih panjang: MFA untuk akun admin (lesson Multi-factor Authentication), access token pendek dengan refresh token yang dirotasi (Refresh Token Rotation), CSRF untuk form berbasis cookie (CSRF Protection), dan catatan setiap login, gagal login, dan logout (Audit Logs & Auth Trail).
⚠️ Jebakan yang sering ketemu
- Pesan sama, tapi status atau waktunya beda.
401untuk password salah dan422untuk email tak terdaftar tetap membocorkan. Server juga tetap menjalankan satu hash palsu untuk email yang gak ada. - Checklist dicentang sekali, lalu dilupakan.
SESSION_SECURE_COOKIElupa diisi di.envproduction. Jadikan cek-cek ini test yang jalan di CI. env()dipanggil di luar folderconfig. Setelahphp artisan config:cache,env('APP_KEY')mengembalikannull. Baca lewatconfig('app.key').- Pesan aturan yang gak persis. Di latihan,
"Minimal 8 karakter"dan teman-temannya dicek huruf per huruf, dan urutannya ikut dicek. - Memaksa huruf besar, angka, dan simbol. Aturan komposisi bikin orang menulis
Password1!, yang ada di setiap daftar bocor. Utamakan panjang dan daftar bocor.
TL;DR: Auth yang aman adalah daftar cek yang semuanya lolos: hash bcrypt atau argon2id dengan salt, cookie HttpOnly; Secure; SameSite, satu pesan untuk semua login gagal, throttle pada login (429 di percobaan keenam), session ID baru setelah login, logout yang mematikan semua sesi, dan secret di .env yang gak ikut di-commit. Token dibuat dengan random_bytes(32) dan disimpan hash-nya. Aturan password di latihan mengikuti NIST SP 800-63B: minimal 8, maksimal 64, tolak password yang ada di daftar bocor, satu pesan per aturan yang gagal, dan tanpa aturan komposisi.