PHP2Shell: Is Your Joomla Site Safe? What to Do Today Introducing Next Boards: Kanban project and task boards for Joomla Next Exams 6.2.0: Exam Studio, six visual question types, exam embeds and six new languages Building the Future of Joomla Extensions
NextSoftware

Verifying signatures

Why verify

An endpoint URL is not a secret. It can be guessed, logged by a proxy, or leak from a configuration file. Without verification, anyone who learns the URL can post invented results to your integration.

Every delivery is signed with the endpoint's own secret. Check the signature on every delivery and reject anything that does not match.


How the signature is made

Part Value
Header X-NextExams-Signature
Format sha256= followed by the lowercase hex digest
Algorithm HMAC-SHA256
Signed bytes The raw request body, exactly as received
Key The endpoint's signing secret, as shown in the webhook editor

Warning: compute the HMAC over the raw bytes you received, not over a re-encoded copy of the parsed JSON. Re-encoding changes key order and whitespace, and the signature will not match. This is the most common integration mistake.

Compare with a constant-time function (hash_equals in PHP, hmac.compare_digest in Python, crypto.timingSafeEqual in Node).


Examples

PHP

$body     = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $body, $secret);
$given    = $_SERVER['HTTP_X_NEXTEXAMS_SIGNATURE'] ?? '';

if (!hash_equals($expected, $given)) {
    http_response_code(401);
    exit;
}

$event = json_decode($body, true);

Node.js

import crypto from 'node:crypto';

// `raw` must be the unparsed body, e.g. express.raw({ type: 'application/json' }).
function verify(raw, header, secret) {
  const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(raw).digest('hex');
  const a = Buffer.from(expected);
  const b = Buffer.from(header || '');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Python

import hashlib, hmac

def verify(raw: bytes, header: str, secret: str) -> bool:
    expected = 'sha256=' + hmac.new(secret.encode(), raw, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header or '')

Your signing secret

  • Each endpoint has its own secret, generated when you create it.
  • Regenerate it at any time from the webhook editor. The new secret signs the next delivery, so update your receiver first.
  • Treat it like a password: keep it out of version control and logs.

Replays

The signature covers the body, and the body carries timestamp. A receiver that must refuse replayed deliveries can reject any whose timestamp is older than a few minutes, and keep the X-NextExams-Delivery ids it has already processed.

Leave enough room for retries: a retried delivery is sent again with the same id and body, up to about 100 minutes after the first attempt. Dedupe on the id rather than rejecting old timestamps outright if you want retries to get through.


See also

All Extensions