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 webhook request is from the expected source

 * @param ServerRequestInterface $request

 * @param RequestHandlerInterface $handler

 * @return ResponseInterface

 */

 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 verifyWebhookRequest(

 string $payload,

 string $signature,

 int $timestamp,

 string $secret

 ): array {

 // Check timestamp to prevent replay attacks

 if (!self::isTimestampValid($timestamp)) {

 return [

 'valid' => false,

 'error' => 'Timestamp too old or in future (possible replay attack)'

 ];

 }

 // Verify signature

 $isValid = self::verifyWebhookWithTimestamp(

 $payload,

 $signature,

 $secret,

 $timestamp

 );

 if (!$isValid) {

 return [

 'valid' => false,

 'error' => 'Invalid signature'

 ];

 }

 return ['valid' => true];

 }

 /**

 * Validate that 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 with timestamp (for recipients)

 */

 public static function verifyWebhookWithTimestamp(

 string $payload,

 string $signature,

 string $secret,

 int $timestamp

 ): bool {

 $signedData = $payload . $timestamp;

 $expectedSignature = hash_hmac('sha256', $signedData, $secret);

 return hash_equals($expectedSignature, $signature);

 }

}

 

   

 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);

 }

}