PHP Fundamentals
User Login System
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:
- Create the login form and implement password verification
- Add the login_attempts table and implement rate limiting
- Test the "Remember Me" functionality by logging in and closing the browser
- Implement a "Forgot Password" feature with email verification
- Add two-factor authentication (2FA) using email or SMS codes
- Create an admin panel to view and manage login attempts
- 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