How to Add CSRF Token to AJAX Request

by Vincy. Last modified on February 28th, 2024.

Cross-site request forgery or CSRF is a popular security attack, where an authenticated website is forged to do an unwanted action by a malicious user or bot. It is generally associated with form submissions. The application should have an anti-CSRF protection layer as a security measure.

This tutorial provides code to build an antiCSRF PHP service. It performs token generation and validation by managing the token in PHP sessions.

You can find AntiCSRF code without AJAX, in the linked article and it has an example use case with downloadable script.

The following steps are used to secure a form request with antiCSRF protection.

  1. Generate CSRF token and store to $_SESSION.
  2. Load the token into the HTML form.
  3. Post the token to PHP via AJAX on submit.
  4. Compare tokens in $_POST and $_SESSION in PHP.
  5. Return CSRF validation response to AJAX callback.

ajax csrf token

1. Generate CSRF token and store to $_SESSION

This example uses a PHP custom service class AntiCSRFService that handles CSRF token generation and validation.

The getCSRFToken function uses PHP core functions bin2hex(openssl_random_pseudo_bytes(32)) to generate unique the CSRF token.

This function generates and sets the token to the PHP session array with respect to the $sessionTokenKey defined in the class.

<?php
namespace Phppot\SecurityService;
class AntiCSRFService
{
    private $formTokenKey = 'phppot-csrf-token-label';
    private $sessionTokenKey = 'PHPPOT_CSRF_TOKEN_SESS_IDX';

    public function __construct()
    {
        if (empty($_SESSION)) {
            throw new \Error('No session available for persistence');
        }
    }
    ...
    ...

    /**
     * Generate, store, and return the CSRF token
     *
     * @return string[]
     */
    public function getCSRFToken()
    {
        if (empty($_SESSION[$this->sessionTokenKey])) {
            $newToken = bin2hex(openssl_random_pseudo_bytes(32));
            $_SESSION[$this->sessionTokenKey] = $newToken;
        }
        $token = $_SESSION[$this->sessionTokenKey];
        return $token;
    }
    ...
    ...
}
?>

The above code is part of that service class. The generated token is stored to a PHP session with respect to an array index. Also, it is shown on the form footer with a hidden field.

This class contains the PHP session index and the hidden field name as its property.

Having the keys in a single place will be helpful at the time of code maintenance. For example, if you want to change the session index or the hidden field name then this is a single place to apply the change.

2. Load the token into the HTML form

The insertHiddenToken function gets the token from the session via the getCSRFToken() seen in the previous step.

It prints the hidden input field HTML using the PHP echo statement. It embeds the CSRF token in a hidden field to the view from where the insertHiddenToken is called.

The hidden field key is from the AntiCSRFService class property. And, the value is from the PHP session.

index.php

<?php
class AntiCSRFService
{
    /**
     * Insert a CSRF token to a form
     */
    public function insertHiddenToken()
    {
        $csrfToken = $this->getCSRFToken();
        echo "<!--\n--><input type=\"hidden\"" .
            " name=\"" . $this->formTokenKey . "\"" .
            " value=\"" . $csrfToken . "\"" . " />";
    }
}
?>

The form-footer.php calls the insertHiddenToken to embed the CSRD token in a hidden field.

form-footer.php

<?php
require_once __DIR__ . '/lib/AntiCSRFService.php';
$antiCSRFService = new \Phppot\SecurityService\AntiCSRFService();
$antiCSRFService->insertHiddenToken();
?>

This is the landing page that shows a “Project enquiry” form. This form is CSRF protected by inserting the form-footer.php file before closing the HTML form.

By posting the HTML form it sends the CSRF token as part of the posted formData to the PHP. This example uses jQuery AJAX to post the form data to the server.

index.php

<form name="frmEnquiry" id="frmEnquiry" class="phppot-form" method="post" 
    onsubmit="handleFormSubmit(event)">

    <div class="phppot-row">
        <div class="label">
            Project title <span id="userName-info" class="validation-message"></span>
        </div>
        <input type="text" class="phppot-input" name="userName" id="userName" />
    </div>
    <div class="phppot-row">
        <div class="label">
            Contact email <span id="userEmail-info" class="validation-message"></span>
        </div>
        <input type="text" class="phppot-input" name="userEmail" id="userEmail" />
    </div>
    <div class="phppot-row">
        <div class="label">
            Short note <span id="userMessage-info" class="validation-message"></span>
        </div>
        <textarea name="content" id="content" class="phppot-input" cols="60" rows="6"></textarea>
    </div>
    <div class="phppot-row">
        <input type="submit" name="send" class="send-button" value="Send" />
    </div>

    <?php require_once __DIR__ . '/form-footer.php';
    ?>

</form>

3. Post the token to PHP via AJAX on submit

This is a jQuery AJAX function that posts the CSRF token with the form data on submit. The handleFormSubmit() calls the PHP endpoint to perform the CSRF validation before processing the formData from AJAX.

This AJAX script receives the CSRF validation response in its success block. This callback construct the response HTML to acknowledge the user via UI.

assets/js/form.js

