Skip to content
← Back to Functions
Code

Webhook Signature Verifier

Verifies a signed webhook payload using a timestamp tolerance and shared secret.

Function signature

ogVerifyWebhookSignature(raw_body, signature, secret, timestamp = 0, tolerance_seconds = 300, algorithm = 'sha256')

Categories

  • Security

Parameters

raw_bodyRaw request body exactly as received.signatureSignature header value.secretShared webhook secret.timestampOptional provider timestamp.tolerance_secondsAccepted clock-skew window when timestamp is supplied.algorithmHMAC algorithm.

Return value

Short public-safe status message.

  • valid
  • algorithm
  • timestamp_checked

Compatibility

Existing function name and call order preserved; metadata signature corrected to source.

Minimum PHP version: 7.4

Security notes

Validate request method, identity, permissions, and caller-owned allowlists before use; keep secrets and internal paths out of public output.

Code

<?php

/*
 * Copyright (c) 2026 Jeffery L. Paris <jparis@phpog.com>.
 * Free for personal and internal use. Paid project use requires visible credit
 * to Jeffery L. Paris. Corporate use requires a paid license fee unless a
 * separate written license states otherwise.
 */

/**
 * Verifies a signed webhook payload using a timestamp tolerance and shared secret.
 *
 * Primary use case: Payment, CRM, deployment, or SaaS webhooks.
 * Typical inputs: raw body, signature header, timestamp, secret.
 * Typical output: boolean validation result.
 *
 * Implementation note: Use hash_hmac and hash_equals; reject stale timestamps.
 *
 * @param string $raw_body Raw request body exactly as received.
 * @param string $signature Signature header value.
 * @param string $secret Shared webhook secret.
 * @param int $timestamp Optional provider timestamp.
 * @param int $tolerance_seconds Accepted clock-skew window when timestamp is supplied.
 * @param string $algorithm HMAC algorithm.
 * @return array Structured verification result.
 */
function ogVerifyWebhookSignature($raw_body, $signature, $secret, $timestamp = 0, $tolerance_seconds = 300, $algorithm = 'sha256') {
	$result = array(
		'success' => false,
		'message' => '',
		'data' => array('valid' => false)
	);

	$raw_body = (string)$raw_body;
	$signature = trim((string)$signature);
	$secret = (string)$secret;
	$timestamp = (int)$timestamp;
	$tolerance_seconds = (int)$tolerance_seconds;
	$algorithm = strtolower(trim((string)$algorithm));

	if (empty($signature) || empty($secret)) {
		$result['message'] = 'Missing signature or secret.';
		return $result;
	}

	if (!in_array($algorithm, hash_hmac_algos(), true)) {
		$result['message'] = 'Unsupported signature algorithm.';
		return $result;
	}

	if ($tolerance_seconds < 60) {
		$tolerance_seconds = 60;
	}

	if ($timestamp > 0) {
		$age = abs(time() - $timestamp);
		if ($age > $tolerance_seconds) {
			$result['message'] = 'Webhook timestamp is outside the accepted window.';
			return $result;
		}
	}

	$normalized_signature = $signature;
	$prefix = $algorithm . '=';
	if (strpos($normalized_signature, $prefix) === 0) {
		$normalized_signature = substr($normalized_signature, strlen($prefix));
	}

	$base = $raw_body;
	if ($timestamp > 0) {
		$base = $timestamp . '.' . $raw_body;
	}

	$expected = hash_hmac($algorithm, $base, $secret);
	if (!hash_equals($expected, $normalized_signature)) {
		$result['message'] = 'Webhook signature mismatch.';
		return $result;
	}

	$result['success'] = true;
	$result['message'] = 'Webhook signature verified.';
	$result['data'] = array(
		'valid' => true,
		'algorithm' => $algorithm,
		'timestamp_checked' => ($timestamp > 0)
	);

	return $result;
}