PHP Email Verification For New User Accounts

We have already seen how to create login and registrations system in PHP. In this step-by-step tutorial you will learn how to verify new user’s email by sending OTP to the email after user creates new account using PHP.

We also seen that how to send emails in PHP. Basically here we are going to combine the login and registration system and sending emails functionality to achieve this.

Steps to Create PHP Email Verification Feature For New User Accounts

Step 1: Create a Project Folder

Open your xampp htdocs folder or your localhost www directory and create a new folder called php-login-email-verification.

Step 2: Install PHPMailer In the Folder

To send OTP to the emails for verification, we will use PHPMailer. Therefore, install phpmailer via Composer in the project folder.

composer require phpmailer/phpmailer

Now we need to create PHP files to build this project. Here is the project folder structure:

PHP login with email verification project folder structure

Step 3: Database Setup

  • Database Name: php_login_verification
  • Table Name: users

Use the following SQL code to create the users table and its columns:

CREATE TABLE `users` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `name` varchar(30) NOT NULL,
    `email` varchar(30) NOT NULL,
    `password` varchar(150) NOT NULL,
    `verified` int(1) NOT NULL DEFAULT 0,
    `verification_code` tinytext DEFAULT NULL,
    `otp` int(8) NOT NULL DEFAULT 0,
    PRIMARY KEY (`id`),
    UNIQUE KEY `email` (`email`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci;

Step 4: Database Connection “db_connection.php”

In the root of the project folder create db_connection.php for the database connection:

<?php
$db_host = 'localhost';
$db_user = 'root';
$db_password = '';
$db_name = 'php_login_verification';

$conn = new mysqli($db_host, $db_user, $db_password, $db_name);

// CHECK DATABASE CONNECTION
if($conn->error){
    echo "Connection Failed - ".$db_connection->connect_error;
    exit;
}

Step 5: Create “Mail.php” For Sending Emails

The send() method of the Mail class sends emails through PHPMailer. Change protected $pass and protected $sender_email according to yours.

<?php
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;

require __DIR__ . "/vendor/autoload.php";
class Mail
{
    protected $pass = "***your_app_password***";
    protected $sender_email = "***[email protected]****";
    protected $mailer;

    function __construct()
    {
        $this->mailer = new PHPMailer(true);
    }

    function send($receiver_email, $otp)
    {
        try {
            $this->mailer->isSMTP();
            $this->mailer->SMTPDebug = SMTP::DEBUG_OFF;
            $this->mailer->Host = 'smtp.gmail.com';
            $this->mailer->Port = 587;
            $this->mailer->SMTPSecure = "TLS";
            $this->mailer->SMTPAuth = true;

            $this->mailer->Username   = $this->sender_email;
            $this->mailer->Password   = $this->pass;

            $this->mailer->setFrom($this->sender_email, 'OTP from PHP Login');
            $this->mailer->addAddress($receiver_email);
            $this->mailer->isHTML(true);
            $this->mailer->Subject = "OPT for email verification.";
            $this->mailer->Body = "Your OTP is - $otp valid for 10 minutes.";
            $this->mailer->send();
            return true;
        } catch (Exception $e) {
            // echo "Message could not be sent. Mailer Error: {$this->mailer->ErrorInfo}";
            return false;
        }
    }
}

Step 6: Register a New User & Send Verification OTP to the Email

To register a new user we have to create the register.php and on_register.php.

on_register.php contains the on_register() function which contains the code for inserting new users, and this on_register() function will be called when the sign up form is submitted.

<?php
session_start();
if (isset($_SESSION['logged_user_id'])) {
    header('Location: home.php');
    exit;
}

if ($_SERVER["REQUEST_METHOD"] === "POST") :
    require_once __DIR__ . "/db_connection.php";
    require_once __DIR__ . "/on_register.php";
    if (
        isset($conn) &&
        isset($_POST["name"]) &&
        isset($_POST["email"]) &&
        isset($_POST["password"])
    ) {
        $result = on_register($conn);
    }
endif;

// If the user is registered successfully, don't show the post values.
$show = isset($result["form_reset"]) ? true : false;
function post_value($field)
{
    global $show;
    if (isset($_POST[$field]) && !$show) {
        echo 'value="' . trim(htmlspecialchars($_POST[$field])) . '"';
        return;
    }
}
?>
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Sign Up</title>
    <link rel="stylesheet" href="./style.css">
</head>

<body>
    <div class="container">
        <h1>Sign Up</h1>
        <form action="" method="POST" id="theForm">
            <label for="user_name">Name: <span></span></label>
            <input type="text" class="input" name="name" <?php post_value("name"); ?> id="user_name" placeholder="Your name">

            <label for="user_email">Email: <span></span></label>
            <input type="email" class="input" name="email" <?php post_value("email"); ?> id="user_email" placeholder="Your email">

            <label for="user_pass">Password: <span></span></label>
            <input type="password" class="input" name="password" <?php post_value("password"); ?> id="user_pass" placeholder="New password">
            <?php if (isset($result["msg"])) { ?>
                <p class="msg<?php if ($result["ok"] === 0) {
                                    echo " error";
                                } ?>"><?php echo $result["msg"]; ?></p>
            <?php } ?>
            <input type="submit" value="Sign Up">
            <div class="link"><a href="./login.php">Login</a></div>
        </form>
    </div>
    <script>
        <?php
        if (isset($result["field_error"])) { ?>
            let field_error = <?php echo json_encode($result["field_error"]); ?>;
            let el = null;
            let msg_el = null;
            for (let i in field_error) {
                el = document.querySelector(`input[name="${i}"]`);
                el.classList.add("error");
                msg_el = document.querySelector(`label[for="${el.getAttribute("id")}"] span`);
                msg_el.innerText = field_error[i];
            }
        <?php } ?>
    </script>
