Auth Best Practices — Auth

Rangkuman praktik terbaik autentikasi dan otorisasi yang harus kamu terapkan di setiap aplikasi. 1. Password yang aman: // Validasi password yang baik…

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

  1. Hash. Isi kolom password Siti dan Pak Andi sama persis: 7f95b733f4210c71482904eb422143f8. Itu MD5 dari rahasia123: 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.
  2. Cookie. Pecah header Set-Cookie per titik koma. Harus ada HttpOnly, Secure, dan SameSite=Lax atau Strict. Versi pertama kurang dua.
  3. Pesan error. Kirim dua login gagal: email yang gak terdaftar, dan email Siti dengan password salah. Status dan pesannya harus sama: 401 dan "Email atau password salah". Kalau beda, form login jadi alat untuk mengecek email mana yang terdaftar.
  4. Rate limit. Kirim enam login salah dalam satu menit. Dengan throttle:5,1, lima pertama dijawab 401, yang keenam 429 Too Many Requests dengan header Retry-After.
  5. Regenerate. Bandingkan session ID sebelum dan sesudah login. Harus beda.
  6. 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.
  7. Secret. .env tercantum di .gitignore, git ls-files gak memuat .env, dan kode membaca APP_KEY lewat 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();

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 };
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

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.

Yang akan kamu pelajari