Password-less authentication in PHP and MySQL

Previously we did password less authentication in Node JS and Mongo DB, in this article we will do it in PHP and MySQL.

Suppose you are trying to login to someone’s computer. You might open an incognito window in order to prevent the password from being saved in the browser. But what if he has installed a keylogger in his computer ? He can track your keystrokes and will know your email and password.

You can try typing your password from virtual keyboard, but what if he has also installed a screen recroding software that quitely records the screen ?

So the website developers must provide a way to secure their users from such vulnerabilities.

The best way is to allow users to login without entering their password. In-fact, there will be no password. User just have to enter his email address. We will send him a verification code. He needs to enter that code to verify.

That code will be auto-generated and will not be used again once user is logged-in. So even if some keylogging or screen recording software knows the verification code, it will be of no use. Because the code will not work next time someone tries with that user’s email address.

Create a Table

First I am going to create users table. If you already have one, you just need to add another column code in it.

<?php

$db_name = "test";
$username = "root";
$password = "";

$conn = new PDO("mysql:host=localhost;dbname=" . $db_name, $username, $password);

$sql = "CREATE TABLE IF NOT EXISTS users(
    id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
    email TEXT NULL,
    code TEXT NULL
)";

$result = $conn->prepare($sql);
$result->execute();

I am using PDO to prevent SQL injection. If you want to know more about PHP PDO, you can check our guide here. If you refresh the page now and check your phpMyAdmin, you will have users table created in your database.

users-table-password-less-authentication-php
users-table-password-less-authentication-php

Login Form

Next we need to create a form to ask for user’s email address. It will also have a submit button.

<form method="POST" action="index.php">
    <p>
        <input type="email" name="email" placeholder="Enter email" />
    </p>

    <input type="submit" value="Login" />
</form>

This will create an input field and a submit button.

Send email in PHP

Then we need to generate a random code and send an email with that verification code. For sending email, we will be using a library PHPMailer. You can download it and extract the folder in your project’s root directory.

use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;

require 'PHPMailer/src/Exception.php';
require 'PHPMailer/src/PHPMailer.php';
require 'PHPMailer/src/SMTP.php';

function send_mail($to, $subject, $body)
{
    //Create an instance; passing `true` enables exceptions
    $mail = new PHPMailer(true);

    try {
        //Server settings
        $mail->SMTPDebug = 0;                      //Enable verbose debug output
        $mail->isSMTP();                                            //Send using SMTP
        $mail->Host       = 'mail.adnan-tech.com';                     //Set the SMTP server to send through
        $mail->SMTPAuth   = true;                                   //Enable SMTP authentication
        $mail->Username   = 'support@adnan-tech.com';                     //SMTP username
        $mail->Password   = '';                               //SMTP password
        $mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;            //Enable implicit TLS encryption
        $mail->Port       = 465;                                    //TCP port to connect to; use 587 if you have set `SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS`

        //Recipients
        $mail->setFrom('support@adnan-tech.com', 'Adnan Afzal');
        $mail->addAddress($to);               //Name is optional

        //Content
        $mail->isHTML(true);                                  //Set email format to HTML
        $mail->Subject = $subject;
        $mail->Body    = $body;
        $mail->AltBody = $body;

        $mail->send();
        // echo 'Message has been sent';
    } catch (Exception $e) {
        // echo "Message could not be sent. Mailer Error: {$mail->ErrorInfo}";
    }
}

if ($_SERVER["REQUEST_METHOD"] == "POST")
{
    $email = $_POST["email"];

    $str = "qwertyuiopasdfghjklzxcvbnm1234567890";
    $code = "";

    for ($a = 1; $a <= 6; $a++)
    {
        $code .= $str[rand(0, strlen($str) - 1)];
    }

    $subject = "Login";
    $body = "Your verification code is " . $code;

    $sql = "SELECT * FROM users WHERE email = :email";
    $result = $conn->prepare($sql);
    $result->execute([
        ":email" => $email
    ]);
    $user = $result->fetch();

    if ($user == null)
    {
        $sql = "INSERT INTO users(email, code) VALUES (:email, :code)";
        $result = $conn->prepare($sql);
        $result->execute([
            ":email" => $email,
            ":code" => $code
        ]);

        send_mail($email, $subject, $body);

        header("Location: verify.php?email=" . $email);
        exit();
    }

    $sql = "UPDATE users SET code = :code WHERE email = :email";
    $result = $conn->prepare($sql);
    $result->execute([
        ":email" => $email,
        ":code" => $code
    ]);

    send_mail($email, $subject, $body);

    header("Location: verify.php?email=" . $email);
    exit();
}