</body>

</html>

In the on_register() function, you can see that after inserting a new user, the system sends an OTP to the user’s email for verification.

<?php
require __DIR__ . "/Mail.php";

/**
 * Handle user registration.
 * 
 * This function handles the process of registering a new user.
 * It performs form validation, checks email uniqueness, hashes the password,
 * generates an OTP (One-Time Password) for email verification, and sends
 * a verification email to the user.
 * 
 * @param mysqli $conn Database connection
 * @return array Registration result
 */
function on_register($conn)
{
    $name = htmlspecialchars(trim($_POST['name']));
    $email = trim($_POST['email']);
    $pass = trim($_POST['password']);

    // Check for empty fields
    if (empty($name) || empty($email) || empty($pass)) {
        $arr = [];
        if (empty($name)) $arr["name"] = "Must not be empty.";
        if (empty($email)) $arr["email"] = "Must not be empty.";
        if (empty($pass)) $arr["password"] = "Must not be empty.";
        return [
            "ok" => 0,
            "field_error" => $arr
        ];
    }

    // Validate email address
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        return [
            "ok" => 0,
            "field_error" => [
                "email" => "Invalid email address."
            ]
        ];
    }

    // Check password length
    if (strlen($pass) < 4) {
        return [
            "ok" => 0,
            "field_error" => [
                "password" => "Must be at least 4 characters."
            ]
        ];
    }

    // Check if email is already registered
    $sql = "SELECT `email` FROM `users` WHERE `email` = ?";
    $stmt = $conn->prepare($sql);
    $stmt->bind_param("s", $email);
    $stmt->execute();
    $stmt->store_result();

    if ($stmt->num_rows !== 0) {
        return [
            "ok" => 0,
            "field_error" => [
                "email" => "This Email is already registered."
            ]
        ];
    }

    // Hash password and generate OTP
    $pass = password_hash($pass, PASSWORD_DEFAULT);
    $otp = rand(100000, 999999);
    $token = [
        "email" => $email,
        "expiry" => time()
    ];
    $token = base64_encode(serialize($token));
    $sql = "INSERT INTO `users` (`name`, `email`, `password`,`verification_code`,`otp`) VALUES (?,?,?,?,?)";
    $stmt = $conn->prepare($sql);
    $stmt->bind_param("ssssi", $name, $email, $pass, $token, $otp);
    $is_inserted = $stmt->execute();
    if ($is_inserted) {
        // Sending OTP to the mail
        $mail =  new Mail();
        $send_email = $mail->send($email, $otp);

        if ($send_email) {
            header("Location: verify.php?token=$token");
            exit;
        }
        return [
            "ok" => 1,
            "msg" => "You have been registered successfully, But failed to send the verification email.",
            "form_reset" => true
        ];
    }
    return [
        "ok" => 0,
        "msg" => "Something going wrong!"
    ];
}

Step 7: Verify Email by Entering Correct OTP

After successfully registering, the system automatically redirects the user to verify.php with a unique token. The base64-encoded token will contain an array containing the user’s email and the token expiration time.

