PHP Fundamentals

User Login System

15 min Lesson 42 of 45

User Login System

A secure login system is essential for authenticating users and managing their sessions. In this lesson, we'll build a complete login system with password verification, session management, and security features like remember me and account lockout.

Login Form HTML

Let's create a professional login form with modern styling:

login.php:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>User Login</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: Arial, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 20px;
        }

        .login-container {
            background: white;
            padding: 40px;
            border-radius: 10px;
            box-shadow: 0 10px 40px rgba(0,0,0,0.2);
            max-width: 450px;
            width: 100%;
        }

        h1 {
            color: #333;
            margin-bottom: 10px;
            text-align: center;
        }

        .subtitle {
            text-align: center;
            color: #777;
            margin-bottom: 30px;
            font-size: 14px;
        }

        .form-group {
            margin-bottom: 20px;
        }

        label {
            display: block;
            margin-bottom: 5px;
            color: #555;
            font-weight: bold;
        }

        input[type="text"],
        input[type="email"],
        input[type="password"] {
            width: 100%;
            padding: 12px;
            border: 2px solid #ddd;
            border-radius: 5px;
            font-size: 16px;
            transition: border-color 0.3s;
        }

        input:focus {
            outline: none;
            border-color: #667eea;
        }

        .checkbox-group {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 20px;
            font-size: 14px;
        }

        .checkbox-group label {
            display: flex;
            align-items: center;
            font-weight: normal;
            cursor: pointer;
        }

        .checkbox-group input[type="checkbox"] {
            margin-right: 5px;
            cursor: pointer;
        }

        .forgot-password {
            color: #667eea;
            text-decoration: none;
            transition: color 0.3s;
        }

        .forgot-password:hover {
            color: #5568d3;
            text-decoration: underline;
        }

        .error-message {
            background: #fee;
            color: #c33;
            padding: 12px;
            border-radius: 5px;
            margin-bottom: 20px;
            border-left: 4px solid #c33;
        }

        .success-message {
            background: #efe;
            color: #3c3;
            padding: 12px;
            border-radius: 5px;
            margin-bottom: 20px;
            border-left: 4px solid #3c3;
        }

        button {
            width: 100%;
            padding: 12px;
            background: #667eea;
            color: white;
            border: none;
            border-radius: 5px;
            font-size: 16px;
            font-weight: bold;
            cursor: pointer;
            transition: background 0.3s;
        }

        button:hover {
            background: #5568d3;
        }

        button:disabled {
            background: #ccc;
            cursor: not-allowed;
        }

        .register-link {
            text-align: center;
            margin-top: 20px;
            color: #555;
        }

        .register-link a {
            color: #667eea;
            text-decoration: none;
            font-weight: bold;
        }

        .login-attempts {
            background: #fff3cd;
            color: #856404;
            padding: 10px;
            border-radius: 5px;
            margin-bottom: 15px;
            font-size: 14px;
            text-align: center;
        }
    </style>
</head>
<body>
    <div class="login-container">
        <h1>Welcome Back</h1>
        <p class="subtitle">Please login to your account</p>

        <?php if (isset($error_message)): ?>
            <div class="error-message"><?php echo htmlspecialchars($error_message); ?></div>
        <?php endif; ?>

        <?php if (isset($success_message)): ?>
            <div class="success-message"><?php echo htmlspecialchars($success_message); ?></div>
        <?php endif; ?>

        <?php if (isset($attempts_warning)): ?>
            <div class="login-attempts"><?php echo $attempts_warning; ?></div>
        <?php endif; ?>

        <form method="POST" action="login.php" id="loginForm">
            <div class="form-group">
                <label for="username">Username or Email</label>
                <input type="text" id="username" name="username"
                       value="<?php echo htmlspecialchars($username ?? ''); ?>"
                       required autofocus>
            </div>

            <div class="form-group">
                <label for="password">Password</label>
                <input type="password" id="password" name="password" required>
            </div>

            <div class="checkbox-group">
                <label>
                    <input type="checkbox" name="remember_me" value="1">
                    Remember Me
                </label>
                <a href="forgot-password.php" class="forgot-password">Forgot Password?</a>
            </div>

            <button type="submit" <?php echo isset($account_locked) ? 'disabled' : ''; ?>>
                Login
            </button>
        </form>

        <div class="register-link">
            Don't have an account? <a href="register.php">Register here</a>
        </div>
    </div>
