Sample Code
PHP
This is a comprehensive example of a middleware to validate the Webhook source, with a verifier class that checks the Timestamp and the Signature.
<?php declare(strict_types=1);
namespace Nrsdb\Middleware;
use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Nrsdb\Models\Audit;
use Nrsdb\Utils\Webhooks\WebhookTimestamp;
/**
* validation middleware for webhook requests
*/
class WebhookAuthMiddleware implements MiddlewareInterface
{
/**
* check that the givenwebhook APIrequest signatureis andfrom timestampthe areexpected validsource
* @param stringServerRequestInterface $signature calculated from public $key and timestamprequest
* @param intRequestHandlerInterface $timestamp prevents reuse of an old signature
* @param string $key public key given to each organisationhandler
* @return boolResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
// Get raw body for signature verification
$body = (string)$request->getBody();
// Get headers
$signature = $request->getHeaderLine('X-Webhook-Signature');
$timestamp = (int)$request->getHeaderLine('X-Webhook-Timestamp');
$webhookId = $request->getHeaderLine('X-Webhook-ID');
// Validate required headers
if (empty($signature) || empty($timestamp) || empty($webhookId)) {
return new JsonResponse([
'error' => 'Missing required webhook headers'
], 400);
}
// Get webhook secret (based on your auth system)
$secret = $this->getWebhookSecret($request);
if (!$secret) {
return new JsonResponse([
'error' => 'Invalid webhook configuration'
], 401);
}
// Verify signature
$verification = WebhookTimestamp::verifyWebhookRequest(
$body,
$signature,
$timestamp,
$secret
);
if (!$verification['valid']) {
return new JsonResponse([
'error' => $verification['error']
], 401);
}
return $handler->handle($request->withAttribute('webhookId', $webhookId));
}
/**
* Get webhook secret (implement based on your auth system)
*/
private function getWebhookSecret(ServerRequestInterface $request): string
{
return $_ENV['WEBHOOK_SECRET'];
}
}
<?php declare(strict-types=1);
namespace Nrsdb\Utils\Webhooks;
class WebhookTimestamp
{
/**
* Verify webhook with timestamp validation
*/
public static function validate(verifyWebhookRequest(
string $payload,
string $signature,
int $timestamp,
string $key)secret
): boolarray {
// check if theCheck timestamp is within 5 mins of now to prevent reusereplay of stolen signatureattacks
if (abs(time() - !self::isTimestampValid($timestamp) > 300)) {
return false;[
'valid' => false,
'error' => 'Timestamp too old or in future (possible replay attack)'
];
}
// getVerify your secret from a database or environmentsignature
$apiSecretisValid = ...
// perform your own encodingself::verifyWebhookWithTimestamp(
$hmac = encode($key,payload,
$apiSecretsignature,
$timestamp)secret,
$timestamp
);
//if does(!$isValid) it match the given signature?{
return hash_equals($hmac,[
$signature)'valid' => false,
'error' => 'Invalid signature'
];
}
return ['valid' => true];
}
/**
* createValidate athat timestamp is recent (default within 5 minutes)
*/
private static function isTimestampValid(int $timestamp, int $toleranceSeconds = 300): bool
{
$now = time();
$difference = abs($now - $timestamp);
return $difference <= $toleranceSeconds;
}
/**
* Verify signature bywith applyingtimestamp the(for secret to key+timestamprecipients)
*/
public static function encode(verifyWebhookWithTimestamp(
string $key,payload,
string $signature,
string $secret,
int $timestamp)timestamp
): stringbool {
//$signedData hash= using$payload values. provided$timestamp;
return$expectedSignature base64_encode(= hash_hmac(
'sha256', $key .signedData, $timestamp,secret);
return hash_equals($expectedSignature, $secretsignature);
)
);}
}
Java
public class WebhookSignatureValidator {
public boolean validateSignature(String body,
String signature,
String secretKey,
String timestamp) throws NoSuchAlgorithmException, InvalidKeyException {
// Create the message to be signed by concatenating the message body and the timestamp
var message = body + timestamp;
// Initialize the MAC routine with the secret key
var mac = Mac.getInstance("HmacSHA256");
var secret = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
mac.init(secret);
// Create a digest of the bytes in the message
byte[] digest = mac.doFinal(message.getBytes());
// Print the digest as lower-case hex characters
var enc = DatatypeConverter.printHexBinary(digest).toLowerCase();
return signature.equals(enc);
}
}