/verify.php?token=YToyOntzOjU6ImVtYWlsIjtzOjIwOiJoZWxsb3dvcmxkQGdtYWlsLmNvbSI7czo2OiJleHBpcnkiO2k6MTY5MDA5OTI2OTt9
<?php
/**
 * Verify OTP (One-Time Password) for user email verification.
 * 
 * This function compares the OTP provided by the user with the OTP stored in the database.
 * If they match, it updates the user's verification status to mark the email as verified.
 * 
 * @param mysqli $conn Database connection
 * @param int $id User ID
 * @param string $db_otp OTP stored in the database
 * @param string $user_otp OTP provided by the user
 * @return bool Returns true if OTP is verified and user's verification status is updated successfully, otherwise returns false
 */
function otp_verify($conn, $id, $db_otp, $user_otp)
{
    // Compare OTP provided by the user with the OTP stored in the database
    if ($db_otp == $user_otp) {
        // If OTPs match, update user's verification status to mark the email as verified
        $sql = "UPDATE `users` SET `verified`=1 WHERE `id`=$id";
        // Execute the SQL query to update verification status
        if ($conn->query($sql)) {
            return true; // Return true if verification status is updated successfully
        }
        return false; // Return false if there is an error updating verification status
    }
    return false; // Return false if OTPs do not match
}
<?php
// Redirect to login page if token is not provided or invalid
if (!isset($_GET["token"]) || !base64_decode($_GET["token"], true)) {
    header("Location: login.php");
    exit;
}

// Decode and unserialize token
$encoded_token = $_GET["token"];
$token = unserialize(base64_decode($_GET["token"], true));

// Redirect to logout page if token is invalid or missing required data
if (!is_array($token) || !isset($token["email"]) || !filter_var($token["email"], FILTER_VALIDATE_EMAIL)) {
    header("Location: logout.php");
    exit;
}

$error = NULL;
$is_valid_otp = NULL;

// Include database connection
require_once __DIR__ . "/db_connection.php";

// Retrieve user data from database based on provided email and verification status
$sql = "SELECT `id`,`verification_code`,`otp` FROM `users` WHERE `email` = ? AND `verified`=0";
$stmt = $conn->prepare($sql);
$stmt->bind_param("s", $token["email"]);
$stmt->execute();

// Fetch user data
$data = $stmt->get_result();
$row = $data->fetch_array(MYSQLI_ASSOC);

// Redirect to logout page if user data is not found or verification code does not match
if ($row === NULL || $row["verification_code"] !== $encoded_token) {
    header("Location: logout.php");
    exit;
}

// Check if OTP has expired
if ((time() - $token["expiry"]) > 600) {
    $error = "Your previous OTP has expired, <strong>resend a new OTP</strong> to verify your email.";
}

// Verify OTP if submitted
if ($error === NULL && $_SERVER["REQUEST_METHOD"] === "POST" && isset($_POST["otp"])) {
    require_once __DIR__ . "/otp_verify.php";
    $is_valid_otp = otp_verify($conn, $row["id"], $row["otp"], $_POST["otp"]);
}
?>
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Email verification</title>
    <link rel="stylesheet" href="./style.css">
</head>

<body>
    <div class="container">
        <h1>Enter OTP</h1>
        <?php if ($error !== NULL) { ?>
            <!-- Display error message and provide option to resend OTP -->
            <p class="msg error"><?php echo $error; ?></p>
            <div class="link"><a href="./resend_otp.php?token=<?php echo $encoded_token; ?>">Resend</a></div>
        <?php } elseif ($is_valid_otp === true) { ?>
            <!-- Display success message if OTP is valid -->
            <p class="msg">Your <strong>email</strong> has been <strong>successfully verified</strong>.</p>
            <div class="link"><a href="./login.php">Login</a></div>

        <?php } else { ?>
            <!-- Display OTP verification form -->
            <form action="" method="POST">
                <p>Please <strong>enter the OTP</strong> which has been sent to your email for verification.</p>
                <label for="otp">Your OTP: <span><?php echo ($is_valid_otp === false) ? "Invalid OTP" : ""; ?></span></label>
                <input type="text" class="input<?php echo ($is_valid_otp === false) ? " error" : ""; ?>" name="otp" id="otp" placeholder="OTP">
                <input type="submit" value="Verify">
                <div class="link"><a href="./resend_otp.php?token=<?php echo $encoded_token; ?>">Resend</a></div>
            </form>
        <?php } ?>
    </div>
</body>

</html>

Step 8: Create Login Feature

login.php and on_login.php contain the code to login for existing users whose email is verified. The on_login() function will be called on submission of the login form.

