Build a Secure PHP Login System from Scratch
Introduction to Modern PHP Authentication
Authentication is the gateway to your web application. Whether you are building an e-commerce platform, a SaaS product, or a simple blog with a private admin panel, the ability to securely log users in and out is non-negotiable.
In the early days of PHP, building a login system was often a messy affair involving raw mysql_query() calls and passwords stored in plain text. Today, in 2026, such practices are not only obsolete but highly dangerous. The threat landscape has evolved, and attackers have access to massive compute power capable of cracking weak systems in seconds.
In this comprehensive guide, we will build a rock-solid, production-ready login system using modern PHP 8+, PDO (PHP Data Objects) for secure database interactions, and the latest password hashing algorithms. We will not be using any frameworks like Laravel or Symfony for this tutorial—understanding the raw mechanics of authentication is a vital skill for every backend developer.
Phase 1: Database Setup and Architecture
Before we write a single line of PHP, we need a place to store our users. We will use MySQL.
A secure user table must account for more than just a username and password. We need to track when the account was created, and ideally, provide a structure that allows for future enhancements (like email verification or password resets).
Execute this SQL to create your database and table:
sqlCREATE DATABASE apex_auth; USE apex_auth; CREATE TABLE users ( id INT(11) AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, email VARCHAR(100) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );
Why VARCHAR(255) for the password?
We are not storing the raw password. We will be storing a cryptographic hash of the password using the PASSWORD_DEFAULT algorithm in PHP (which currently uses Bcrypt, but may upgrade to Argon2 in the future). Bcrypt hashes are always 60 characters, but giving the column 255 characters ensures our database won't break if PHP upgrades its default algorithm to something that produces longer hashes.
Phase 2: Secure Database Connection (PDO)
The most common vulnerability in legacy PHP applications is SQL Injection (SQLi). SQLi occurs when user input is concatenated directly into a SQL string.
To prevent this, we MUST use PDO with Prepared Statements. Prepared statements separate the SQL logic from the data payload, making SQL injection mathematically impossible.
Create a file named config.php:
php<?php // config.php $host = '127.0.0.1'; $db = 'apex_auth'; $user = 'root'; $pass = ''; // Use a strong password in production! $charset = 'utf8mb4'; $dsn = "mysql:host=$host;dbname=$db;charset=$charset"; $options = [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, // Crucial for security! ]; try { $pdo = new PDO($dsn, $user, $pass, $options); } catch (PDOException $e) { // In production, log this error instead of echoing it to prevent leaking DB details. error_log($e->getMessage()); die("Database connection failed."); } ?>
Phase 3: Registration and Password Hashing
When a user registers, we must capture their input, validate it, hash the password, and store it.
Never store plain text passwords. Even MD5 and SHA-1 hashes are considered "plain text" by modern hacking standards because they can be cracked instantly using rainbow tables. PHP provides the password_hash() function which automatically generates a secure salt and uses a slow algorithm to thwart brute-force attacks.
Here is the logic for register.php:
php<?php require 'config.php'; if ($_SERVER["REQUEST_METHOD"] == "POST") { $username = trim($_POST['username']); $email = trim($_POST['email']); $password = $_POST['password']; // Basic Validation if(empty($username) || empty($email) || empty($password)){ die("All fields are required."); } if(strlen($password) < 8){ die("Password must be at least 8 characters long."); } // Hash the password $hashed_password = password_hash($password, PASSWORD_DEFAULT); // Insert into database using Prepared Statements $sql = "INSERT INTO users (username, email, password) VALUES (?, ?, ?)"; $stmt = $pdo->prepare($sql); try { $stmt->execute([$username, $email, $hashed_password]); echo "Registration successful! You can now login."; } catch (PDOException $e) { if ($e->getCode() == 23000) { echo "Username or email already exists."; } else { echo "An error occurred during registration."; } } } ?>
Phase 4: Secure Login Authentication
Now that the user is in the database, let's authenticate them. The login process involves fetching the user record by their email/username, and then verifying the provided password against the stored hash using password_verify().
Here is the logic for login.php:
php<?php require 'config.php'; session_start(); if ($_SERVER["REQUEST_METHOD"] == "POST") { $email = trim($_POST['email']); $password = $_POST['password']; // Fetch user by email $sql = "SELECT id, username, password FROM users WHERE email = ?"; $stmt = $pdo->prepare($sql); $stmt->execute([$email]); $user = $stmt->fetch(); // Verify user exists and password is correct if ($user && password_verify($password, $user['password'])) { // Session Hijacking Prevention session_regenerate_id(true); $_SESSION['loggedin'] = true; $_SESSION['user_id'] = $user['id']; $_SESSION['username'] = $user['username']; header("Location: dashboard.php"); exit; } else { // Generic error message prevents Username Enumeration attacks echo "Invalid email or password."; } } ?>
Phase 5: Session Management and Security
Notice in the login script, we called session_regenerate_id(true). This is a critical defense against Session Fixation attacks. Without it, an attacker could force a known session ID onto a victim, wait for them to log in, and then use that same session ID to access their account.
Furthermore, you must secure your PHP sessions at the server level. Add these lines to the very top of your scripts, before calling session_start(), to harden the session cookies:
phpini_set('session.cookie_httponly', 1); // Prevents JS access (mitigates XSS) ini_set('session.cookie_secure', 1); // Ensures cookies are only sent over HTTPS ini_set('session.use_only_cookies', 1); // Prevents passing session ID in URL
Phase 6: Protecting Pages (Authorization)
Authentication proves who the user is. Authorization proves what they are allowed to see. To protect your dashboard.php, you simply check the session:
php<?php session_start(); // Check if user is logged in if (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) { header("Location: login.php"); exit; } echo "Welcome to your secure dashboard, " . htmlspecialchars($_SESSION['username']) . "!"; ?>
Always use htmlspecialchars() when outputting user data (like their username) to the screen to prevent Cross-Site Scripting (XSS).
Conclusion
You have now built a robust, secure authentication system in pure PHP. We successfully mitigated SQL Injection using PDO, defended against brute-force attacks using password_hash(), prevented Session Fixation using ID regeneration, and secured our cookies using HttpOnly flags.
While this system is secure, remember that security is a moving target. Always keep your PHP version updated, enforce HTTPS across your entire domain, and consider implementing Two-Factor Authentication (2FA) for sensitive applications.