Table of Contents
1. Essential Security Requirements for Authentication
Authentication is the front gate of any web application. A production-ready login system must guarantee three foundational pillars: safe credential storage (using modern slow hashing algorithms), strict transport/session management (stopping hijacking and fixation), and parameterized database queries (eliminating SQL injection).
2. Relational User Schema Design
Always allocate at least 255 characters for password hashes to accommodate future algorithm upgrades (such as Argon2id) without database truncation errors:
CREATE TABLE users (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(180) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
3. Registering Users with password_hash()
<?php
declare(strict_types=1);
// Hash password using default modern cost parameters (Bcrypt)
$password = $_POST['password'] ?? '';
$email = filter_var($_POST['email'] ?? '', FILTER_VALIDATE_EMAIL);
if (!$email || strlen($password) < 12) {
throw new InvalidArgumentException('Password must be at least 12 characters.');
}
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
$stmt = $pdo->prepare('INSERT INTO users (email, password_hash) VALUES (:email, :hash)');
$stmt->execute([
':email' => $email,
':hash' => $hash
]);
4. Validating Credentials with password_verify()
<?php
declare(strict_types=1);
$email = trim($_POST['email'] ?? '');
$password = $_POST['password'] ?? '';
$stmt = $pdo->prepare('SELECT id, email, password_hash FROM users WHERE email = :email LIMIT 1');
$stmt->execute([':email' => $email]);
$user = $stmt->fetch();
// Constant-time verification prevents timing attacks
if ($user && password_verify($password, $user['password_hash'])) {
// Regenerate session ID to prevent session fixation
session_regenerate_id(true);
$_SESSION['user_id'] = $user['id'];
$_SESSION['logged_in_at'] = time();
// Check if algorithm requires rehash under updated system cost
if (password_needs_rehash($user['password_hash'], PASSWORD_BCRYPT, ['cost' => 12])) {
$newHash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
$update = $pdo->prepare('UPDATE users SET password_hash = :hash WHERE id = :id');
$update->execute([':hash' => $newHash, ':id' => $user['id']]);
}
header('Location: /dashboard');
exit;
} else {
// Generic error message prevents account enumeration
$error = 'Invalid email address or password provided.';
}
Key Takeaways
- Never store plaintext passwords or outdated hashes (MD5, SHA1).
- Always call
session_regenerate_id(true)immediately upon successful authentication. - Return identical, generic error messages for both non-existent emails and wrong passwords to eliminate account harvesting.