Here, first I am generating a random 6 character code. Then I am setting the body of the email in a $body variable.

Then I am checking if the user already exists in the database. If not, then insert a new row in database. Sends an email and redirect the user to the verification page.

If user already exists, then I am simply updating his code column. Sends an email and redirect to a new page to verify the code.

Note: You need to enter your correct SMTP credentials (email and password) in order to send the email.

Verify the Code

Create a new file verify.php and inside it, first get the email from the URL. Then create a form with an hidden input field for email. One input field for user to enter the code sent at email, and a submit button.

<?php

    $db_name = "test";
    $username = "root";
    $password = "";

    $conn = new PDO("mysql:host=localhost;dbname=" . $db_name, $username, $password);
    $email = $_GET["email"] ?? "";

?>

<form method="POST" action="verify.php">
    <input type="hidden" name="email" value="<?php echo $email; ?>" />

    <p>
        <input type="text" name="code" placeholder="Enter your code here..." required />
    </p>

    <input type="submit" value="Login" />
</form>

We are creating a hidden input field so that it will be sent along with form. Now when the form submits, we need to check if user has provided correct code.

if ($_SERVER["REQUEST_METHOD"] == "POST")
{
    $email = $_POST["email"] ?? "";
    $code = $_POST["code"] ?? "";

    $sql = "SELECT * FROM users WHERE email = :email AND code = :code";
    $result = $conn->prepare($sql);
    $result->execute([
        ":email" => $email,
        ":code" => $code
    ]);
    $user = $result->fetch();

    if ($user == null)
    {
        die("Invalid code.");
    }

    $sql = "UPDATE users SET code = NULL WHERE id = :id";
    $result = $conn->prepare($sql);
    $result->execute([
        ":id" => $user["id"]
    ]);

    die("Logged-in");
}

If the user provides an in-valid code, then we are simply displaying him an error message. If he has provided the correct code, then we are setting his code column to NULL so it won’t be used again.

You can also try sending an AJAX and pass the code value as null, it still won’t be logged-in.

// for testing only

const ajax = new XMLHttpRequest()
ajax.open("POST", "verify.php", true)

const formData = new FormData()
formData.append("email", "support@adnan-tech.com")
formData.append("code", null)
ajax.send(formData)

So that’s it. That’s how you can add password-less authentication system in your application using PHP and MySQL. If you face any problem in following this, kindly do let me know.

Do not use .env to store sensitive credentials in Laravel

If you are working in Laravel, you might have noticed that majority of the developers save sensitive information like database credentials, SMTP settings and API keys in the .env file. This file exists at the root level of your project.

I was also saving my credentials in .env files for a long time until I found some problems in it. So I had to find a better way to save sensitive credentials securely.

Problem with .env file

The problems I found while saving sensitive credentials in .env file in Laravel are:

  1. Caches

The variables you set in .env file will be cached by Laravel. You might change the value of variable but the Laravel will be picking the old cached value.

However, you can refresh cache by running the following commands but it still an extra work:

php artisan config:clear
php artisan cache:clear
php artisan config:cache
  1. Not dynamic

Imagine you are using PayPal or Stripe in your Laravel application and you have saved their API keys in .env file. You have delivered the project but now the client has to change his account again and again. Instead of him messaging you everytime he needs to change his API keys, you can provide him a simple admin panel from where he can set those values dynamically.

  1. Might be exposed

Wrong deployment or 1 bad configuration in .htaccess might lead to exposing your .env file to the public. Hence exposing all your sensitive credentials to everyone.

Database Credentials

Probably the first credential that developers set are the database credentials. Because without them, migrations and seeders won’t run.

I have commented out the database credentials in .env file:

# DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=

I am setting database credentials directly in config/database.php file.

Change:

'default' => env('DB_CONNECTION', 'sqlite'),

To:

'default' => "mysql",

And change:

'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),

To:

'host' => '127.0.0.1',
'port' => '3306',
'database' => 'database_name',
'username' => 'root',
'password' => '',

Session Lifetime

You can comment out the session lifetime from .env file:

# SESSION_LIFETIME=120

And set this value in config/session.php file:

'lifetime' => 52560000, // 100 years
'expire_on_close' => false,

SMTP Credentials & API Keys

