1. php
  2. /security
  3. /password-security

Password Security in PHP

Introduction to Password Security

Password security is one of the most critical aspects of web application security. Proper password handling involves secure storage through hashing, strong validation policies, and protection against common attacks. PHP provides built-in functions for secure password handling that follow current best practices.

Password Hashing

Never Store Plain Text Passwords

<?php
// NEVER do this - plain text storage
$password = $_POST['password'];
$sql = "INSERT INTO users (email, password) VALUES (?, ?)";
$stmt->execute([$email, $password]); // WRONG!
?>

Using password_hash()

PHP's password_hash() function provides secure password hashing using industry-standard algorithms:

<?php
function registerUser($email, $password) {
    // Hash the password securely
    $hashedPassword = password_hash($password, PASSWORD_DEFAULT);
    
    // Store in database
    $pdo = new PDO("mysql:host=localhost;dbname=myapp", $username, $dbpassword);
    $stmt = $pdo->prepare("INSERT INTO users (email, password_hash) VALUES (?, ?)");
    
    return $stmt->execute([$email, $hashedPassword]);
}

// Usage
$email = $_POST['email'];
$password = $_POST['password'];

if (registerUser($email, $password)) {
    echo "User registered successfully";
}
?>

Password Hashing Options

<?php
// Use default algorithm (currently bcrypt, may change in future PHP versions)
$hash = password_hash($password, PASSWORD_DEFAULT);

// Explicitly use bcrypt
$hash = password_hash($password, PASSWORD_BCRYPT);

// Use Argon2i (PHP 7.2+)
$hash = password_hash($password, PASSWORD_ARGON2I);

// Use Argon2id (PHP 7.2+) - recommended for new applications
$hash = password_hash($password, PASSWORD_ARGON2ID);

// Custom cost for bcrypt (higher = more secure but slower)
$options = ['cost' => 12];
$hash = password_hash($password, PASSWORD_BCRYPT, $options);

// Custom options for Argon2
$options = [
    'memory_cost' => 65536, // 64 MB
    'time_cost' => 4,       // 4 iterations
    'threads' => 3,         // 3 threads
];
$hash = password_hash($password, PASSWORD_ARGON2ID, $options);
?>

Password Verification

Using password_verify()

<?php
function authenticateUser($email, $password) {
    $pdo = new PDO("mysql:host=localhost;dbname=myapp", $username, $dbpassword);
    $stmt = $pdo->prepare("SELECT id, password_hash FROM users WHERE email = ?");
    $stmt->execute([$email]);
    
    $user = $stmt->fetch(PDO::FETCH_ASSOC);
    
    if ($user && password_verify($password, $user['password_hash'])) {
        return $user['id']; // Authentication successful
    }
    
    return false; // Authentication failed
}

// Usage
$email = $_POST['email'];
$password = $_POST['password'];

$userId = authenticateUser($email, $password);
if ($userId) {
    $_SESSION['user_id'] = $userId;
    echo "Login successful";
} else {
    echo "Invalid credentials";
}
?>

Complete Login System