<?php
/**
 * Handle user login.
 * 
 * This function handles user login by validating input fields,
 * checking if the provided email exists in the database, verifying
 * if the email is verified, and verifying the password.
 * 
 * @param mysqli $conn Database connection
 * @return array Login result
 */
function on_login($conn)
{
    // Retrieve form data
    $email = trim($_POST['email']);
    $pass = trim($_POST['password']);

    // Check for empty fields
    if (empty($email) || empty($pass)) {
        $arr = [];
        if (empty($email)) $arr["email"] = "Must not be empty.";
        if (empty($pass)) $arr["password"] = "Must not be empty.";
        return [
            "ok" => 0,
            "field_error" => $arr
        ];
    } 
    // Validate email address
    elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        return [
            "ok" => 0,
            "field_error" => [
                "email" => "Invalid email address."
            ]
        ];
    }

    // Retrieve user data from database based on provided email
    $sql = "SELECT * FROM `users` WHERE `email` = ?";
    $stmt = $conn->prepare($sql);
    $stmt->bind_param("s", $email);
    $stmt->execute();

    $data = $stmt->get_result();
    $row = $data->fetch_array(MYSQLI_ASSOC);

    // If user data is not found in database
    if ($row === NULL) {
        return [
            "ok" => 0,
            "field_error" => [
                "email" => "This email is not registered."
            ]
        ];
    }

    // If user email is not verified, redirect to verification page
    if ($row["verified"] === 0) {
        header("Location: verify.php?token={$row["verification_code"]}");
        exit;
    }

    // Verify password
    $password_check = password_verify($pass, $row["password"]);
    if ($password_check === false) {
        return [
            "ok" => 0,
            "field_error" => [
                "password" => "Incorrect Password."
            ]
        ];
    }

    // If login is successful, set session variable and redirect to home page
    $_SESSION['logged_user_id'] = $row["id"];
    header('Location: home.php');
    exit;
}
<?php
session_start();
if(isset($_SESSION['logged_user_id'])){
    header('Location: home.php');
    exit;
}

if ($_SERVER["REQUEST_METHOD"] === "POST") :
    require_once __DIR__ . "/db_connection.php";
    require_once __DIR__."/on_login.php";
    if (isset($conn) && isset($_POST["email"]) && isset($_POST["password"])) {
        $result = on_login($conn);
    }
endif;

function post_value($field){
    if(isset($_POST[$field])){
        echo 'value="'.trim(htmlspecialchars($_POST[$field])).'"';
        return;
    }    
}
?>
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Login Page</title>
    <link rel="stylesheet" href="./style.css">
</head>

<body>
    <div class="container">
        <h1>Login</h1>
        <form action="" method="POST">
            <label for="user_email">Email: <span></span></label>
            <input type="email" class="input" name="email" <?php post_value("email"); ?> id="user_email" placeholder="Your email">

            <label for="user_pass">Password: <span></span></label>
            <input type="password" class="input" name="password" <?php post_value("password"); ?> id="user_pass" placeholder="Your password">
            
            <input type="submit" value="Login">
            <div class="link"><a href="./register.php">Sign Up</a></div>
        </form>
    </div>
    <script>
        <?php 
        if(isset($result["field_error"])){ ?>
        let field_error = <?php echo json_encode($result["field_error"]); ?>;
        let el = null;
        let msg_el = null;
        for(let i in field_error){
            el = document.querySelector(`input[name="${i}"]`);
            el.classList.add("error");
            msg_el = document.querySelector(`label[for="${el.getAttribute("id")}"] span`);
            msg_el.innerText = field_error[i];
        }
        <?php } ?>
    </script>
</body>

</html>

Step 9: Create Home Page for the Logged in User

After successfully logging in, the system redirects the user to home.php, where get_user.php fetches the user information from the database.

<?php
/**
 * Retrieve user data by ID.
 * 
 * This function fetches user data from the database based on the provided user ID.
 * 
 * @param mysqli $conn Database connection
 * @param int $id User ID
 * @return array|bool Returns user data as an array if found, otherwise returns false
 */
function get_user($conn, $id)
{
    // Validate user ID
    if (!filter_var($id, FILTER_VALIDATE_INT)) {
        return false; // Return false if user ID is not valid
    }
    
    // Retrieve user data from database based on provided user ID
    $sql = "SELECT * FROM `users` WHERE `id` = ?";
    $stmt = $conn->prepare($sql);
    $stmt->bind_param("i", $id);
    $stmt->execute();
    $data = $stmt->get_result();
    $row = $data->fetch_array(MYSQLI_ASSOC);

    // Return user data as an array if found, otherwise return false
    if ($row === NULL) {
        return false; // Return false if user data is not found
    }
    return $row; // Return user data as an array
}
<?php
session_start();
session_regenerate_id(true);

