Introduction
Never store passwords in plain text. Use proper cryptographic hashing.
Key Concepts
- password_hash(): PHP's built-in function that generates a secure bcrypt or Argon2 hash with automatic salting.
- password_verify(): Safely compares a plain-text password against a stored hash without revealing timing information.
- Adaptive Hashing: Algorithms like bcrypt and Argon2 can increase their cost factor as hardware improves.
- password_needs_rehash(): Detects when a stored hash uses outdated settings and should be re-hashed on next login.
Real World Context
The 2012 LinkedIn breach exposed 117 million passwords stored as unsalted SHA-1 hashes, which were cracked within days. Using password_hash() with bcrypt or Argon2id would have made those hashes computationally infeasible to crack, even with modern GPU hardware.
Deep Dive
Intro
Never store passwords in plain text. Use proper cryptographic hashing.
The password_hash() function
php<?php // Creating a hash $password = 'user_password_here'; $hash = password_hash($password, PASSWORD_DEFAULT); // Output: $2y$10$... (60+ characters) // PASSWORD_DEFAULT uses bcrypt (currently) // Automatically generates a secure salt // Future-proof: algorithm may change
Verifying passwords
php<?php $submitted = $_POST['password']; $storedHash = $user['password_hash']; // From database if (password_verify($submitted, $storedHash)) { // Password is correct login($user); } else { // Password is wrong // Don't reveal which field was wrong! throw new AuthenticationException('Invalid credentials'); }
Algorithm options
php<?php // Bcrypt (default, recommended for most cases) $hash = password_hash($password, PASSWORD_BCRYPT, [ 'cost' => 12 // Higher = slower = more secure ]); // Argon2id (PHP 7.3+, best for new projects) $hash = password_hash($password, PASSWORD_ARGON2ID, [ 'memory_cost' => 65536, // 64 MB 'time_cost' => 4, // 4 iterations 'threads' => 3 // 3 threads ]);
Rehashing when needed
php<?php function login(string $password, string $hash): bool { if (!password_verify($password, $hash)) { return false; } // Check if hash needs upgrade if (password_needs_rehash($hash, PASSWORD_DEFAULT)) { $newHash = password_hash($password, PASSWORD_DEFAULT); updateUserPasswordHash($user->id, $newHash); } return true; }
Timing attacks
php<?php // WRONG: Early return reveals information if (strlen($password) < 8) { return false; // Fast response = password too short } // WRONG: String comparison timing if ($hash === $expected) { // Timing varies by position return true; } // RIGHT: Constant-time comparison if (hash_equals($expected, $hash)) { return true; }
Common Pitfalls
php<?php // WRONG: Plain text storage $user->password = $password; // WRONG: Simple hashing (crackable) $hash = md5($password); $hash = sha1($password); $hash = hash('sha256', $password); // WRONG: Unsalted hash (rainbow tables) $hash = hash('sha256', $password); // WRONG: Static salt (if leaked, all passwords vulnerable) $hash = hash('sha256', 'static_salt' . $password); // RIGHT: password_hash() with unique salt per password $hash = password_hash($password, PASSWORD_DEFAULT);
Best Practices
- Use Argon2id for new projects —
PASSWORD_ARGON2ID(PHP 7.3+) is resistant to both GPU and side-channel attacks. UsePASSWORD_DEFAULT(bcrypt) as a reliable fallback. - Implement password_needs_rehash() in login flow — Automatically upgrade password hashes when algorithm settings change, ensuring all active users get the latest protection.
Summary
- Always use
password_hash()withPASSWORD_DEFAULTorPASSWORD_ARGON2ID— never MD5, SHA1, or plain SHA-256. - Use
password_verify()for authentication andpassword_needs_rehash()to upgrade hashes over time. - Never store plain text passwords or use homegrown hashing schemes.
Code Examples
php
<?php
declare(strict_types=1);
class PasswordService {
private const ALGORITHM = PASSWORD_ARGON2ID;
private const OPTIONS = [
'memory_cost' => 65536,
'time_cost' => 4,
'threads' => 3,
];
public function hash(string $password): string {
$this->validateStrength($password);
return password_hash($password, self::ALGORITHM, self::OPTIONS);
}
public function verify(string $password, string $hash): bool {
return password_verify($password, $hash);
}
public function needsRehash(string $hash): bool {
return password_needs_rehash($hash, self::ALGORITHM, self::OPTIONS);
}
public function validateStrength(string $password): void {
$errors = [];
if (strlen($password) < 8) {
$errors[] = 'Password must be at least 8 characters';
}
if (!preg_match('/[A-Z]/', $password)) {
$errors[] = 'Password must contain an uppercase letter';
}
if (!preg_match('/[a-z]/', $password)) {
$errors[] = 'Password must contain a lowercase letter';
}
if (!preg_match('/[0-9]/', $password)) {
$errors[] = 'Password must contain a number';
}
if ($errors) {
throw new WeakPasswordException(implode('. ', $errors));
}
}
}
// Registration
$passwordService = new PasswordService();
$hash = $passwordService->hash($_POST['password']);
$stmt->execute(['password_hash' => $hash]);
// Login
if ($passwordService->verify($_POST['password'], $user['password_hash'])) {
if ($passwordService->needsRehash($user['password_hash'])) {
$newHash = $passwordService->hash($_POST['password']);
// Update in database
}
// Login successful
}
?>