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