Authentication bugs are the costliest bugs in web apps, and PHP gives you the right primitives if you use them properly.
Sessions keep state across requests. session_start() sends a session id cookie and loads $_SESSION from storage (files by default, Redis in larger setups). Call it before any output. Configure the cookie safely: httponly (no JavaScript access), secure (HTTPS only) and samesite=Lax or Strict, via session_set_cookie_params() or php.ini.
After login, call session_regenerate_id(true). Otherwise an attacker who planted a known session id before login (session fixation) shares the authenticated session. On logout clear $_SESSION and call session_destroy().
Passwords: never store them or hash them with md5/sha1. Use password_hash($pw, PASSWORD_DEFAULT) (bcrypt today; PHP 8.4 raised its default cost to 12) and check with password_verify(). When the algorithm or cost changes, password_needs_rehash() tells you to rehash at the next login. Argon2id is available with PASSWORD_ARGON2ID.
CSRF: a malicious site can make the browser submit a form to yours with the user's cookies. Put a random token, bin2hex(random_bytes(32)), in the session and in every form, then compare with hash_equals(), which is constant-time.
The rest of the checklist: prepared statements against SQL injection, htmlspecialchars on output against XSS, random_bytes and random_int for anything secret (never rand), and never unserialize() untrusted data.
Example
<?php
session_set_cookie_params(['httponly' => true, 'secure' => true, 'samesite' => 'Lax']);
session_start();
$hash = password_hash('correct horse', PASSWORD_DEFAULT);
echo substr($hash, 0, 7), "\n"; // $2y$12$ (bcrypt, cost 12 in 8.4)
var_dump(password_verify('correct horse', $hash)); // bool(true)
var_dump(password_verify('wrong', $hash)); // bool(false)
function login(int $userId): void {
session_regenerate_id(true); // defeat session fixation
$_SESSION['user_id'] = $userId;
}
$_SESSION['csrf'] ??= bin2hex(random_bytes(32));
// In the form: <input type="hidden" name="csrf" value="...">
function checkCsrf(string $sent): bool {
return hash_equals($_SESSION['csrf'], $sent);
}
var_dump(checkCsrf('guess')); // bool(false)Remember
- Regenerate the session id on login and set httponly, secure and samesite cookies
- password_hash and password_verify are the only password API you need
- Protect state-changing forms with a CSRF token compared via hash_equals
Watch out for
- Comparing tokens or hashes with ==, which is not constant-time and is subject to type juggling.