To make SMTP credentials and API keys dynamic, I have created a form at the admin panel:

<form onsubmit="saveSettings()">
    <p>
        <input type="text" class="form-control" name="host" />
    </p>

    <p>
        <input type="text" class="form-control" name="port" />
    </p>

    <p>
        <label>
            SSL
            <input type="radio" name="encryption" value="ssl" />
        </label>

        <label>
            TLS
            <input type="radio" name="encryption" value="tls" />
        </label>
    </p>

    <p>
        <input type="email" name="username" />
    </p>

    <p>
        <input type="password" name="password" />
    </p>

    <button type="submit">Save Settings</button>
</form>

Then in Javascript, I created a function that will be called when this form submits. This function will call an AJAX to save these credentials in the database:

async function saveSettings() {
    event.preventDefault()
    
    const form = event.target
    const formData = new FormData(form)
    form.submit.setAttribute("disabled", "disabled")

    try {
        const response = await axios.post(
            "http://localhost:8000/api/admin/save-settings",
            formData,
            {
                headers: {
                    Authorization: "Bearer " + localStorage.getItem(accessTokenKey)
                }
            }
        )

        if (response.data.status == "success") {
            swal.fire("Save Settings", response.data.message, "success")
        } else {
            swal.fire("Error", response.data.message, "error")
        }
    } catch (exp) {
        swal.fire("Error", exp.message, "error")
    } finally {
        form.submit.removeAttribute("disabled")
    }
}

Then I create a route in routes/api.php file that will handle this AJAX request:

Route::post("/admin/save-settings", [AdminController::class, "save_settings"]);

After that, we need to create a method in AdminController.php that will be called when this route is accessed.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

use DB;

class AdminController extends Controller
{
    public function save_settings()
    {
        $host = request()->host ?? "";
        $port = request()->port ?? "";
        $encryption = request()->encryption ?? "";
        $username = request()->username ?? "";
        $password = request()->password ?? "";

        $this->set_setting("smtp_host", $host);
        $this->set_setting("smtp_port", $port);
        $this->set_setting("smtp_encryption", $encryption);
        $this->set_setting("smtp_username", $username);
        $this->set_setting("smtp_password", $password);

        return response()->json([
            "status" => "success",
            "message" => "Settings has been saved."
        ]);
    }

    private function set_setting($key, $value)
    {
        $setting = DB::table("settings")
            ->where("key", "=", $key)
            ->first();

        if ($setting == null)
        {
            DB::table("settings")
                ->insertGetId([
                    "key" => $key,
                    "value" => $value,
                    "created_at" => now()->utc(),
                    "updated_at" => now()->utc()
                ]);
        }
        else
        {
            DB::table("settings")
                ->where("id", "=", $setting->id)
                ->update([
                    "value" => $value,
                    "updated_at" => now()->utc()
                ]);
        }
    }
}

This will save credentials in settings table if does not exists. If exists, then it will update its value.

Following is the schema of the settings table:

Schema::create('settings', function (Blueprint $table) {
    $table->id();
    $table->string("key")->nullable();
    $table->longText("value")->nullable();
    $table->timestamps();
});
settings-table
settings-table

Now that you have saved the values in database, I will show you how to fetch the credentials from database.

$settings = DB::table("settings")->get();

$settings_obj = new \stdClass();
foreach ($settings as $setting)
{
    $settings_obj->{$setting->key} = $setting->value;
}

echo $settings_obj->smtp_host;

This will fetch all the credentials from database and convert them into object using key value pairs. So you can access your values just like a normal object.

Hard-coded strings

There might be some scenarios where you do want to save some hard-coded strings. In that case, I have created a new file named “config.php” inside config folder and wrote all my hard-coded variables there:

<?php

// config/config.php

return [
    "app_name" => "adnan-tech.com"
];

And you can access them in the following way:

<p>
    {{ config("config.app_name") }}
</p>

config is a built-in function in Laravel. In “config.app_name”, config is the name of the file I created and app_name is the variable created inside it.

Get file extension from name – Javascript, PHP

Getting file extension from file name or path can be done in Javascript and PHP. I will show you the steps to get the file extension and you can apply them in any language you want.

Following code will get the file extension from file name or file path in Javascript:

const path = "https://adnan-tech.com/wp-content/uploads/2024/07/Manage-google-account.png"

const parts = path.split(".")

const extension = parts[parts.length - 1].toLowerCase()

