Verifying Webhooks
When Influencer Hero sends a webhook to your endpoint, it includes a cryptographic signature in the request headers. This signature is intended to help you verify that the request genuinely originated from Influencer Hero and has not been tampered with in transit.
Steps to Verify
- Retrieve the timestamp from
X-InfluencerHero-Timestamp
. - Retrieve the signature from
X-InfluencerHero-Signature
. - Read the raw request body (the JSON payload) as bytes.
- Compute the HMAC-SHA256 signature over the raw request body, using the same secret key.
- Compare the computed signature with the
X-InfluencerHero-Signature
header value using a timing-safe comparison. - (Optional) Confirm the timestamp is within an acceptable window (e.g., 5 minutes) to prevent replay attacks.
Example Verification Logic in Various Languages
Below are short examples illustrating how you might perform the verification. You may need to adapt them to specific frameworks or libraries in your environment.
Python
import hmac
import hashlib
import time
def verify_influencerhero_webhook(request_body: bytes, timestamp_header: str, signature_header: str, secret_key: str) -> bool:
# 1. Compute local HMAC-SHA256 of the raw request body with the secret key
computed_signature = hmac.new(
key=secret_key.encode('utf-8'),
msg=request_body,
digestmod=hashlib.sha256
).hexdigest()
# 2. Timing-safe comparison to prevent leaks in signature checking
if not hmac.compare_digest(computed_signature, signature_header):
return False
# 3. (Optional) Check if the timestamp is within 5 minutes of current time
current_timestamp = int(time.time())
request_timestamp = int(timestamp_header or 0)
if abs(current_timestamp - request_timestamp) > 300:
return False
return True
Node.js (Express)
const crypto = require('crypto');
function verifyInfluencerHeroWebhook(rawBody, timestamp, signature, secretKey) {
// 1. Compute local HMAC-SHA256
const computedSignature = crypto
.createHmac('sha256', secretKey)
.update(rawBody)
.digest('hex');
// 2. Timing-safe comparison
if (!crypto.timingSafeEqual(Buffer.from(computedSignature), Buffer.from(signature))) {
return false;
}
// 3. (Optional) Check timestamp
const current = Math.floor(Date.now() / 1000);
if (Math.abs(current - parseInt(timestamp, 10)) > 300) {
return false;
}
return true;
}
// Usage example:
// Make sure to capture the raw request body in `req.rawBody` (Buffer) before parsing
app.post('/webhook', (req, res) => {
const timestamp = req.get('X-InfluencerHero-Timestamp');
const signature = req.get('X-InfluencerHero-Signature');
const verified = verifyInfluencerHeroWebhook(
req.rawBody, // Buffer of the raw request body
timestamp,
signature,
process.env.INFLUENCERHERO_SECRET // your stored secret
);
if (!verified) {
return res.status(400).send('Invalid signature');
}
// If verified, process the webhook
res.send('OK');
});
PHP
<?php
function verifyInfluencerHeroWebhook($rawBody, $timestamp, $signature, $secretKey) {
// 1. Compute expected HMAC-SHA256 signature
$computedSignature = hash_hmac('sha256', $rawBody, $secretKey);
// 2. Use a timing-safe comparison
if (!hash_equals($computedSignature, $signature)) {
return false;
}
// 3. (Optional) Check timestamp (e.g., 5-minute window)
$currentTime = time();
if (abs($currentTime - (int)$timestamp) > 300) {
return false;
}
return true;
}
// Usage in a typical endpoint:
$rawBody = file_get_contents('php://input'); // raw POST body
$timestamp = $_SERVER['HTTP_X_INFLUENCERHERO_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_INFLUENCERHERO_SIGNATURE'] ?? '';
$secretKey = 'YOUR_WEBHOOK_SECRET';
if (!verifyInfluencerHeroWebhook($rawBody, $timestamp, $signature, $secretKey)) {
http_response_code(400);
echo "Invalid signature";
exit;
}
// Proceed if verified
http_response_code(200);
echo "OK";
Ruby
require 'openssl'
require 'time'
def verify_influencerhero_webhook(raw_body, timestamp, signature, secret_key)
# 1. Compute local HMAC-SHA256
computed_signature = OpenSSL::HMAC.hexdigest('SHA256', secret_key, raw_body)
# 2. Timing-safe comparison
return false unless Rack::Utils.secure_compare(computed_signature, signature)
# 3. (Optional) Check timestamp (5-minute window)
request_time = Time.at(timestamp.to_i)
return false if (Time.now.utc - request_time).abs > 300
true
end
# Usage (e.g., in a Sinatra/Rails controller):
post '/webhook' do
raw_body = request.body.read
timestamp = request.env['HTTP_X_INFLUENCERHERO_TIMESTAMP']
signature = request.env['HTTP_X_INFLUENCERHERO_SIGNATURE']
secret_key = ENV['INFLUENCERHERO_SECRET']
unless verify_influencerhero_webhook(raw_body, timestamp, signature, secret_key)
halt 400, 'Invalid signature'
end
status 200
'OK'
end
Java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
public class WebhookVerifier {
public static boolean verifyInfluencerHeroWebhook(
byte[] rawBody,
String timestamp,
String signature,
String secretKey
) {
String computedSignature;
try {
// 1. Compute local HMAC-SHA256
Mac hmacSha256 = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
hmacSha256.init(secretKeySpec);
byte[] hash = hmacSha256.doFinal(rawBody);
computedSignature = bytesToHex(hash);
// 2. Timing-safe comparison
if (!timingSafeEquals(computedSignature, signature)) {
return false;
}
// 3. (Optional) timestamp validation
long currentTime = System.currentTimeMillis() / 1000L; // seconds
long requestTime = Long.parseLong(timestamp);
if (Math.abs(currentTime - requestTime) > 300) {
return false;
}
} catch (NoSuchAlgorithmException | InvalidKeyException | NumberFormatException e) {
return false;
}
return true;
}
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
// Simple timing-safe comparison
private static boolean timingSafeEquals(String a, String b) {
if (a.length() != b.length()) return false;
int result = 0;
for (int i = 0; i < a.length(); i++) {
result |= a.charAt(i) ^ b.charAt(i);
}
return result == 0;
}
}
Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"math"
"net/http"
"strconv"
"time"
)
func verifyInfluencerHeroWebhook(rawBody []byte, timestamp, signature, secretKey string) bool {
// 1. Compute local HMAC-SHA256
mac := hmac.New(sha256.New, []byte(secretKey))
mac.Write(rawBody)
expected := hex.EncodeToString(mac.Sum(nil))
// 2. Timing-safe comparison
if !timingSafeCompare(expected, signature) {
return false
}
// 3. (Optional) Check timestamp
currentSec := time.Now().Unix()
reqSec, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil {
return false
}
if math.Abs(float64(currentSec-reqSec)) > 300 {
return false
}
return true
}
// Timing-safe compare for two hex strings of equal length.
func timingSafeCompare(a, b string) bool {
if len(a) != len(b) {
return false
}
var diff byte
for i := 0; i < len(a); i++ {
diff |= a[i] ^ b[i]
}
return diff == 0
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
rawBody := make([]byte, r.ContentLength)
r.Body.Read(rawBody)
timestamp := r.Header.Get("X-InfluencerHero-Timestamp")
signature := r.Header.Get("X-InfluencerHero-Signature")
secretKey := "YOUR_WEBHOOK_SECRET"
if !verifyInfluencerHeroWebhook(rawBody, timestamp, signature, secretKey) {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Invalid signature"))
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
func main() {
http.HandleFunc("/webhook", webhookHandler)
http.ListenAndServe(":8080", nil)
}
C#
using System;
using System.Security.Cryptography;
using System.Text;
public class InfluencerHeroWebhookVerifier
{
public static bool VerifyInfluencerHeroWebhook(
byte[] rawBody,
string timestamp,
string signature,
string secretKey)
{
// 1. Compute local HMAC-SHA256
using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secretKey)))
{
var computedHash = hmac.ComputeHash(rawBody);
var computedSignature = BitConverter.ToString(computedHash).Replace("-", "").ToLowerInvariant();
// 2. Timing-safe comparison
if (!TimingSafeCompare(computedSignature, signature))
{
return false;
}
}
// 3. (Optional) Check timestamp (e.g., 5 minutes)
if (!long.TryParse(timestamp, out long ts))
{
return false;
}
var currentTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
if (Math.Abs(currentTime - ts) > 300)
{
return false;
}
return true;
}
private static bool TimingSafeCompare(string a, string b)
{
if (a.Length != b.Length)
return false;
int result = 0;
for (int i = 0; i < a.Length; i++)
{
result |= a[i] ^ b[i];
}
return (result == 0);
}
}
Security Recommendations
- Always use HTTPS when setting up your webhook endpoint. This ensures payloads and headers are encrypted in transit.
- Keep your webhook signing secret as safe as you would a password or API key.
- Consider periodically rotating your webhook secret if you suspect it may have been compromised.
- Enforce a timeout window by comparing the
X-InfluencerHero-Timestamp
against the current time.
Implementing these steps allows your application to handle webhooks securely, ensuring that any requests not properly signed (or too old) are rejected.