Almost every real application needs some way to identify its users — a PHP Login System is the foundation of that. In this tutorial, you will combine everything from earlier tutorials — forms, MySQLi, prepared statements, and sessions — into one complete, working authentication flow.
This is Tutorial 10 of 12 in our PHP Advanced series. You will build a full PHP Login System with signup, login, session-based access control, and a basic forgot-password flow. Every section includes a complete, runnable script — build each file exactly as shown and test it against your own database.
Setting Up the Users Table
Before writing any PHP Login System code, create a users table in phpMyAdmin using this SQL inside your existing school_db database.
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100) UNIQUE,
password VARCHAR(255),
reset_token VARCHAR(255) NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Notice the email column is marked UNIQUE, preventing two accounts from ever sharing the same email address — an essential rule for any PHP Login System.
Example 1: The Config File
Reuse the same config pattern from earlier tutorials to keep database credentials in one place.
<?php
// File: config.php
$host = "localhost";
$username = "root";
$password = "";
$dbname = "school_db";
$conn = mysqli_connect($host, $username, $password, $dbname);
if (!$conn) {
die("Connection failed: " . mysqli_connect_error());
}
?>
Example 2: Building the Signup Form
The signup page collects a name, email, and password, then inserts a new user into the database.
<?php
require_once "config.php";
if (isset($_POST["signup"])) {
$name = trim($_POST["name"]);
$email = trim($_POST["email"]);
$password = $_POST["password"];
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
$stmt = mysqli_prepare($conn, "INSERT INTO users (name, email, password) VALUES (?, ?, ?)");
mysqli_stmt_bind_param($stmt, "sss", $name, $email, $hashedPassword);
if (mysqli_stmt_execute($stmt)) {
echo "Account created successfully! You can now log in.";
} else {
echo "Signup failed: " . mysqli_error($conn);
}
}
?>
<form method="POST">
Name: <input type="text" name="name" required>
Email: <input type="email" name="email" required>
Password: <input type="password" name="password" required>
<input type="submit" name="signup" value="Sign Up">
</form>
Notice password_hash() runs before the password ever touches the database. A PHP Login System must never store plain-text passwords — password_hash() converts it into a secure, unreadable format automatically.
Example 3: Why Hashing Matters
Let’s see exactly what password_hash() produces, so you understand what actually gets stored.
<?php
$plainPassword = "mySecret123";
$hashed = password_hash($plainPassword, PASSWORD_DEFAULT);
echo "Original: " . $plainPassword . "<br>";
echo "Hashed: " . $hashed;
?>
Running this shows a long, scrambled string completely unrelated to the original password visually. Even if someone accessed your database directly, they could not read actual passwords — this is the entire point of a secure PHP Login System.
Example 4: Building the Login Form
The login page checks submitted credentials against the stored hashed password using password_verify().
<?php
session_start();
require_once "config.php";
if (isset($_POST["login"])) {
$email = trim($_POST["email"]);
$password = $_POST["password"];
$stmt = mysqli_prepare($conn, "SELECT * FROM users WHERE email = ?");
mysqli_stmt_bind_param($stmt, "s", $email);
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
$user = mysqli_fetch_assoc($result);
if ($user && password_verify($password, $user["password"])) {
$_SESSION["user_id"] = $user["id"];
$_SESSION["user_name"] = $user["name"];
echo "Login successful! Welcome, " . $user["name"];
} else {
echo "Invalid email or password.";
}
}
?>
<form method="POST">
Email: <input type="email" name="email" required>
Password: <input type="password" name="password" required>
<input type="submit" name="login" value="Log In">
</form>
password_verify() compares the plain-text password entered at login against the hashed version stored during signup, returning true only if they actually match. This PHP Login System never compares raw password strings directly.
Example 5: Protecting a Page with Session Checks
Once logged in, other pages should confirm a valid session exists before showing protected content.
<?php
session_start();
if (!isset($_SESSION["user_id"])) {
echo "Please log in to view this page.";
exit;
}
echo "Welcome to your dashboard, " . $_SESSION["user_name"];
?>
The exit statement stops the script immediately after showing the message, preventing any dashboard content below it from accidentally displaying to a logged-out visitor.
Example 6: Building a Logout Script
Logging out clears the session completely, matching the pattern from the earlier Sessions tutorial.
<?php
session_start();
session_unset();
session_destroy();
echo "You have been logged out.";
?>
Example 7: Forgot Password — Requesting a Reset
A basic forgot-password flow generates a unique reset token, stores it against the user’s account, and would normally email it — here we display it directly for learning purposes.
<?php
require_once "config.php";
if (isset($_POST["forgot"])) {
$email = trim($_POST["email"]);
$token = bin2hex(random_bytes(16));
$stmt = mysqli_prepare($conn, "UPDATE users SET reset_token = ? WHERE email = ?");
mysqli_stmt_bind_param($stmt, "ss", $token, $email);
if (mysqli_stmt_execute($stmt) && mysqli_stmt_affected_rows($stmt) > 0) {
echo "Reset token generated: " . $token;
echo "<br>In a real project, this token would be emailed to the user instead of displayed here.";
} else {
echo "No account found with that email.";
}
}
?>
<form method="POST">
Email: <input type="email" name="email" required>
<input type="submit" name="forgot" value="Request Password Reset">
</form>
The bin2hex(random_bytes(16)) combination generates a secure, unpredictable token, making it extremely difficult for anyone to guess a valid reset token for someone else’s account.
Example 8: Forgot Password — Setting a New Password
The final step verifies the token and updates the account with a freshly hashed new password.
<?php
require_once "config.php";
if (isset($_POST["reset"])) {
$token = trim($_POST["token"]);
$newPassword = $_POST["new_password"];
$stmt = mysqli_prepare($conn, "SELECT * FROM users WHERE reset_token = ?");
mysqli_stmt_bind_param($stmt, "s", $token);
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
$user = mysqli_fetch_assoc($result);
if ($user) {
$hashedPassword = password_hash($newPassword, PASSWORD_DEFAULT);
$updateStmt = mysqli_prepare($conn, "UPDATE users SET password = ?, reset_token = NULL WHERE id = ?");
mysqli_stmt_bind_param($updateStmt, "si", $hashedPassword, $user["id"]);
mysqli_stmt_execute($updateStmt);
echo "Password reset successful! You can now log in with your new password.";
} else {
echo "Invalid or expired reset token.";
}
}
?>
<form method="POST">
Reset Token: <input type="text" name="token" required>
New Password: <input type="password" name="new_password" required>
<input type="submit" name="reset" value="Reset Password">
</form>
Notice reset_token gets set back to NULL after a successful reset — this prevents the same token being reused again later, closing the security gap. For complete official documentation on password hashing functions, refer to the official PHP password hashing manual.
Putting the Full PHP Login System Together
A complete real project would organize these examples into separate files: signup.php, login.php, dashboard.php, logout.php, forgot-password.php, and reset-password.php — each require_once-ing the same config.php, exactly matching the Include/Require pattern from an earlier tutorial.
Common PHP Login System Mistakes to Avoid
Never store passwords using plain md5() or without hashing at all — always use password_hash() and password_verify(). Never trust a reset token without checking it actually exists in the database first. Always call session_start() at the very top of every protected page, before any HTML output.
What’s Next?
You now have a complete, working PHP Login System covering signup, secure login, session protection, logout, and a basic forgot-password flow. In the next tutorial, we will explore Composer, the tool that manages external libraries and autoloading automatically in almost every modern PHP project.
If you missed the previous lesson, check out Tutorial 9: PHP Namespaces Guide, or explore all lessons in our PHP category.
Practice Exercise
Complete the following tasks to reinforce what you learned in this tutorial. Build and test every file yourself against your own users table.
- Create the users table using the SQL provided at the start of this tutorial
- Build signup.php exactly as shown in Example 2, and create two test accounts with different emails
- Build login.php exactly as shown in Example 4, and confirm both test accounts can log in successfully
- Try logging in with a wrong password on purpose and confirm the “Invalid email or password” message appears
- Build dashboard.php using the session-check pattern from Example 5, and confirm it blocks access when not logged in
- Build logout.php from Example 6, and confirm the dashboard blocks access again immediately after logging out
- Build the two forgot-password files from Examples 7 and 8, and walk through the full flow: request a token, then use it to set a new password, then log in with the new password
Bonus Challenge: Add an extra check to signup.php that prevents duplicate email addresses from creating a second account — attempt a signup using an email that already exists, and display a clear “Email already registered” message instead of a raw database error.