console.log(extension)

Your parts variable will look like this:

Array split explode - Javascript, PHP
Array split explode – Javascript, PHP

Following code will get the file extension from file name or path in PHP:

$path = "https://adnan-tech.com/wp-content/uploads/2024/07/Manage-google-account.png";

$parts = explode(".", $path);

$extension = $parts[count($parts) - 1];

$extension = strtolower($extension);

echo $extension;

We are converting the extension to lower-case because it will be helpful when you want to perform some action based on file extension. For example:

if (extension == "jpg") {
    // do something if file is JPEG
}

Some files has their extension in upper-case and some in lower-case. So it would be good if you convert that to lower-case to check properly.

That’s how you can get the file extension from it’s name or path using Javascript and PHP.

Highlight current tab after page refresh – HTML, Javascript, PHP

Suppose you are creating a tab layout where there are multiple tabs and each tab has its own content. You click on the tab button and its content is displayed.

But when you refresh the page, it starts from the first tab again. How can you make so that when you refresh the page, it keeps the last active tab as “active” ?

Bootstrap Tabs

Following code will display a tab layout with 3 tabs in Bootstrap 5:

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css" />

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.min.js"></script>

<ul class="nav nav-tabs" id="myTab" role="tablist">
    <li class="nav-item" role="presentation">
        <button class="nav-link active" id="home-tab" data-bs-toggle="tab" data-bs-target="#home" type="button" role="tab" aria-controls="home" aria-selected="true">Home</button>
    </li>

    <li class="nav-item" role="presentation">
        <button class="nav-link" id="profile-tab" data-bs-toggle="tab" data-bs-target="#profile" type="button" role="tab" aria-controls="profile" aria-selected="false">Profile</button>
    </li>

    <li class="nav-item" role="presentation">
        <button class="nav-link" id="contact-tab" data-bs-toggle="tab" data-bs-target="#contact" type="button" role="tab" aria-controls="contact" aria-selected="false">Contact</button>
    </li>
</ul>

<div class="tab-content" id="myTabContent">
    <div class="tab-pane fade show active" id="home" role="tabpanel" aria-labelledby="home-tab">Home</div>
    <div class="tab-pane fade" id="profile" role="tabpanel" aria-labelledby="profile-tab">Profile</div>
    <div class="tab-pane fade" id="contact" role="tabpanel" aria-labelledby="contact-tab">Contact</div>
</div>

Right now it makes the “Home” tab active. When you click on other tabs, you will see their content. But once you refresh the page, it will again make the home tab active regardless of which tab you clicked last time.

Updating URL

Next step is to set the value of current tab in the URL when user changes the tab. So on each tab button, we will add an onclick listener that will call a function to update the URL in the browser.

<ul class="nav nav-tabs" id="myTab" role="tablist">
    <li class="nav-item" role="presentation">
        <button class="nav-link active" onclick="changeTab('home')" id="home-tab" data-bs-toggle="tab" data-bs-target="#home" type="button" role="tab" aria-controls="home" aria-selected="true">Home</button>
    </li>

    <li class="nav-item" role="presentation">
        <button class="nav-link" onclick="changeTab('profile')" id="profile-tab" data-bs-toggle="tab" data-bs-target="#profile" type="button" role="tab" aria-controls="profile" aria-selected="false">Profile</button>
    </li>

    <li class="nav-item" role="presentation">
        <button class="nav-link" onclick="changeTab('contact')" id="contact-tab" data-bs-toggle="tab" data-bs-target="#contact" type="button" role="tab" aria-controls="contact" aria-selected="false">Contact</button>
    </li>
</ul>

Next we will create that Javascript function that will handle the onclick listener:

function changeTab(tab) {
    const urlSearchParam = new URLSearchParams(window.location.search)
    urlSearchParam.set("tab", tab)
    const newPath = window.location.pathname + "?" + urlSearchParam.toString()
    history.pushState(null, "", newPath)
}

If you want to learn more about this in detail, you can read this article.

If you change the tabs now, you will see that the “tab” query will be appended in the URL and its value will be the tab clicked. But if you refresh the page now, the tab will not be highlighted yet. This is because we are setting the “tab” query in URL but not reading it.

Highlight current tab as active

Last step is to get the current tab from the URL and highlight it.

<?php
    $tab = $_GET["tab"] ?? "home";
?>

This is get the “tab” query from URL parameter. If it does not exists in the URL, then the default value of $tab variable will be “home”.

