Authenticating and protecting Ajax requests with PHP

Methods to authenticate and protect jQuery Ajax calls with PHP. This prevents external access and usage to your web app.

ajax authenticate protect calls PHP

Method 1: Tokens

Method 1 is using tokens set via sessions that are sent within the header of the HTTP request from Ajax. These are then checked at the call file.

The token is created with bin2hex() and random_bytes() then set with a session:

$token = bin2hex(random_bytes(64));
$_SESSION["token"] = $token;

Then it is set as within HTML meta:

echo '<meta name="token" content="' . $_SESSION["token"] . '">';

Jquery will then grab the token from the meta and the Ajax call will send it in the header:

const token = $('meta[name="token"]').attr('content');
$.ajax({
    type: "POST",
    url: "api.php",
    headers: {"token": token},
    data: {"ajax_call": true},
    success: function (result) {
        $("#resultText").text(result);
    }
});

Now onto api.php which is the endpoint for the calls, first a check is made for if the HTTP request is a POST or GET and then if a token exists in the header. If a token does exist then it is checked if it doesn’t match the one set from the session.

If either of these conditions fail, output the error message and exit. The error message is only shown as an example it would be best to return HTTP code 403 rather than specifically why the request failed.

if (isset($_POST['ajax_call']) || isset($_GET['ajax_call'])) {
    $headers = getallheaders();
    if (isset($headers['token'])) {
        $header_token = $headers['token'];
        if ($header_token != $_SESSION['token']) {
            echo "ERROR: Tokens dont match";
            exit;
        }
    } else {
        echo "ERROR: No token sent";
        exit;
    }
} else {
    echo "ERROR: Not a GET or POST request";
    exit;
}

Method 2: Origin / referer

Method 2 is checking the HTTP origin or the referee comes from the hostname and not externally.

This method is a fallback as HTTP referer can be spoofed if one does some snooping and tinkering with the requests, or sometimes the browser can strip out the referee to be empty.

Other methods

Timed tokens

Similar to the session tokens but instead they have a very short expiration time and need to be automatically refreshed by the authenticated user to be valid.

Ip Address matching

Use the logged in/authenticated users IP to ensure the request is coming from the right source. The IP is logged via a database rather than being sent as a header.

 

The full scripts below or Gist.

index.php

<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://write.corbpie.com/wp-content/litespeed/localres/aHR0cHM6Ly9jZG5qcy5jbG91ZGZsYXJlLmNvbS8=ajax/libs/twitter-bootstrap/4.6.0/css/bootstrap.min.css"/>
<?php
if (session_status() == PHP_SESSION_NONE) {
    session_start();
}

if (isset($_GET['get_token']) && empty($_SESSION["token"])) {
    $token = bin2hex(random_bytes(64));
    $_SESSION["token"] = $token;
}

if (isset($_GET['kill_token'])) {
    unset($_SESSION["token"]);
    session_destroy();
}
?>
<div class="container">
    <div class="row">
        <div class="col-12">
            <?php
            if (isset($_SESSION["token"])) {
                echo '<meta name="token" content="' . $_SESSION["token"] . '">';
                ?>
                <p><?php echo "Token is set:</p> <code>{$_SESSION["token"]}</code><br>"; ?>
                <a class="btn btn-danger btn-sm" href="?kill_token" role="button">Kill token</a>
                <?php
            } else {
                ?>
                <p>Token not set </p><code> </code><br>
                <a class="btn btn-success btn-sm" href="?get_token" role="button">Get token</a>
                <?php
            }
            ?>
            <a id="POSTcallBtn" class="btn btn-secondary btn-sm" href="#" role="button">Do ajax POST call</a>
            <a id="GETcallBtn" class="btn btn-secondary btn-sm" href="#" role="button">Do ajax GET call</a>
            <div id="resultsDiv" class="mt-3"><b><p id="resultText"></p></b></div>
        </div>
    </div>
</div>

<script type="text/javascript" src="https://write.corbpie.com/wp-content/litespeed/localres/aHR0cHM6Ly9jZG5qcy5jbG91ZGZsYXJlLmNvbS8=ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script type="application/javascript">
    function demoCallTemplate(request_type) {
        const token = $('meta[name="token"]').attr('content');
        $("#resultText").empty();
        $.ajax({
            type: request_type,
            url: "api.php",
            headers: {"token": token},
            data: {"ajax_call": true},
            success: function (result) {
                $("#resultText").text(result);
            }
        });
    }

    $(document).on("click", "#POSTcallBtn", function () {
        demoCallTemplate('POST');
    });

    $(document).on("click", "#GETcallBtn", function () {
        demoCallTemplate('GET');
    });
</script>

api.php

<?php
if (session_status() == PHP_SESSION_NONE) {
    session_start();//Start session if none exists/already started
}

if (isset($_POST['ajax_call']) || isset($_GET['ajax_call'])) {
    $headers = getallheaders();
    if (isset($headers['token'])) {
        $header_token = $headers['token'];
        if ($header_token != $_SESSION['token']) {
            echo "ERROR: Tokens dont match";
            exit;
        }
    } else {
        echo "ERROR: No token sent";
        exit;
    }
    if (isset($_POST['ajax_call'])) {//POST call
        if (isset($_SERVER['HTTP_ORIGIN'])) {
            $address = 'https://' . $_SERVER['SERVER_NAME'];
            if (strpos($address, $_SERVER['HTTP_ORIGIN']) == 0) {
                echo "POST SUCCESS! [token = session token], [HTTP origin = server host]";
                //Do the POST request
                //........
            }
        } else {
            echo "POST ERROR: No Origin header";
        }
    } elseif (isset($_GET['ajax_call'])) {//GET call
        if (isset($_SERVER['HTTP_REFERER'])) {
            if (parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST) != $_SERVER['HTTP_HOST']) {
                echo "ERROR: External GET request";
                exit;
            }
            echo "GET SUCCESS! [token = session token], [referer = host (" . parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST) . " = {$_SERVER['HTTP_HOST']})]";
            //Return data from GET request
            //........
        } else {
            echo "GET ERROR: No HTTP referer";
            exit;
        }
    }
} else {
    echo "ERROR: Not a GET or POST request";
    exit;
}