Skip to main content

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