Following is the updated code that will highlight the tab button and also will show its content.

<ul class="nav nav-tabs" id="myTab" role="tablist">
    <li class="nav-item" role="presentation">
        <button class="nav-link <?php echo $tab == 'home' ? 'active' : ''; ?>" onclick="changeTab('home')" id="home-tab" data-bs-toggle="tab" data-bs-target="#home" type="button" role="tab" aria-controls="home" aria-selected="true">Home</button>
    </li>

    <li class="nav-item" role="presentation">
        <button class="nav-link <?php echo $tab == 'profile' ? 'active' : ''; ?>" onclick="changeTab('profile')" id="profile-tab" data-bs-toggle="tab" data-bs-target="#profile" type="button" role="tab" aria-controls="profile" aria-selected="false">Profile</button>
    </li>

    <li class="nav-item" role="presentation">
        <button class="nav-link <?php echo $tab == 'contact' ? 'active' : ''; ?>" onclick="changeTab('contact')" id="contact-tab" data-bs-toggle="tab" data-bs-target="#contact" type="button" role="tab" aria-controls="contact" aria-selected="false">Contact</button>
    </li>
</ul>

<div class="tab-content" id="myTabContent">
    <div class="tab-pane fade <?php echo $tab == 'home' ? 'show active' : ''; ?>" id="home" role="tabpanel" aria-labelledby="home-tab">Home</div>
    <div class="tab-pane fade <?php echo $tab == 'profile' ? 'show active' : ''; ?>" id="profile" role="tabpanel" aria-labelledby="profile-tab">Profile</div>
    <div class="tab-pane fade <?php echo $tab == 'contact' ? 'show active' : ''; ?>" id="contact" role="tabpanel" aria-labelledby="contact-tab">Contact</div>
</div>

Complete code: Highlight current tab

Following is the complete code that displays 3 tabs along with their contents. On change, will set the query parameter in the URL. After refresh will highlight the tab last selected.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <title>Highlight Current Tab after Page Refresh</title>

        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css" />

        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>

        <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.min.js"></script>
    </head>

    <body>

        <div class="container mt-5">
            <div class="row">
                <div class="col-12">



                    <?php
                        $tab = $_GET["tab"] ?? "home";
                    ?>


                    <script>
                        function changeTab(tab) {
                            const urlSearchParam = new URLSearchParams(window.location.search)
                            urlSearchParam.set("tab", tab)
                            const newPath = window.location.pathname + "?" + urlSearchParam.toString()
                            history.pushState(null, "", newPath)
                        }
                    </script>

                    <ul class="nav nav-tabs" id="myTab" role="tablist">
                        <li class="nav-item" role="presentation">
                            <button class="nav-link <?php echo $tab == 'home' ? 'active' : ''; ?>" onclick="changeTab('home')" id="home-tab" data-bs-toggle="tab" data-bs-target="#home" type="button" role="tab" aria-controls="home" aria-selected="true">Home</button>
                        </li>

                        <li class="nav-item" role="presentation">
                            <button class="nav-link <?php echo $tab == 'profile' ? 'active' : ''; ?>" onclick="changeTab('profile')" id="profile-tab" data-bs-toggle="tab" data-bs-target="#profile" type="button" role="tab" aria-controls="profile" aria-selected="false">Profile</button>
                        </li>

                        <li class="nav-item" role="presentation">
                            <button class="nav-link <?php echo $tab == 'contact' ? 'active' : ''; ?>" onclick="changeTab('contact')" id="contact-tab" data-bs-toggle="tab" data-bs-target="#contact" type="button" role="tab" aria-controls="contact" aria-selected="false">Contact</button>
                        </li>
                    </ul>

                    <div class="tab-content" id="myTabContent">
                        <div class="tab-pane fade <?php echo $tab == 'home' ? 'show active' : ''; ?>" id="home" role="tabpanel" aria-labelledby="home-tab">Home</div>
                        <div class="tab-pane fade <?php echo $tab == 'profile' ? 'show active' : ''; ?>" id="profile" role="tabpanel" aria-labelledby="profile-tab">Profile</div>
                        <div class="tab-pane fade <?php echo $tab == 'contact' ? 'show active' : ''; ?>" id="contact" role="tabpanel" aria-labelledby="contact-tab">Contact</div>
                    </div>



            


                </div>
            </div>
        </div>
    </body>
</html>

If you face any problem in following this, kindly do let me know.