function handleFormSubmit(event) {
	event.preventDefault();
	var valid = validateEnquiryForm();

	if (valid) {
		var formData = new FormData($("#frmEnquiry")[0]);

		$.ajax({
			url: 'csrf-validate-ep.php',
			method: 'POST',
			processData: false,
			contentType: false,
			data: formData,
			success: function (response) {
				var jsonResponse = JSON.parse(response);
				var message = jsonResponse.message;
				var type = jsonResponse.type;
				var responseHTML = '<div class="' + type + '">' + message + '</div>';
				$('#phppot-message').html(responseHTML).show();
			},
			error: function (xhr, status, error) {
				console.error(error);
			}
		});
	}
} 

function validateEnquiryForm() {
	var valid = true;
	$(".phppot-input").removeClass("error-field");
	$(".validation-message").html("").hide();

	var userName = $("#userName").val();
	var userEmail = $("#userEmail").val();
	var content = $("#content").val();

	if (userName.trim() == "") {
		$("#userName-info").html("required.").show();
		$("#userName").addClass("error-field");
		valid = false;
	}
	if (userEmail.trim() == "") {
		$("#userEmail-info").html("required.").show();
		$("#userEmail").addClass("error-field");
		valid = false;
	}
	if (!userEmail.match(/^([\w-\.]+@([\w-]+\.)+[\w-]{2,4})?$/)) {
		$("#userEmail-info").html("invalid.").show();
		$("#userEmail").addClass("error-field");
		valid = false;
	}
	if (content == "") {
		$("#userMessage-info").html("required.").show();
		$("#content").addClass("error-field");
		valid = false;
	}

	if (valid == false) {
		$('.error-field').first().focus();
		valid = false;
	}
	return valid;
}

jquery ajax logo

4. Compare tokens in $_POST and $_SESSION in PHP

The isValidRequest() and validateRequest() are the part of AntiCSRF service class. It processes the post array and compare the CSRF token in the $_POST and the $_SESSION array.

It uses PHP hash_equals() to compare the CSRF hash tokens. If match found then the validation function will return true.

<?php
class AntiCSRFService
{
    /**
     * the actual validation of CSRF happens here and returns boolean
     *
     * @return boolean
     */
    public function isValidRequest()
    {
        $isValid = false;
        if (!empty($_POST)) {
            $isValid = $this->validateRequest();
        }
        return $isValid;
    }

    /**
     * Validate a request based on session
     *
     * @return bool
     */
    public function validateRequest()
    {
        if (!isset($_SESSION[$this->sessionTokenKey])) {
            // CSRF Token not found in the session
            return false;
        }

        if (!empty($_POST[$this->formTokenKey])) {
            // Let's pull the POST data
            $token = $_POST[$this->formTokenKey];
        } else {
            return false;
        }

        if (!\is_string($token)) {
            return false;
        }
        $expected = $_SESSION[$this->sessionTokenKey];
        return \hash_equals($expected, $token);
    }
}
?>

5. Return CSRF validation response to AJAX callback

The below PHP endpoint gets the $csrfResponse from the service class. If it is a boolean true then it processes further to email the project enquiry to the site admin.

This example uses PHP mail() function with a MailService() handler. You can also replace this with PHPMailer script to have more features in sending emails. The linked article has the code to learn how to use PHPMailer to send email via SMTP.

csrf-validate-ep.php

<?php

use Phppot\MailService;

session_start();
if ($_SERVER["REQUEST_METHOD"] == "POST" && !empty($_POST)) {
	require_once __DIR__ . '/lib/AntiCSRFService.php';
	$antiCSRFService = new \Phppot\SecurityService\AntiCSRFService();
	$csrfResponse = $antiCSRFService->isValidRequest();
	if (!empty($csrfResponse)) {
		require_once __DIR__ . '/lib/MailService.php';
		$mailService = new MailService();
		$response = $mailService->sendContactMail($_POST);
		if (!empty($response)) {
			$message = "Hi, we have received your message. Thank you.";
			$type = "success";
		} else {
			$message = "Unable to send email.";
			$type = "error";
		}
	} else {
		$message = "Security Alert: Unable to process your request.";
		$type = "error";
	}
	$responseArray = array(
		'message' => $message,
		'type' => $type
	);

	$jsonResponse = json_encode($responseArray);

	echo $jsonResponse;
	exit;
}
?>

anti csrf alert via ajax

Mail sending script

lib/MailService.php

<?php
namespace Phppot;

class MailService
{

    function sendContactMail($postValues)
    {
        $name = $postValues["userName"];
        $email = $postValues["userEmail"];
        $subject = $postValues["subject"];
        $content = $postValues["content"];

        $toEmail = "ADMIN EMAIL";
        $mailHeaders = "From: " . $name . "(" . $email . ")\r\n";
        $response = mail($toEmail, $subject, $content, $mailHeaders);

        return $response;
    }
}
?>

Download

Vincy
Written by Vincy, a web developer with 15+ years of experience and a Masters degree in Computer Science. She specializes in building modern, lightweight websites using PHP, JavaScript, React, and related technologies. Phppot helps you in mastering web development through over a decade of publishing quality tutorials.

Leave a Reply

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

↑ Back to Top

Share this page