PHP Authentication Systems
Introduction to PHP Authentication
Authentication is the fundamental process of verifying user identity in web applications - essentially answering the question "who is this user?" This is distinctly different from authorization, which determines what an authenticated user is allowed to do.
In the early days of web development, authentication was often implemented with basic username/password forms that stored passwords in plain text. Today's landscape demands much more sophisticated approaches due to increased security threats, regulatory requirements, and user expectations for seamless experiences.
Why Modern Authentication Matters
Modern authentication systems must address several critical challenges:
Security Threats: Attackers use automated tools to attempt billions of password combinations, making brute force protection essential. Data breaches have exposed billions of credentials, making password reuse dangerous. According to Verizon's Data Breach Investigations Report, over 80% of breaches involve compromised credentials.
User Experience: Users expect features like "remember me" functionality, social login options, and password reset capabilities that work smoothly across devices. Poor authentication UX leads to password reuse, weak passwords, and user frustration.
Compliance Requirements: Regulations like GDPR, CCPA, and industry standards (PCI DSS, HIPAA) require specific security measures for handling user credentials and personal data. Non-compliance can result in significant fines and legal liability.
Scalability Demands: Authentication systems must handle high loads while maintaining security. This often requires distributed session management, sophisticated caching strategies, and efficient database design.
Core Authentication Principles
Understanding these fundamental principles guides all authentication implementation decisions:
Defense in Depth: No single security measure is sufficient. Effective authentication combines multiple protective layers including secure password storage, rate limiting, session management, and monitoring. If one layer fails, others provide backup protection.
Principle of Least Privilege: Users should only receive the minimum access necessary for their role, and this access should be regularly validated and updated. Over-privileged accounts increase the damage potential of compromised credentials.
Fail Securely: When authentication fails, the system should fail to a secure state, providing minimal information to potential attackers while logging attempts for security monitoring. Error messages should be generic to prevent information leakage.
Zero Trust: Never trust user input or assume the client-side is secure. All authentication decisions must be made server-side with proper validation and verification.
Understanding Authentication Components
Modern authentication systems consist of several interconnected components, each serving specific security purposes:
Password Security: Encompasses everything from password complexity requirements to secure storage using proper hashing algorithms. The goal is making passwords both strong enough to resist attacks and usable enough that users won't circumvent security measures.
Session Management: Creates and maintains user state across HTTP's stateless protocol. Involves secure session creation, lifetime management, and protection against hijacking and fixation attacks.
Rate Limiting: Prevents automated attacks by limiting login attempts per user account or IP address. Must balance security with user experience to avoid creating denial-of-service vulnerabilities.
Multi-Factor Authentication: Adds additional verification layers beyond passwords. Can include SMS codes, authenticator apps, hardware tokens, or biometric verification.
Monitoring and Logging: Tracks authentication events to detect attacks, compromised accounts, and system abuse. Essential for incident response and compliance requirements.
Password Security Fundamentals
Password-based authentication remains the most common method, but implementing it securely requires understanding several key concepts that many developers overlook:
Why Password Hashing Matters
Storing passwords in plain text is one of the most serious security vulnerabilities in web applications. When databases are compromised - and they frequently are - plain text passwords immediately expose all user accounts to attackers.
The Problem with Encryption: Some developers mistakenly think encrypting passwords is sufficient. However, encryption is reversible - if attackers gain access to the encryption key (often stored on the same server), they can decrypt all passwords.
One-Way Hashing: Proper password storage uses one-way hashing functions that are mathematically difficult to reverse. Even if attackers access the database, they cannot directly obtain the original passwords.
Rainbow Table Attacks: Pre-computed tables of hash values for common passwords can instantly reverse simple hashes. For example, the MD5 hash of "password" is always "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8". Attackers maintain databases of these pre-computed hashes.
Salt Protection: Salts are random values added to passwords before hashing, ensuring each password hash is unique even if users have identical passwords. This makes rainbow table attacks impractical because attackers would need separate rainbow tables for each possible salt value.
Modern Password Hashing Algorithms
bcrypt: Based on the Blowfish cipher, bcrypt includes automatic salt generation and configurable work factors. It's been battle-tested for over two decades and remains secure. The work factor can be increased as computing power grows.
scrypt: Designed to be memory-intensive as well as computationally expensive, making it more resistant to custom hardware attacks. However, it's more complex to implement correctly and can consume significant server resources.
Argon2: Winner of the Password Hashing Competition, designed specifically for password hashing with excellent resistance to various attack types. Offers multiple variants optimized for different scenarios.
PHP's Evolution: PHP's PASSWORD_DEFAULT
constant automatically selects the best available algorithm, currently bcrypt, but will upgrade to newer algorithms in future PHP versions without requiring code changes.
Basic Authentication Implementation
User Registration System
<?php
class UserRegistration {
private $pdo;
private $minPasswordLength = 8;
public function __construct($pdo) {
$this->pdo = $pdo;
}
public function registerUser($username, $email, $password, $confirmPassword) {
$errors = [];
// Validate input
$errors = array_merge($errors, $this->validateUsername($username));
$errors = array_merge($errors, $this->validateEmail($email));
$errors = array_merge($errors, $this->validatePassword($password, $confirmPassword));
if (!empty($errors)) {
return ['success' => false, 'errors' => $errors];
}
// Check if username or email already exists
if ($this->userExists($username, $email)) {
return ['success' => false, 'errors' => ['User already exists']];
}
try {
// Hash password
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
// Generate verification token
$verificationToken = bin2hex(random_bytes(32));
// Insert user
$stmt = $this->pdo->prepare("
INSERT INTO users (username, email, password_hash, verification_token, created_at)
VALUES (?, ?, ?, ?, NOW())
");
$stmt->execute([$username, $email, $hashedPassword, $verificationToken]);
$userId = $this->pdo->lastInsertId();
// Send verification email (implement separately)
$this->sendVerificationEmail($email, $verificationToken);
return [
'success' => true,
'user_id' => $userId,
'message' => 'Registration successful. Please check your email to verify your account.'
];
} catch (PDOException $e) {
error_log("Registration error: " . $e->getMessage());
return ['success' => false, 'errors' => ['Registration failed. Please try again.']];
}
}
private function validateUsername($username) {
$errors = [];
if (empty($username)) {
$errors[] = 'Username is required';
} elseif (strlen($username) < 3) {
$errors[] = 'Username must be at least 3 characters';
} elseif (strlen($username) > 50) {
$errors[] = 'Username must not exceed 50 characters';
} elseif (!preg_match('/^[a-zA-Z0-9_]+$/', $username)) {
$errors[] = 'Username can only contain letters, numbers, and underscores';
}
return $errors;
}
private function validateEmail($email) {
$errors = [];
if (empty($email)) {
$errors[] = 'Email is required';
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = 'Invalid email format';
}
return $errors;
}
private function validatePassword($password, $confirmPassword) {
$errors = [];
if (empty($password)) {
$errors[] = 'Password is required';
} elseif (strlen($password) < $this->minPasswordLength) {
$errors[] = "Password must be at least {$this->minPasswordLength} characters";
} elseif ($password !== $confirmPassword) {
$errors[] = 'Passwords do not match';
}
// Check password strength
if (!$this->isPasswordStrong($password)) {
$errors[] = 'Password must contain at least one uppercase letter, one lowercase letter, and one number';
}
return $errors;
}
private function isPasswordStrong($password) {
return preg_match('/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/', $password);
}
private function userExists($username, $email) {
$stmt = $this->pdo->prepare("SELECT id FROM users WHERE username = ? OR email = ?");
$stmt->execute([$username, $email]);
return $stmt->fetch() !== false;
}
private function sendVerificationEmail($email, $token) {
// Implementation depends on your email service
// This is a placeholder
$verificationUrl = "https://yoursite.com/verify.php?token=" . $token;
$subject = "Verify Your Account";
$message = "Click the following link to verify your account: " . $verificationUrl;
$headers = "From: [email protected]";
mail($email, $subject, $message, $headers);
}
public function verifyEmail($token) {
$stmt = $this->pdo->prepare("
UPDATE users
SET email_verified = 1, verification_token = NULL
WHERE verification_token = ?
");
return $stmt->execute([$token]) && $stmt->rowCount() > 0;
}
}
?>
Secure Login System
<?php
class SecureAuthentication {
private $pdo;
private $maxLoginAttempts = 5;
private $lockoutDuration = 900; // 15 minutes
public function __construct($pdo) {
$this->pdo = $pdo;
$this->startSecureSession();
}
private function startSecureSession() {
// Configure secure session settings
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1);
ini_set('session.use_strict_mode', 1);
ini_set('session.cookie_samesite', 'Strict');
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Regenerate session ID periodically
if (!isset($_SESSION['created'])) {
$_SESSION['created'] = time();
} elseif (time() - $_SESSION['created'] > 300) { // 5 minutes
session_regenerate_id(true);
$_SESSION['created'] = time();
}
}
public function login($username, $password, $rememberMe = false) {
// Check rate limiting
if ($this->isAccountLocked($username)) {
return [
'success' => false,
'message' => 'Account temporarily locked due to too many failed attempts'
];
}
// Get user data
$user = $this->getUserByUsername($username);
if (!$user) {
$this->recordFailedAttempt($username);
return ['success' => false, 'message' => 'Invalid credentials'];
}
// Verify password
if (!password_verify($password, $user['password_hash'])) {
$this->recordFailedAttempt($username);
return ['success' => false, 'message' => 'Invalid credentials'];
}
// Check if email is verified
if (!$user['email_verified']) {
return ['success' => false, 'message' => 'Please verify your email before logging in'];
}
// Check if account is active
if (!$user['active']) {
return ['success' => false, 'message' => 'Account is disabled'];
}
// Successful login
$this->clearFailedAttempts($username);
$this->createUserSession($user);
$this->updateLastLogin($user['id']);
// Handle "Remember Me"
if ($rememberMe) {
$this->createRememberMeToken($user['id']);
}
return [
'success' => true,
'user' => [
'id' => $user['id'],
'username' => $user['username'],
'email' => $user['email']
]
];
}
private function getUserByUsername($username) {
$stmt = $this->pdo->prepare("
SELECT id, username, email, password_hash, email_verified, active, failed_attempts, last_failed_login
FROM users
WHERE username = ? OR email = ?
");
$stmt->execute([$username, $username]);
return $stmt->fetch();
}
private function isAccountLocked($username) {
$stmt = $this->pdo->prepare("
SELECT failed_attempts, last_failed_login
FROM users
WHERE username = ? OR email = ?
");
$stmt->execute([$username, $username]);
$user = $stmt->fetch();
if (!$user || $user['failed_attempts'] < $this->maxLoginAttempts) {
return false;
}
$timeSinceLastFail = time() - strtotime($user['last_failed_login']);
return $timeSinceLastFail < $this->lockoutDuration;
}
private function recordFailedAttempt($username) {
$stmt = $this->pdo->prepare("
UPDATE users
SET failed_attempts = failed_attempts + 1, last_failed_login = NOW()
WHERE username = ? OR email = ?
");
$stmt->execute([$username, $username]);
}
private function clearFailedAttempts($username) {
$stmt = $this->pdo->prepare("
UPDATE users
SET failed_attempts = 0, last_failed_login = NULL
WHERE username = ? OR email = ?
");
$stmt->execute([$username, $username]);
}
private function createUserSession($user) {
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['email'] = $user['email'];
$_SESSION['logged_in'] = true;
$_SESSION['login_time'] = time();
$_SESSION['last_activity'] = time();
// Generate session fingerprint for security
$_SESSION['fingerprint'] = $this->generateSessionFingerprint();
}
private function generateSessionFingerprint() {
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
$acceptLanguage = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '';
$acceptEncoding = $_SERVER['HTTP_ACCEPT_ENCODING'] ?? '';
return hash('sha256', $userAgent . $acceptLanguage . $acceptEncoding);
}
private function updateLastLogin($userId) {
$stmt = $this->pdo->prepare("UPDATE users SET last_login = NOW() WHERE id = ?");
$stmt->execute([$userId]);
}
private function createRememberMeToken($userId) {
$token = bin2hex(random_bytes(32));
$expiry = date('Y-m-d H:i:s', time() + (30 * 24 * 60 * 60)); // 30 days
// Store in database
$stmt = $this->pdo->prepare("
INSERT INTO remember_tokens (user_id, token, expires_at)
VALUES (?, ?, ?)
");
$stmt->execute([$userId, hash('sha256', $token), $expiry]);
// Set secure cookie
setcookie('remember_token', $token, time() + (30 * 24 * 60 * 60), '/', '', true, true);
}
public function isLoggedIn() {
if (!isset($_SESSION['logged_in'], $_SESSION['user_id'])) {
return false;
}
// Check session timeout (30 minutes)
if (time() - $_SESSION['last_activity'] > 1800) {
$this->logout();
return false;
}
// Verify session fingerprint
if (!isset($_SESSION['fingerprint']) || $_SESSION['fingerprint'] !== $this->generateSessionFingerprint()) {
$this->logout();
return false;
}
$_SESSION['last_activity'] = time();
return true;
}
public function logout() {
// Clear remember me token if exists
if (isset($_COOKIE['remember_token'])) {
$this->clearRememberMeToken($_COOKIE['remember_token']);
setcookie('remember_token', '', time() - 3600, '/', '', true, true);
}
// Destroy session
$_SESSION = [];
session_destroy();
// Delete session cookie
if (isset($_COOKIE[session_name()])) {
setcookie(session_name(), '', time() - 3600, '/');
}
}
private function clearRememberMeToken($token) {
$stmt = $this->pdo->prepare("DELETE FROM remember_tokens WHERE token = ?");
$stmt->execute([hash('sha256', $token)]);
}
public function checkRememberMeToken() {
if (!isset($_COOKIE['remember_token'])) {
return false;
}
$token = $_COOKIE['remember_token'];
$hashedToken = hash('sha256', $token);
$stmt = $this->pdo->prepare("
SELECT rt.user_id, u.username, u.email
FROM remember_tokens rt
JOIN users u ON rt.user_id = u.id
WHERE rt.token = ? AND rt.expires_at > NOW() AND u.active = 1
");
$stmt->execute([$hashedToken]);
$user = $stmt->fetch();
if ($user) {
$this->createUserSession($user);
return true;
}
// Invalid token, clear cookie
setcookie('remember_token', '', time() - 3600, '/', '', true, true);
return false;
}
public function requireAuth() {
if (!$this->isLoggedIn()) {
header('Location: /login.php');
exit;
}
}
public function getCurrentUser() {
if (!$this->isLoggedIn()) {
return null;
}
return [
'id' => $_SESSION['user_id'],
'username' => $_SESSION['username'],
'email' => $_SESSION['email']
];
}
}
?>
Password Security
Password Reset System
<?php
class PasswordReset {
private $pdo;
private $tokenExpiry = 3600; // 1 hour
public function __construct($pdo) {
$this->pdo = $pdo;
}
public function requestPasswordReset($email) {
// Check if user exists
$stmt = $this->pdo->prepare("SELECT id, username FROM users WHERE email = ? AND active = 1");
$stmt->execute([$email]);
$user = $stmt->fetch();
if (!$user) {
// Don't reveal if email exists, return success anyway
return ['success' => true, 'message' => 'If the email exists, a reset link has been sent.'];
}
// Generate reset token
$token = bin2hex(random_bytes(32));
$expiry = date('Y-m-d H:i:s', time() + $this->tokenExpiry);
// Store token in database
$stmt = $this->pdo->prepare("
INSERT INTO password_resets (user_id, token, expires_at)
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE token = ?, expires_at = ?
");
$stmt->execute([$user['id'], $token, $expiry, $token, $expiry]);
// Send reset email
$this->sendPasswordResetEmail($email, $token, $user['username']);
return ['success' => true, 'message' => 'If the email exists, a reset link has been sent.'];
}
public function resetPassword($token, $newPassword, $confirmPassword) {
// Validate passwords
if ($newPassword !== $confirmPassword) {
return ['success' => false, 'message' => 'Passwords do not match'];
}
if (strlen($newPassword) < 8) {
return ['success' => false, 'message' => 'Password must be at least 8 characters'];
}
// Verify token
$stmt = $this->pdo->prepare("
SELECT pr.user_id, u.username
FROM password_resets pr
JOIN users u ON pr.user_id = u.id
WHERE pr.token = ? AND pr.expires_at > NOW()
");
$stmt->execute([$token]);
$user = $stmt->fetch();
if (!$user) {
return ['success' => false, 'message' => 'Invalid or expired reset token'];
}
try {
$this->pdo->beginTransaction();
// Update password
$hashedPassword = password_hash($newPassword, PASSWORD_DEFAULT);
$stmt = $this->pdo->prepare("UPDATE users SET password_hash = ? WHERE id = ?");
$stmt->execute([$hashedPassword, $user['user_id']]);
// Delete used token
$stmt = $this->pdo->prepare("DELETE FROM password_resets WHERE user_id = ?");
$stmt->execute([$user['user_id']]);
// Clear all remember me tokens for this user
$stmt = $this->pdo->prepare("DELETE FROM remember_tokens WHERE user_id = ?");
$stmt->execute([$user['user_id']]);
$this->pdo->commit();
return ['success' => true, 'message' => 'Password has been reset successfully'];
} catch (Exception $e) {
$this->pdo->rollBack();
error_log("Password reset error: " . $e->getMessage());
return ['success' => false, 'message' => 'Failed to reset password'];
}
}
private function sendPasswordResetEmail($email, $token, $username) {
$resetUrl = "https://yoursite.com/reset-password.php?token=" . $token;
$subject = "Password Reset Request";
$message = "Hello $username,\n\n";
$message .= "You have requested a password reset. Click the link below to reset your password:\n\n";
$message .= $resetUrl . "\n\n";
$message .= "This link will expire in 1 hour.\n\n";
$message .= "If you did not request this reset, please ignore this email.\n\n";
$message .= "Best regards,\nYour Website Team";
$headers = "From: [email protected]";
mail($email, $subject, $message, $headers);
}
public function cleanupExpiredTokens() {
$stmt = $this->pdo->prepare("DELETE FROM password_resets WHERE expires_at < NOW()");
$stmt->execute();
$stmt = $this->pdo->prepare("DELETE FROM remember_tokens WHERE expires_at < NOW()");
$stmt->execute();
}
}
?>
Two-Factor Authentication (2FA)
<?php
class TwoFactorAuthentication {
private $pdo;
public function __construct($pdo) {
$this->pdo = $pdo;
}
public function generateSecret() {
// Generate a base32 secret (simplified version)
$characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
$secret = '';
for ($i = 0; $i < 32; $i++) {
$secret .= $characters[random_int(0, strlen($characters) - 1)];
}
return $secret;
}
public function enableTwoFactor($userId, $verificationCode) {
// Get pending secret
$stmt = $this->pdo->prepare("SELECT two_factor_secret FROM users WHERE id = ?");
$stmt->execute([$userId]);
$user = $stmt->fetch();
if (!$user || !$user['two_factor_secret']) {
return ['success' => false, 'message' => 'No pending 2FA setup found'];
}
// Verify the code
if (!$this->verifyTOTP($user['two_factor_secret'], $verificationCode)) {
return ['success' => false, 'message' => 'Invalid verification code'];
}
// Enable 2FA
$stmt = $this->pdo->prepare("UPDATE users SET two_factor_enabled = 1 WHERE id = ?");
$stmt->execute([$userId]);
// Generate backup codes
$backupCodes = $this->generateBackupCodes($userId);
return [
'success' => true,
'message' => '2FA enabled successfully',
'backup_codes' => $backupCodes
];
}
public function disableTwoFactor($userId, $password) {
// Verify password before disabling
$stmt = $this->pdo->prepare("SELECT password_hash FROM users WHERE id = ?");
$stmt->execute([$userId]);
$user = $stmt->fetch();
if (!$user || !password_verify($password, $user['password_hash'])) {
return ['success' => false, 'message' => 'Invalid password'];
}
// Disable 2FA and clear secret
$stmt = $this->pdo->prepare("
UPDATE users
SET two_factor_enabled = 0, two_factor_secret = NULL
WHERE id = ?
");
$stmt->execute([$userId]);
// Delete backup codes
$stmt = $this->pdo->prepare("DELETE FROM backup_codes WHERE user_id = ?");
$stmt->execute([$userId]);
return ['success' => true, 'message' => '2FA disabled successfully'];
}
public function verifyTwoFactor($userId, $code) {
$stmt = $this->pdo->prepare("
SELECT two_factor_secret, two_factor_enabled
FROM users
WHERE id = ?
");
$stmt->execute([$userId]);
$user = $stmt->fetch();
if (!$user || !$user['two_factor_enabled']) {
return false;
}
// Check if it's a backup code
if ($this->verifyBackupCode($userId, $code)) {
return true;
}
// Verify TOTP
return $this->verifyTOTP($user['two_factor_secret'], $code);
}
private function verifyTOTP($secret, $code) {
// Simplified TOTP verification
// In production, use a library like RobThree/TwoFactorAuth
$timeStep = floor(time() / 30);
// Check current time window and adjacent windows for clock drift
for ($i = -1; $i <= 1; $i++) {
$testCode = $this->generateTOTP($secret, $timeStep + $i);
if (hash_equals($testCode, $code)) {
return true;
}
}
return false;
}
private function generateTOTP($secret, $timeStep) {
// Simplified TOTP generation
// This is a basic implementation - use a proper library in production
$data = pack('N*', 0, $timeStep);
$hash = hash_hmac('sha1', $data, $this->base32Decode($secret), true);
$offset = ord($hash[19]) & 0xf;
$code = (
((ord($hash[$offset]) & 0x7f) << 24) |
((ord($hash[$offset + 1]) & 0xff) << 16) |
((ord($hash[$offset + 2]) & 0xff) << 8) |
(ord($hash[$offset + 3]) & 0xff)
) % 1000000;
return sprintf('%06d', $code);
}
private function base32Decode($data) {
// Simplified base32 decode - use a proper library in production
$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
$output = '';
$v = 0;
$vbits = 0;
for ($i = 0, $j = strlen($data); $i < $j; $i++) {
$v <<= 5;
$v += strpos($alphabet, $data[$i]);
$vbits += 5;
if ($vbits >= 8) {
$output .= chr($v >> ($vbits - 8));
$vbits -= 8;
}
}
return $output;
}
private function generateBackupCodes($userId) {
$codes = [];
// Clear existing backup codes
$stmt = $this->pdo->prepare("DELETE FROM backup_codes WHERE user_id = ?");
$stmt->execute([$userId]);
// Generate 10 backup codes
for ($i = 0; $i < 10; $i++) {
$code = '';
for ($j = 0; $j < 8; $j++) {
$code .= random_int(0, 9);
}
$codes[] = $code;
// Store hashed version
$stmt = $this->pdo->prepare("
INSERT INTO backup_codes (user_id, code_hash)
VALUES (?, ?)
");
$stmt->execute([$userId, password_hash($code, PASSWORD_DEFAULT)]);
}
return $codes;
}
private function verifyBackupCode($userId, $code) {
$stmt = $this->pdo->prepare("SELECT id, code_hash FROM backup_codes WHERE user_id = ?");
$stmt->execute([$userId]);
$backupCodes = $stmt->fetchAll();
foreach ($backupCodes as $backupCode) {
if (password_verify($code, $backupCode['code_hash'])) {
// Remove used backup code
$stmt = $this->pdo->prepare("DELETE FROM backup_codes WHERE id = ?");
$stmt->execute([$backupCode['id']]);
return true;
}
}
return false;
}
}
?>
Database Schema
-- Users table
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
email_verified BOOLEAN DEFAULT FALSE,
verification_token VARCHAR(64),
active BOOLEAN DEFAULT TRUE,
failed_attempts INT DEFAULT 0,
last_failed_login TIMESTAMP NULL,
last_login TIMESTAMP NULL,
two_factor_enabled BOOLEAN DEFAULT FALSE,
two_factor_secret VARCHAR(32),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_username (username),
INDEX idx_email (email),
INDEX idx_verification_token (verification_token)
);
-- Remember me tokens
CREATE TABLE remember_tokens (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
token VARCHAR(64) NOT NULL,
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE KEY unique_user_token (user_id, token),
INDEX idx_token (token),
INDEX idx_expires (expires_at)
);
-- Password reset tokens
CREATE TABLE password_resets (
user_id INT PRIMARY KEY,
token VARCHAR(64) NOT NULL,
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_token (token),
INDEX idx_expires (expires_at)
);
-- Two-factor backup codes
CREATE TABLE backup_codes (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
code_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_user (user_id)
);
-- Login history for auditing
CREATE TABLE login_history (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT,
ip_address VARCHAR(45),
user_agent TEXT,
success BOOLEAN,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
INDEX idx_user_id (user_id),
INDEX idx_created_at (created_at),
INDEX idx_ip_address (ip_address)
);
Complete Authentication Example
<?php
// login.php
require_once 'config.php';
require_once 'classes/SecureAuthentication.php';
require_once 'classes/PasswordReset.php';
$auth = new SecureAuthentication($pdo);
$message = '';
$messageType = '';
// Check for remember me login
if (!$auth->isLoggedIn()) {
$auth->checkRememberMeToken();
}
// Redirect if already logged in
if ($auth->isLoggedIn()) {
header('Location: dashboard.php');
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['login'])) {
$username = trim($_POST['username']);
$password = $_POST['password'];
$rememberMe = isset($_POST['remember_me']);
$result = $auth->login($username, $password, $rememberMe);
if ($result['success']) {
header('Location: dashboard.php');
exit;
} else {
$message = $result['message'];
$messageType = 'error';
}
}
if (isset($_POST['reset_password'])) {
$passwordReset = new PasswordReset($pdo);
$email = trim($_POST['reset_email']);
$result = $passwordReset->requestPasswordReset($email);
$message = $result['message'];
$messageType = $result['success'] ? 'success' : 'error';
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login</title>
<style>
.container { max-width: 400px; margin: 100px auto; padding: 20px; }
.form-group { margin-bottom: 15px; }
.form-group label { display: block; margin-bottom: 5px; }
.form-group input { width: 100%; padding: 8px; border: 1px solid #ddd; }
.btn { padding: 10px 20px; background: #007cba; color: white; border: none; cursor: pointer; width: 100%; }
.message { padding: 10px; margin-bottom: 20px; border-radius: 4px; }
.error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
.success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
.tabs { display: flex; margin-bottom: 20px; }
.tab { flex: 1; padding: 10px; background: #f8f9fa; border: 1px solid #ddd; cursor: pointer; text-align: center; }
.tab.active { background: #007cba; color: white; }
.tab-content { display: none; }
.tab-content.active { display: block; }
.remember-me { display: flex; align-items: center; }
.remember-me input { width: auto; margin-right: 8px; }
</style>
</head>
<body>
<div class="container">
<h1>Login</h1>
<?php if ($message): ?>
<div class="message <?php echo $messageType; ?>">
<?php echo htmlspecialchars($message); ?>
</div>
<?php endif; ?>
<div class="tabs">
<div class="tab active" onclick="switchTab('login')">Login</div>
<div class="tab" onclick="switchTab('reset')">Reset Password</div>
</div>
<div id="login-tab" class="tab-content active">
<form method="POST">
<div class="form-group">
<label for="username">Username or Email:</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="password">Password:</label>
<input type="password" id="password" name="password" required>
</div>
<div class="form-group remember-me">
<input type="checkbox" id="remember_me" name="remember_me">
<label for="remember_me">Remember me for 30 days</label>
</div>
<button type="submit" name="login" class="btn">Login</button>
</form>
</div>
<div id="reset-tab" class="tab-content">
<form method="POST">
<div class="form-group">
<label for="reset_email">Email Address:</label>
<input type="email" id="reset_email" name="reset_email" required>
</div>
<button type="submit" name="reset_password" class="btn">Send Reset Link</button>
</form>
</div>
<p style="text-align: center; margin-top: 20px;">
Don't have an account? <a href="register.php">Register here</a>
</p>
</div>
<script>
function switchTab(tab) {
// Hide all tab contents
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
// Remove active class from all tabs
document.querySelectorAll('.tab').forEach(tabEl => {
tabEl.classList.remove('active');
});
// Show selected tab content
document.getElementById(tab + '-tab').classList.add('active');
// Add active class to selected tab
event.target.classList.add('active');
}
</script>
</body>
</html>
Security Monitoring and Logging
<?php
class SecurityMonitor {
private $pdo;
public function __construct($pdo) {
$this->pdo = $pdo;
}
public function logLoginAttempt($userId, $username, $success, $ipAddress, $userAgent) {
$stmt = $this->pdo->prepare("
INSERT INTO login_history (user_id, username, success, ip_address, user_agent)
VALUES (?, ?, ?, ?, ?)
");
$stmt->execute([$userId, $username, $success, $ipAddress, $userAgent]);
}
public function detectSuspiciousActivity($ipAddress, $timeWindow = 300) {
// Check for multiple failed logins from same IP
$stmt = $this->pdo->prepare("
SELECT COUNT(*) as attempts
FROM login_history
WHERE ip_address = ? AND success = 0 AND created_at > DATE_SUB(NOW(), INTERVAL ? SECOND)
");
$stmt->execute([$ipAddress, $timeWindow]);
$result = $stmt->fetch();
return $result['attempts'] > 10; // More than 10 failed attempts in time window
}
public function getLoginStatistics($userId, $days = 30) {
$stmt = $this->pdo->prepare("
SELECT
DATE(created_at) as date,
COUNT(*) as total_attempts,
SUM(success) as successful_logins,
COUNT(DISTINCT ip_address) as unique_ips
FROM login_history
WHERE user_id = ? AND created_at > DATE_SUB(NOW(), INTERVAL ? DAY)
GROUP BY DATE(created_at)
ORDER BY date DESC
");
$stmt->execute([$userId, $days]);
return $stmt->fetchAll();
}
}
?>
Related Topics
For more PHP security concepts:
- PHP Security - Overall security principles
- PHP SQL Injection Prevention - Database security
- PHP XSS Protection - Cross-site scripting prevention
- PHP CSRF Protection - Cross-site request forgery
- PHP Session Management - Session handling
Summary
Secure PHP authentication systems require:
- Strong Password Policies: Minimum length, complexity requirements, and secure hashing
- Session Security: Secure session configuration, regeneration, and fingerprinting
- Rate Limiting: Protection against brute force attacks
- Two-Factor Authentication: Additional security layer for sensitive applications
- Secure Token Management: Proper handling of reset tokens and remember-me functionality
- Comprehensive Logging: Security monitoring and audit trails
- Regular Security Reviews: Ongoing assessment and updates
Modern authentication goes beyond basic username/password to include multi-factor authentication, behavioral analysis, and comprehensive security monitoring. Building secure authentication systems requires attention to detail and understanding of common attack vectors.