</body>
</html>

Login Logic with Security Features

Implement secure login logic with password verification and session management:

login.php (PHP logic at the top):
<?php
session_start();

// Database configuration
$host = 'localhost';
$dbname = 'your_database';
$db_username = 'your_username';
$db_password = 'your_password';

// Initialize variables
$error_message = '';
$success_message = '';
$username = '';

// Create database connection
try {
    $pdo = new PDO("mysql:host=$host;dbname=$dbname;charset=utf8mb4", $db_username, $db_password);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    die("Connection failed: " . $e->getMessage());
}

// Check if user is already logged in
if (isset($_SESSION['user_id'])) {
    header('Location: dashboard.php');
    exit;
}

// Handle logout message
if (isset($_GET['logout'])) {
    $success_message = 'You have been logged out successfully.';
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $username = trim($_POST['username'] ?? '');
    $password = $_POST['password'] ?? '';
    $remember_me = isset($_POST['remember_me']);

    // Validate inputs
    if (empty($username)) {
        $error_message = 'Please enter your username or email';
    } elseif (empty($password)) {
        $error_message = 'Please enter your password';
    } else {
        // Check login attempts (rate limiting)
        $ip_address = $_SERVER['REMOTE_ADDR'];
        $attempts = getLoginAttempts($pdo, $ip_address);

        if ($attempts >= 5) {
            $error_message = 'Too many failed login attempts. Please try again in 15 minutes.';
            $account_locked = true;
        } else {
            // Find user by username or email
            $stmt = $pdo->prepare('
                SELECT id, username, email, password, full_name, is_active
                FROM users
                WHERE username = ? OR email = ?
                LIMIT 1
            ');
            $stmt->execute([$username, $username]);
            $user = $stmt->fetch(PDO::FETCH_ASSOC);

            if ($user) {
                // Check if account is active
                if (!$user['is_active']) {
                    $error_message = 'Your account has been deactivated. Please contact support.';
                    logLoginAttempt($pdo, $ip_address, $username, false);
                }
                // Verify password
                elseif (password_verify($password, $user['password'])) {
                    // Successful login

                    // Regenerate session ID to prevent session fixation
                    session_regenerate_id(true);

                    // Set session variables
                    $_SESSION['user_id'] = $user['id'];
                    $_SESSION['username'] = $user['username'];
                    $_SESSION['full_name'] = $user['full_name'];
                    $_SESSION['email'] = $user['email'];
                    $_SESSION['logged_in_at'] = time();

                    // Update last login timestamp
                    $stmt = $pdo->prepare('UPDATE users SET last_login = NOW() WHERE id = ?');
                    $stmt->execute([$user['id']]);

                    // Handle "Remember Me"
                    if ($remember_me) {
                        setRememberMeCookie($pdo, $user['id']);
                    }

                    // Clear failed login attempts
                    clearLoginAttempts($pdo, $ip_address);

                    // Log successful login
                    logLoginAttempt($pdo, $ip_address, $username, true);

                    // Redirect to dashboard
                    header('Location: dashboard.php');
                    exit;
                } else {
                    // Invalid password
                    $error_message = 'Invalid username or password';
                    logLoginAttempt($pdo, $ip_address, $username, false);
                }
            } else {
                // User not found
                $error_message = 'Invalid username or password';
                logLoginAttempt($pdo, $ip_address, $username, false);
            }

            // Show attempts warning
            $remaining_attempts = 5 - getLoginAttempts($pdo, $ip_address);
            if ($remaining_attempts < 5 && $remaining_attempts > 0) {
                $attempts_warning = "Warning: $remaining_attempts login attempts remaining";
            }
        }
    }
}

/**
 * Get number of failed login attempts from this IP in the last 15 minutes
 */
function getLoginAttempts($pdo, $ip_address) {
    $stmt = $pdo->prepare('
        SELECT COUNT(*) FROM login_attempts
        WHERE ip_address = ?
        AND success = 0
        AND attempted_at > DATE_SUB(NOW(), INTERVAL 15 MINUTE)
    ');
    $stmt->execute([$ip_address]);
    return (int) $stmt->fetchColumn();
}

/**
 * Log a login attempt
 */
function logLoginAttempt($pdo, $ip_address, $username, $success) {
    $stmt = $pdo->prepare('
        INSERT INTO login_attempts (ip_address, username, success, attempted_at)
        VALUES (?, ?, ?, NOW())
    ');
    $stmt->execute([$ip_address, $username, $success ? 1 : 0]);
}

/**
 * Clear failed login attempts for this IP
 */
function clearLoginAttempts($pdo, $ip_address) {
    $stmt = $pdo->prepare('DELETE FROM login_attempts WHERE ip_address = ?');
    $stmt->execute([$ip_address]);
}

/**
 * Set "Remember Me" cookie
 */
function setRememberMeCookie($pdo, $user_id) {
    // Generate a random token
    $token = bin2hex(random_bytes(32));
    $hashed_token = hash('sha256', $token);

    // Store token in database (expires in 30 days)
    $stmt = $pdo->prepare('
        INSERT INTO remember_tokens (user_id, token, expires_at)
        VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 30 DAY))
    ');
    $stmt->execute([$user_id, $hashed_token]);

    // Set cookie (30 days)
    setcookie('remember_me', $token, [
        'expires' => time() + (30 * 24 * 60 * 60),
        'path' => '/',
        'secure' => true,      // HTTPS only
        'httponly' => true,    // Not accessible via JavaScript
        'samesite' => 'Strict' // CSRF protection
    ]);
}
?>
Security Warning: Always use password_verify() to check passwords, never compare hashed passwords directly. This function is designed to be secure against timing attacks.

Login Attempts Tracking Table

Create a table to track login attempts for rate limiting:

SQL:
CREATE TABLE login_attempts (
    id INT AUTO_INCREMENT PRIMARY KEY,
    ip_address VARCHAR(45) NOT NULL,
    username VARCHAR(100),
    success BOOLEAN DEFAULT FALSE,
    attempted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_ip_time (ip_address, attempted_at),
    INDEX idx_username (username)
);

-- Remember Me tokens table
CREATE TABLE remember_tokens (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    token VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    expires_at TIMESTAMP NOT NULL,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    INDEX idx_token (token),
    INDEX idx_expires (expires_at)
);

Remember Me Auto-Login

Implement automatic login using "Remember Me" cookies:

auto_login.php (include at the top of protected pages):
<?php
/**
 * Check for "Remember Me" cookie and auto-login
 * Include this at the top of your login.php or protected pages
 */
function checkRememberMe($pdo) {
    // Skip if already logged in
    if (isset($_SESSION['user_id'])) {
        return;
    }

    // Check for remember_me cookie
    if (!isset($_COOKIE['remember_me'])) {
        return;
    }

    $token = $_COOKIE['remember_me'];
    $hashed_token = hash('sha256', $token);

    // Find valid token in database
    $stmt = $pdo->prepare('
        SELECT rt.user_id, u.username, u.email, u.full_name, u.is_active
        FROM remember_tokens rt
        JOIN users u ON rt.user_id = u.id
        WHERE rt.token = ?
        AND rt.expires_at > NOW()
        AND u.is_active = 1
        LIMIT 1
    ');
    $stmt->execute([$hashed_token]);
    $result = $stmt->fetch(PDO::FETCH_ASSOC);

    if ($result) {
        // Valid token - auto login
        session_regenerate_id(true);

        $_SESSION['user_id'] = $result['user_id'];
        $_SESSION['username'] = $result['username'];
        $_SESSION['full_name'] = $result['full_name'];
        $_SESSION['email'] = $result['email'];
        $_SESSION['logged_in_at'] = time();
        $_SESSION['auto_logged_in'] = true;

        // Update last login
        $stmt = $pdo->prepare('UPDATE users SET last_login = NOW() WHERE id = ?');
        $stmt->execute([$result['user_id']]);

        return true;
    } else {
        // Invalid or expired token - remove cookie
        setcookie('remember_me', '', time() - 3600, '/');
        return false;
    }
}

// Call this function
checkRememberMe($pdo);
?>

Logout Functionality

Create a secure logout script that clears sessions and cookies:

logout.php:
<?php
session_start();

// Database connection (reuse your connection code)
require_once 'db_connect.php';

// Remove "Remember Me" token if exists
if (isset($_COOKIE['remember_me'])) {
    $token = $_COOKIE['remember_me'];
    $hashed_token = hash('sha256', $token);

    // Delete token from database
    $stmt = $pdo->prepare('DELETE FROM remember_tokens WHERE token = ?');
    $stmt->execute([$hashed_token]);

    // Remove cookie
    setcookie('remember_me', '', time() - 3600, '/', '', true, true);
}

// Unset all session variables
$_SESSION = [];

// Destroy session cookie
if (isset($_COOKIE[session_name()])) {
    setcookie(session_name(), '', time() - 3600, '/');
}

// Destroy the session
session_destroy();

// Redirect to login page
header('Location: login.php?logout=1');
exit;
?>
Pro Tip: Always regenerate session IDs after login using session_regenerate_id(true) to prevent session fixation attacks. The true parameter deletes the old session file.

Session Security Enhancements

Add additional security checks to prevent session hijacking:

session_security.php:
<?php
/**
 * Enhanced session security
 * Include this at the top of every protected page
 */

// Start session with secure settings
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1);    // HTTPS only
ini_set('session.use_only_cookies', 1);
ini_set('session.cookie_samesite', 'Strict');

session_start();

/**
 * Validate session security
 */
function validateSession() {
    // Check if user is logged in
    if (!isset($_SESSION['user_id'])) {
        return false;
    }

    // Check session age (expire after 2 hours of inactivity)
    if (isset($_SESSION['last_activity'])) {
        $inactive_time = time() - $_SESSION['last_activity'];
        if ($inactive_time > 7200) { // 2 hours
            session_unset();
            session_destroy();
            return false;
        }
    }

    // Update last activity time
    $_SESSION['last_activity'] = time();

    // Check user agent (basic fingerprinting)
    $current_ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
    if (isset($_SESSION['user_agent'])) {
        if ($_SESSION['user_agent'] !== $current_ua) {
            // User agent changed - possible session hijacking
            session_unset();
            session_destroy();
            return false;
        }
    } else {
        $_SESSION['user_agent'] = $current_ua;
    }

    // Check IP address (optional - may cause issues with changing IPs)
    // $current_ip = $_SERVER['REMOTE_ADDR'];
    // if (isset($_SESSION['ip_address']) && $_SESSION['ip_address'] !== $current_ip) {
    //     session_unset();
    //     session_destroy();
    //     return false;
    // }
    // $_SESSION['ip_address'] = $current_ip;

    return true;
}

// Validate the session
if (!validateSession()) {
    header('Location: login.php?session_expired=1');
    exit;
}
?>
Note: Be cautious with IP-based session validation. Some users have dynamic IPs (mobile networks, VPNs) that change frequently, which would incorrectly invalidate their sessions.
Exercise:
  1. Create the login form and implement password verification
  2. Add the login_attempts table and implement rate limiting
  3. Test the "Remember Me" functionality by logging in and closing the browser
  4. Implement a "Forgot Password" feature with email verification
  5. Add two-factor authentication (2FA) using email or SMS codes
  6. Create an admin panel to view and manage login attempts
  7. Add activity logging (login times, IP addresses, browser info)

Best Practices Summary

  • Use password_verify() to check passwords securely
  • Regenerate session IDs after successful login
  • Implement rate limiting to prevent brute force attacks
  • Validate sessions with user agent and activity timeouts
  • Use secure cookies with httponly, secure, and samesite attributes
  • Hash remember tokens before storing in database
  • Clear sessions properly on logout
  • Log all login attempts for security monitoring