if (!isset($_SESSION['logged_user_id']) || empty($_SESSION['logged_user_id']) || !is_numeric($_SESSION['logged_user_id'])) {
    header('Location: logout.php');
    exit;
}
require_once __DIR__ . "/db_connection.php";
require_once __DIR__ . "/get_user.php";
// Get the User by ID that stored in the session
$user = get_user($conn, $_SESSION['logged_user_id']);
// If User is Empty
if ($user === false) {
    header('Location: logout.php');
    exit;
}
?>
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Home</title>
    <link rel="stylesheet" href="./style.css">
</head>

<body>
    <div class="container">
        <div class="profile">
            <img src="https://robohash.org/set_set3/<?php echo $user["id"]; ?>?size=200x200" alt="<?php echo $user["name"]; ?>">
            <h2><?php echo $user["name"]; ?><span><?php echo $user["email"]; ?></span></h2>
            <a href="./logout.php">Log out</a>
        </div>

    </div>
</body>

</html>

Step 10: Logout the logged-in user “logout.php”

Here is the code for user logout:

<?php
// Initialize the session.
// If you are using session_name("something"), don't forget it now!
session_start();

// Unset all of the session variables.
$_SESSION = array();

// If it's desired to kill the session, also delete the session cookie.
// Note: This will destroy the session, and not just the session data!
if (ini_get("session.use_cookies")) {
    $params = session_get_cookie_params();
    setcookie(
        session_name(),
        '',
        time() - 42000,
        $params["path"],
        $params["domain"],
        $params["secure"],
        $params["httponly"]
    );
}

// Finally, destroy the session.
session_destroy();
header("Location: login.php");
exit;

Step 11: Stylesheet for this Application “style.css”

This the CSS for this application user-interface:

@import url("https://fonts.googleapis.com/css2?family=Ubuntu:wght@300;400;700&display=swap");
*,
*::before,
*::after {
    box-sizing: border-box;
}

html {
    -webkit-text-size-adjust: 100%;
    -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
    font-size: 16px;
}

body {
    background-color: #f7f7f7;
    font-family: "Ubuntu", sans-serif;
    margin: 0;
    padding: 0;
    color: #222222;
    overflow-x: hidden;
    overflow-wrap: break-word;
    -moz-osx-font-smoothing: grayscale;
    -webkit-font-smoothing: antialiased;
    padding: 50px;
}

.container {
    background-color: white;
    border-radius: 3px;
    box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175);
    margin: 0 auto;
    max-width: 450px;
    padding: 40px;
}

.container h1 {
    margin: 0 0 40px 0;
    text-align: center;
}

input,
button {
    font-family: "Ubuntu", sans-serif;
    font-size: 1rem;
    outline: none;
}

.input {
    padding: 15px;
    width: 100%;
    margin-bottom: 15px;
    border: 1px solid #bbbbbb;
    border-radius: 3px;
}

.input:hover {
    border-color: #999999;
}

.input:focus {
    border-color: #0d6efd;
}

.input.error {
    border-color: red !important;
}
label span {
    color: red;
}

.msg {
    border: 1px solid #66ba7a;
    background: #f3ffd1;
    padding: 10px;
    border-radius: 3px;
}
.msg.error {
    border-color: #e33b54;
    background: #f9d7dc;
}

[type="submit"] {
    background: #0d6efd;
    color: white;
    border: 1px solid rgba(0, 0, 0, 0.175);
    border-radius: 3px;
    padding: 12px 0;
    cursor: pointer;
    box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
    margin-top: 5px;
    font-weight: bold;
    width: 100%;
}

[type="submit"]:hover {
    box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}

label {
    font-weight: bold;
    display: inline-block;
    margin-bottom: 3px;
}

.link {
    margin-top: 20px;
    text-align: center;
}

.link a {
    color: #0d6efd;
}

.profile {
    text-align: center;
}
.profile img {
    display: inline-block;
    border: 3px solid #ccc;
    border-radius: 50%;
    width: 150px;
    height: 150px;
}

h2 span {
    display: block;
    font-size: 15px;
    font-weight: 400;
    color: #888;
}

Step 12: Test the PHP Email Verification Feature

Testing of the PHP email verification for new user accounts

Leave a Reply

Your email address will not be published. Required fields are marked *

We use cookies to ensure that we give you the best experience on our website. Privacy Policy