Password Hashing & Sessions — PHP

Autentikasi web butuh dua fondasi: hash password dengan benar dan kelola session dengan aman. Kenapa JANGAN simpan password plaintext? Jika database bocor…

Login butuh dua hal: memeriksa password tanpa pernah menyimpan password aslinya, lalu mengingat siapa yang sudah login di request-request berikutnya. Bayangin pelanggan tetap Warung Bu Tini yang pesan lewat website. Di buku pelanggan, Bu Tini gak menulis kata sandinya, cuma "sidik jari"-nya: hasil olahan satu arah yang bisa dicocokkan tapi gak bisa dibalik (itu hash). Setelah cocok, pelanggan dapat kartu meja bernomor. Kartu itu gak berisi apa-apa tentang dia, tapi di kasir ada catatan "meja 7: Budi" (itu session: nomornya di cookie browser, catatannya di server).

🧪 Tebakan cepat: password_hash("rahasia", PASSWORD_DEFAULT) dijalankan dua kali. Dua hasilnya sama? Dan kalau beda, apakah password_verify("rahasia", ...) masih menjawab true untuk keduanya? Tebak dulu, jawabannya ada di bagian "Simpan hash, bukan password".

Simpan hash, bukan password

<?php
$hash = password_hash("rahasia", PASSWORD_DEFAULT);
echo $hash;
// $2y$12$HpOCDD7o12bfvO..lyDNKOY8AmLzK4Z4iJ144gqXbuHQSaq4uepXO

String 60 karakter itu yang disimpan di kolom password tabel users. Bagian-bagiannya:

Bagian Contoh Artinya
$2y$ $2y$ algoritmanya bcrypt
cost 12$ tingkat kerja: tiap naik 1, waktu hash jadi 2 kali lipat
salt HpOCDD7o12bfvO..lyDNKO 22 karakter acak, beda di tiap panggilan
hash Y8AmLzK4Z4iJ144gqXbuHQSaq4uepXO hasil olahan password plus salt

Kenapa harus hash yang sengaja lambat seperti bcrypt dan Argon2id, bukan md5 atau SHA-256, dibahas di lesson Password Hashing & Storage di track Security.

Login: password_verify

$stmt = $pdo->prepare("SELECT id, password FROM users WHERE email = ?");
$stmt->execute([$_POST["email"] ?? ""]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);

if (!$user || !password_verify($_POST["password"] ?? "", $user["password"])) {
    exit("Email atau password salah.");
}

💡 password_needs_rehash: naikkan hash lama

Hash Budi dibuat waktu server masih PHP 8.3, jadi diawali $2y$10$. Setelah upgrade ke PHP 8.4:

var_dump(password_needs_rehash('$2y$10$PMS5ysymbjbsqqayqfaFp.DvmBxfiuEkY0er1X.6doDVA864tBVCW', PASSWORD_DEFAULT));
// bool(true)

password_needs_rehash menjawab true kalau algoritma atau cost hash-nya beda dari yang diminta sekarang (cost 10 lawan 12, atau Argon2id lawan bcrypt). Satu-satunya saat password asli ada di tangan adalah saat login, jadi di situlah hash-nya diperbarui:

if (password_needs_rehash($user["password"], PASSWORD_DEFAULT)) {
    $baru = password_hash($_POST["password"], PASSWORD_DEFAULT);
    $pdo->prepare("UPDATE users SET password = ? WHERE id = ?")->execute([$baru, $user["id"]]);
}

Session: session_start dan $_SESSION

HTTP gak mengingat apa pun di antara dua request. Session yang mengingatnya:

<?php
session_start();
$_SESSION["user_id"] = 1;
Set-Cookie: PHPSESSID=058a6624a1fd3d5a2204f8257a9f7609; path=/

Setelah login: session_regenerate_id(true)

if (password_verify($_POST["password"], $user["password"])) {
    session_regenerate_id(true);
    $_SESSION["user_id"] = $user["id"];
}

💡 Cookie session yang aman, dan logout

session_set_cookie_params([
    "lifetime" => 0,
    "path" => "/",
    "secure" => true,
    "httponly" => true,
    "samesite" => "Lax",
]);
session_start();
Set-Cookie: PHPSESSID=4baf911e6cd84716755e705c61296fa0; path=/; secure; HttpOnly; SameSite=Lax

⚠️ Jebakan yang sering ketemu

TL;DR: Simpan password_hash($password, PASSWORD_DEFAULT), bukan password-nya: di PHP 8.4 hasilnya bcrypt $2y$12$... 60 karakter, dengan salt acak di dalamnya, jadi dua hash dari password yang sama selalu beda (PASSWORD_ARGON2ID memberi $argon2id$... 97 karakter, siapkan VARCHAR(255)). Cocokkan dengan password_verify($input, $hash), gak pernah dengan ===, dan perbarui hash lama dengan password_needs_rehash saat login. session_start() (sebelum output) memberi browser cookie PHPSESSID dan menyimpan $_SESSION di server. Setelah login berhasil panggil session_regenerate_id(true) supaya ID lama gak bisa dipakai (session fixation), atur cookie-nya dengan session_set_cookie_params (secure, httponly, samesite) sebelum session_start(), dan logout dengan $_SESSION = [], session_destroy(), lalu hapus cookie-nya.

Yang akan kamu pelajari