<?php
class AuthenticationService {
    private $pdo;
    
    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }
    
    public function register($email, $password) {
        // Validate password strength
        if (!$this->isPasswordStrong($password)) {
            throw new Exception("Password does not meet security requirements");
        }
        
        // Check if email already exists
        $stmt = $this->pdo->prepare("SELECT id FROM users WHERE email = ?");
        $stmt->execute([$email]);
        
        if ($stmt->fetch()) {
            throw new Exception("Email already registered");
        }
        
        // Hash password and create user
        $hashedPassword = password_hash($password, PASSWORD_ARGON2ID);
        
        $stmt = $this->pdo->prepare("INSERT INTO users (email, password_hash, created_at) VALUES (?, ?, NOW())");
        return $stmt->execute([$email, $hashedPassword]);
    }
    
    public function login($email, $password) {
        $stmt = $this->pdo->prepare("
            SELECT id, password_hash, failed_attempts, locked_until 
            FROM users 
            WHERE email = ?
        ");
        $stmt->execute([$email]);
        $user = $stmt->fetch(PDO::FETCH_ASSOC);
        
        if (!$user) {
            $this->logFailedAttempt($email);
            throw new Exception("Invalid credentials");
        }
        
        // Check if account is locked
        if ($user['locked_until'] && strtotime($user['locked_until']) > time()) {
            throw new Exception("Account is temporarily locked. Try again later.");
        }
        
        // Verify password
        if (password_verify($password, $user['password_hash'])) {
            $this->clearFailedAttempts($user['id']);
            $this->logSuccessfulLogin($user['id']);
            return $user['id'];
        } else {
            $this->recordFailedAttempt($user['id']);
            throw new Exception("Invalid credentials");
        }
    }
    
    private function isPasswordStrong($password) {
        // Minimum 8 characters
        if (strlen($password) < 8) return false;
        
        // At least one uppercase letter
        if (!preg_match('/[A-Z]/', $password)) return false;
        
        // At least one lowercase letter
        if (!preg_match('/[a-z]/', $password)) return false;
        
        // At least one number
        if (!preg_match('/[0-9]/', $password)) return false;
        
        // At least one special character
        if (!preg_match('/[^a-zA-Z0-9]/', $password)) return false;
        
        return true;
    }
    
    private function recordFailedAttempt($userId) {
        $stmt = $this->pdo->prepare("
            UPDATE users 
            SET failed_attempts = failed_attempts + 1,
                locked_until = CASE 
                    WHEN failed_attempts >= 4 THEN DATE_ADD(NOW(), INTERVAL 15 MINUTE)
                    ELSE NULL
                END
            WHERE id = ?
        ");
        $stmt->execute([$userId]);
    }
    
    private function clearFailedAttempts($userId) {
        $stmt = $this->pdo->prepare("UPDATE users SET failed_attempts = 0, locked_until = NULL WHERE id = ?");
        $stmt->execute([$userId]);
    }
    
    private function logFailedAttempt($email) {
        $stmt = $this->pdo->prepare("INSERT INTO login_attempts (email, success, ip_address, attempted_at) VALUES (?, 0, ?, NOW())");
        $stmt->execute([$email, $_SERVER['REMOTE_ADDR']]);
    }
    
    private function logSuccessfulLogin($userId) {
        $stmt = $this->pdo->prepare("INSERT INTO login_attempts (user_id, success, ip_address, attempted_at) VALUES (?, 1, ?, NOW())");
        $stmt->execute([$userId, $_SERVER['REMOTE_ADDR']]);
    }
}
?>

Password Rehashing

When security standards change or you want to increase hash strength, you should rehash existing passwords:

<?php
function checkAndRehashPassword($userId, $password, $currentHash) {
    // Verify current password
    if (!password_verify($password, $currentHash)) {
        return false;
    }
    
    // Check if rehashing is needed
    if (password_needs_rehash($currentHash, PASSWORD_ARGON2ID)) {
        $newHash = password_hash($password, PASSWORD_ARGON2ID);
        
        // Update database with new hash
        $pdo = new PDO("mysql:host=localhost;dbname=myapp", $username, $dbpassword);
        $stmt = $pdo->prepare("UPDATE users SET password_hash = ? WHERE id = ?");
        $stmt->execute([$newHash, $userId]);
    }
    
    return true;
}

// Usage during login
$user = getUserFromDatabase($email);
if (checkAndRehashPassword($user['id'], $password, $user['password_hash'])) {
    // User authenticated and password rehashed if necessary
    startUserSession($user['id']);
}
?>

Password Strength Validation

Comprehensive Password Validation

<?php
class PasswordValidator {
    private $minLength = 8;
    private $maxLength = 128;
    
    public function validate($password) {
        $errors = [];
        
        // Length checks
        if (strlen($password) < $this->minLength) {
            $errors[] = "Password must be at least {$this->minLength} characters long";
        }
        
        if (strlen($password) > $this->maxLength) {
            $errors[] = "Password must not exceed {$this->maxLength} characters";
        }
        
        // Character type requirements
        if (!preg_match('/[a-z]/', $password)) {
            $errors[] = "Password must contain at least one lowercase letter";
        }
        
        if (!preg_match('/[A-Z]/', $password)) {
            $errors[] = "Password must contain at least one uppercase letter";
        }
        
        if (!preg_match('/[0-9]/', $password)) {
            $errors[] = "Password must contain at least one number";
        }
        
        if (!preg_match('/[^a-zA-Z0-9]/', $password)) {
            $errors[] = "Password must contain at least one special character";
        }
        
        // Check for common patterns
        if ($this->hasCommonPatterns($password)) {
            $errors[] = "Password contains common patterns";
        }
        
        // Check against common passwords
        if ($this->isCommonPassword($password)) {
            $errors[] = "Password is too common";
        }
        
        return [
            'valid' => empty($errors),
            'errors' => $errors,
            'strength' => $this->calculateStrength($password)
        ];
    }
    
    private function hasCommonPatterns($password) {
        $patterns = [
            '/(.)\1{2,}/',        // Three or more consecutive identical characters
            '/123|abc|qwe/i',     // Sequential characters
            '/password|admin/i',   // Common words
        ];
        
        foreach ($patterns as $pattern) {
            if (preg_match($pattern, $password)) {
                return true;
            }
        }
        
        return false;
    }
    
    private function isCommonPassword($password) {
        $commonPasswords = [
            'password', '123456', 'password123', 'admin', 'letmein',
            'welcome', 'monkey', '1234567890', 'qwerty', 'abc123'
        ];
        
        return in_array(strtolower($password), $commonPasswords);
    }
    
    private function calculateStrength($password) {
        $score = 0;
        
        // Length bonus
        $score += min(strlen($password) * 2, 50);
        
        // Character variety
        if (preg_match('/[a-z]/', $password)) $score += 10;
        if (preg_match('/[A-Z]/', $password)) $score += 10;
        if (preg_match('/[0-9]/', $password)) $score += 10;
        if (preg_match('/[^a-zA-Z0-9]/', $password)) $score += 15;
        
        // Entropy bonus for mixed character types
        $types = 0;
        if (preg_match('/[a-z]/', $password)) $types++;
        if (preg_match('/[A-Z]/', $password)) $types++;
        if (preg_match('/[0-9]/', $password)) $types++;
        if (preg_match('/[^a-zA-Z0-9]/', $password)) $types++;
        
        $score += $types * 5;
        
        // Penalty for common patterns
        if ($this->hasCommonPatterns($password)) {
            $score -= 20;
        }
        
        if ($score < 30) return 'weak';
        if ($score < 60) return 'fair';
        if ($score < 80) return 'good';
        return 'strong';
    }
}

// Usage
$validator = new PasswordValidator();
$result = $validator->validate($_POST['password']);

if (!$result['valid']) {
    foreach ($result['errors'] as $error) {
        echo "Error: $error\n";
    }
} else {
    echo "Password strength: " . $result['strength'];
}
?>

Password Reset Security

Secure Password Reset Implementation

<?php
class PasswordResetService {
    private $pdo;
    private $tokenExpiry = 3600; // 1 hour
    
    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }
    
    public function initiateReset($email) {
        // Check if user exists
        $stmt = $this->pdo->prepare("SELECT id FROM users WHERE email = ?");
        $stmt->execute([$email]);
        $user = $stmt->fetch();
        
        if (!$user) {
            // Don't reveal if email exists or not
            return true;
        }
        
        // Generate secure token
        $token = bin2hex(random_bytes(32));
        $expiresAt = date('Y-m-d H:i:s', time() + $this->tokenExpiry);
        
        // Store reset token
        $stmt = $this->pdo->prepare("
            INSERT INTO password_resets (user_id, token, expires_at, created_at) 
            VALUES (?, ?, ?, NOW())
            ON DUPLICATE KEY UPDATE 
            token = VALUES(token), 
            expires_at = VALUES(expires_at),
            used_at = NULL
        ");
        $stmt->execute([$user['id'], hash('sha256', $token), $expiresAt]);
        
        // Send email (implement your email service)
        $this->sendResetEmail($email, $token);
        
        return true;
    }
    
    public function resetPassword($token, $newPassword) {
        // Hash the token for lookup
        $hashedToken = hash('sha256', $token);
        
        // Find valid reset request
        $stmt = $this->pdo->prepare("
            SELECT pr.user_id, pr.expires_at, u.email
            FROM password_resets pr
            JOIN users u ON pr.user_id = u.id
            WHERE pr.token = ? 
            AND pr.expires_at > NOW() 
            AND pr.used_at IS NULL
        ");
        $stmt->execute([$hashedToken]);
        $reset = $stmt->fetch();
        
        if (!$reset) {
            throw new Exception("Invalid or expired reset token");
        }
        
        // Validate new password
        $validator = new PasswordValidator();
        $validation = $validator->validate($newPassword);
        
        if (!$validation['valid']) {
            throw new Exception("Password validation failed: " . implode(', ', $validation['errors']));
        }
        
        // Update password
        $this->pdo->beginTransaction();
        
        try {
            // Hash new password
            $hashedPassword = password_hash($newPassword, PASSWORD_ARGON2ID);
            
            // Update user password
            $stmt = $this->pdo->prepare("UPDATE users SET password_hash = ? WHERE id = ?");
            $stmt->execute([$hashedPassword, $reset['user_id']]);
            
            // Mark reset token as used
            $stmt = $this->pdo->prepare("UPDATE password_resets SET used_at = NOW() WHERE token = ?");
            $stmt->execute([$hashedToken]);
            
            // Invalidate all other tokens for this user
            $stmt = $this->pdo->prepare("DELETE FROM password_resets WHERE user_id = ? AND token != ?");
            $stmt->execute([$reset['user_id'], $hashedToken]);
            
            $this->pdo->commit();
            
            // Log password change
            $this->logPasswordChange($reset['user_id']);
            
            return true;
            
        } catch (Exception $e) {
            $this->pdo->rollBack();
            throw $e;
        }
    }
    
    private function sendResetEmail($email, $token) {
        $resetUrl = "https://yoursite.com/reset-password?token=" . urlencode($token);
        
        $subject = "Password Reset Request";
        $message = "
            A password reset was requested for your account.
            
            Click the link below to reset your password:
            $resetUrl
            
            This link will expire in 1 hour.
            
            If you did not request this reset, please ignore this email.
        ";
        
        // Use your preferred email service
        mail($email, $subject, $message);
    }
    
    private function logPasswordChange($userId) {
        $stmt = $this->pdo->prepare("
            INSERT INTO security_events (user_id, event_type, ip_address, created_at) 
            VALUES (?, 'password_changed', ?, NOW())
        ");
        $stmt->execute([$userId, $_SERVER['REMOTE_ADDR']]);
    }
}
?>

Two-Factor Authentication Integration

Adding 2FA to Password Authentication

<?php
class TwoFactorAuth {
    private $pdo;
    
    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }
    
    public function setupTwoFactor($userId) {
        // Generate secret key
        $secret = $this->generateSecret();
        
        // Store secret
        $stmt = $this->pdo->prepare("
            UPDATE users 
            SET two_factor_secret = ?, two_factor_enabled = 0 
            WHERE id = ?
        ");
        $stmt->execute([$secret, $userId]);
        
        return [
            'secret' => $secret,
            'qr_code_url' => $this->generateQRCode($secret)
        ];
    }
    
    public function verifyAndEnable($userId, $code) {
        $stmt = $this->pdo->prepare("SELECT two_factor_secret FROM users WHERE id = ?");
        $stmt->execute([$userId]);
        $secret = $stmt->fetchColumn();
        
        if ($this->verifyTOTP($secret, $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, 'backup_codes' => $backupCodes];
        }
        
        return ['success' => false];
    }
    
    public function verifyLogin($userId, $password, $totpCode = null) {
        // First verify password
        $stmt = $this->pdo->prepare("SELECT password_hash, two_factor_enabled, two_factor_secret FROM users WHERE id = ?");
        $stmt->execute([$userId]);
        $user = $stmt->fetch();
        
        if (!password_verify($password, $user['password_hash'])) {
            return false;
        }
        
        // If 2FA is enabled, verify TOTP
        if ($user['two_factor_enabled']) {
            if (!$totpCode) {
                throw new Exception("2FA code required");
            }
            
            if (!$this->verifyTOTP($user['two_factor_secret'], $totpCode)) {
                // Check backup codes as fallback
                if (!$this->verifyBackupCode($userId, $totpCode)) {
                    return false;
                }
            }
        }
        
        return true;
    }
    
    private function generateSecret($length = 32) {
        return bin2hex(random_bytes($length));
    }
    
    private function verifyTOTP($secret, $code) {
        // Simple TOTP implementation (use a library like sonata-project/GoogleAuthenticator for production)
        $timeStep = floor(time() / 30);
        
        for ($i = -1; $i <= 1; $i++) { // Allow ±30 seconds tolerance
            $expectedCode = $this->generateTOTP($secret, $timeStep + $i);
            if ($code === $expectedCode) {
                return true;
            }
        }
        
        return false;
    }
    
    private function generateTOTP($secret, $timeStep) {
        // Simplified TOTP - use proper library in production
        $data = pack('N*', 0, $timeStep);
        $hash = hash_hmac('sha1', $data, hex2bin($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 generateBackupCodes($userId) {
        $codes = [];
        
        for ($i = 0; $i < 10; $i++) {
            $codes[] = bin2hex(random_bytes(4));
        }
        
        // Store hashed backup codes
        $stmt = $this->pdo->prepare("DELETE FROM backup_codes WHERE user_id = ?");
        $stmt->execute([$userId]);
        
        $stmt = $this->pdo->prepare("INSERT INTO backup_codes (user_id, code_hash, created_at) VALUES (?, ?, NOW())");
        
        foreach ($codes as $code) {
            $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 = ? AND used_at IS NULL");
        $stmt->execute([$userId]);
        
        while ($backupCode = $stmt->fetch()) {
            if (password_verify($code, $backupCode['code_hash'])) {
                // Mark code as used
                $useStmt = $this->pdo->prepare("UPDATE backup_codes SET used_at = NOW() WHERE id = ?");
                $useStmt->execute([$backupCode['id']]);
                
                return true;
            }
        }
        
        return false;
    }
}
?>

Security Best Practices

1. Use Strong Hashing Algorithms

<?php
// Good - Use modern algorithms
$hash = password_hash($password, PASSWORD_ARGON2ID);

// Avoid deprecated methods
// md5($password) - NEVER use
// sha1($password) - NEVER use
// hash('sha256', $password) - Not sufficient without salt
?>

2. Implement Rate Limiting

<?php
class LoginRateLimiter {
    private $pdo;
    private $maxAttempts = 5;
    private $lockoutDuration = 300; // 5 minutes
    
    public function checkRateLimit($identifier) {
        $stmt = $this->pdo->prepare("
            SELECT COUNT(*) as attempts, MAX(attempted_at) as last_attempt
            FROM failed_login_attempts 
            WHERE identifier = ? 
            AND attempted_at > DATE_SUB(NOW(), INTERVAL ? SECOND)
        ");
        $stmt->execute([$identifier, $this->lockoutDuration]);
        $result = $stmt->fetch();
        
        if ($result['attempts'] >= $this->maxAttempts) {
            $remainingTime = $this->lockoutDuration - (time() - strtotime($result['last_attempt']));
            throw new Exception("Too many failed attempts. Try again in $remainingTime seconds.");
        }
        
        return true;
    }
    
    public function recordFailedAttempt($identifier) {
        $stmt = $this->pdo->prepare("
            INSERT INTO failed_login_attempts (identifier, attempted_at) 
            VALUES (?, NOW())
        ");
        $stmt->execute([$identifier]);
    }
    
    public function clearFailedAttempts($identifier) {
        $stmt = $this->pdo->prepare("DELETE FROM failed_login_attempts WHERE identifier = ?");
        $stmt->execute([$identifier]);
    }
}
?>

3. Secure Session Management

<?php
class SecureSessionManager {
    public 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');
        
        session_start();
        
        // Regenerate session ID on login
        if (!isset($_SESSION['initiated'])) {
            session_regenerate_id(true);
            $_SESSION['initiated'] = true;
        }
    }
    
    public function login($userId) {
        $this->startSecureSession();
        
        // Regenerate session ID to prevent session fixation
        session_regenerate_id(true);
        
        $_SESSION['user_id'] = $userId;
        $_SESSION['login_time'] = time();
        $_SESSION['last_activity'] = time();
    }
    
    public function logout() {
        if (session_status() === PHP_SESSION_ACTIVE) {
            $_SESSION = [];
            session_destroy();
        }
    }
    
    public function validateSession() {
        if (!isset($_SESSION['user_id'])) {
            return false;
        }
        
        // Check session timeout (30 minutes of inactivity)
        if (time() - $_SESSION['last_activity'] > 1800) {
            $this->logout();
            return false;
        }
        
        $_SESSION['last_activity'] = time();
        return true;
    }
}
?>

Password security is fundamental to application security. Always use PHP's built-in password hashing functions, implement strong validation policies, and consider additional security measures like two-factor authentication and rate limiting to protect user accounts from compromise.