PHP Login and Registration with email verification Project

PHP Email Verification for New Users

Here you will learn how to verify new users’ email through OTP using PHP.

I have already shared a project of user login and registration system, and here we will implement email verification in this system.

We will be using PHPMailer to send verification emails, so please checkoutHow to use PHPMailer?

PHP Login & Registration with email verification

Live demo of PHP login and registration with email verification via OTP

The following image shows what files we have to create to build this PHP Login & Registration with email verification application –

PHP login with email verification project folder structure

1. 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;
<?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;
}

2. Mail.php & style.css

First, install the PHPMailer via the composer, because we are going to use this package in the Mail.php class to send verification emails.

composer require phpmailer/phpmailer

The send() method of the Mail class is for sending 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;
        }
    }
}
@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;
}

3. New User Registration

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.

In the on_register() function you can see – after inserting a new user an OTP will be sent to the user’s email for verification.

<?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>
<?php
require __DIR__ . "/Mail.php";
function on_register($conn)
{
    $name = htmlspecialchars(trim($_POST['name']));
    $email = trim($_POST['email']);
    $pass = trim($_POST['password']);

    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
        ];
    }

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

    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."
            ]
        ];
    }

    $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!"
    ];
}

4. Email Verification through OTP

After a user is successfully registered, the user will automatically be redirected to verify.php with a unique token.

/verify.php?token=YToyOntzOjU6ImVtYWlsIjtzOjIwOiJoZWxsb3dvcmxkQGdtYWlsLmNvbSI7czo2OiJleHBpcnkiO2k6MTY5MDA5OTI2OTt9

The base64 encoded token will contain an array containing the user’s email and token expiration time.

<?php
if (!isset($_GET["token"]) || !base64_decode($_GET["token"], true)) {
    header("Location: login.php");
    exit;
}
$encoded_token = $_GET["token"];
$token = unserialize(base64_decode($_GET["token"], true));
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;

require_once __DIR__ . "/db_connection.php";

$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();

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

// DB verification code and URL token are equal or not
if ($row === NULL || $row["verification_code"] !== $encoded_token) {
    header("Location: logout.php");
    exit;
}

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

// IF OTP is Submitted, verify the OTP
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) { ?>
            <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) { ?>
            <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 { ?>
            <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>
<?php
function otp_verify($conn, $id, $db_otp, $user_otp)
{
    if ($db_otp == $user_otp) {
        $sql = "UPDATE `users` SET `verified`=1 WHERE `id`=$id";
        $conn->query($sql);
        if ($conn->query($sql)) {
            return true;
        }
        return false;
    }
    return false;
}
<?php
if (!isset($_GET["token"]) || !base64_decode($_GET["token"], true)) {
    header("Location: login.php");
    exit;
}

$token = unserialize(base64_decode($_GET["token"], true));
if (!is_array($token) || !isset($token["email"]) || !filter_var($token["email"], FILTER_VALIDATE_EMAIL)) {
    header("Location: logout.php");
    exit;
}

require_once __DIR__ . "/db_connection.php";
require_once __DIR__ . "/Mail.php";

$sql = "SELECT `id` FROM `users` WHERE `email` = ? AND `verified`=0";

$stmt = $conn->prepare($sql);
$stmt->bind_param("s", $token["email"]);
$stmt->execute();

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

if ($row === NULL) {
    header("Location: logout.php");
    exit;
}
$userID = $row["id"];

$otp = rand(100000, 999999);
$new_token = [
    "email" => $token["email"],
    "expiry" => time()
];
$new_token = base64_encode(serialize($new_token));

$sql = "UPDATE `users` SET `verification_code`=?, `otp`=$otp WHERE `id`=$userID";
$stmt = $conn->prepare($sql);
$stmt->bind_param("s", $new_token);
$mail =  new Mail();
$send_email = $mail->send($token["email"], $otp);
if ($send_email && $stmt->execute()) {
    header("Location: verify.php?token=$new_token");
    exit;
}
header("Location: verify.php?token=$token");
exit;

5. User Login

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
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>
<?php
function on_login($conn)
{
    $email = trim($_POST['email']);
    $pass = trim($_POST['password']);

    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
        ];
    } elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        return [
            "ok" => 0,
            "field_error" => [
                "email" => "Invalid email address."
            ]
        ];
    }

    $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 ($row === NULL) {
        return [
            "ok" => 0,
            "field_error" => [
                "email" => "This email is not registered."
            ]
        ];
    }

    if ($row["verified"] === 0) {
        header("Location: verify.php?token={$row["verification_code"]}");
        exit;
    }

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

    $_SESSION['logged_user_id'] = $row["id"];
    header('Location: home.php');
    exit;
}

6. After login successfully

After login successfully, the user will be redirected to the home.php and the get_user.php will fetch the user information from the database.

<?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>
<?php
function get_user($conn, $id)
{
    if (!filter_var($id, FILTER_VALIDATE_INT)) {
        return false;
    }
    $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);

    if ($row === NULL) return false;
    return $row;
}

7. Logout the logged-in user

<?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;

Thank You … ❤️❤️❤️