NueForm

Webhook Signature Verification

Node.js, Python, Go, PHP, और Ruby में code samples के साथ HMAC-SHA256 उपयोग करके NueForm webhook signatures verify करने का तरीका।

NueForm से प्रत्येक webhook request में cryptographic signature शामिल होता है जो आपको verify करने देता है कि request authentic है और इसमें tampering नहीं हुई है। Webhook data process करने से पहले आपको हमेशा इस signature को verify करना चाहिए।

Signing कैसे काम करता है

जब NueForm webhook dispatch करता है, यह:

  1. Payload को JSON string के रूप में serialize करता है।
  2. आपके webhook secret को key के रूप में उपयोग करके उस JSON string का HMAC-SHA256 hash compute करता है।
  3. Hash को lowercase hexadecimal string के रूप में encode करता है।
  4. Hex digest को X-NueForm-Signature HTTP header में भेजता है।

आपकी तरफ, आप raw request body पर same computation perform करते हैं और अपने result को header value से compare करते हैं। यदि वे match करते हैं, request genuine है।

Signature Header

text
X-NueForm-Signature: 5d41402abc4b2a76b9719d911017c592a3f6e7d4b9c1d5e8f2a7b3c6d9e0f1a2

Header value 64-character hexadecimal string (HMAC-SHA256 का output) है।

आपका Webhook Secret

आपका webhook secret 32 random bytes से generate हुआ 64-character hexadecimal string है। यह आपके account के लिए unique है और आपके सभी webhook endpoints (per-form और global दोनों) में shared है।

अपना Secret Retrieve करना

API के माध्यम से अपना current secret fetch करें:

bash
curl https://app.nueform.com/api/v1/webhooks/secret \
  -H "Authorization: Bearer nf_your_api_key"

Response:

json
{
  "secret": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
}

यदि आपके पास अभी तक secret नहीं है, NueForm पहली request पर स्वचालित रूप से generate करता है।

अपना Secret Regenerate करना

यदि आपका secret compromise हो जाता है, तुरंत regenerate करें:

bash
curl -X POST https://app.nueform.com/api/v1/webhooks/secret/regenerate \
  -H "Authorization: Bearer nf_your_api_key"

अपना secret regenerate करने पर पुराना तुरंत invalidate हो जाता है। उस point से आगे सभी webhook deliveries नया secret उपयोग करेंगी। Valid webhooks reject होने से बचने के लिए regenerate करने से पहले या तुरंत बाद अपना verification code update करें।

Verification Code Samples

Node.js

javascript
import crypto from 'crypto';

function verifyWebhookSignature(rawBody, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  // Use timing-safe comparison to prevent timing attacks
  const a = Buffer.from(signature, 'hex');
  const b = Buffer.from(expected, 'hex');

  if (a.length !== b.length) {
    return false;
  }

  return crypto.timingSafeEqual(a, b);
}

// Express.js example
app.post('/webhooks/nueform', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-nueform-signature'];
  const rawBody = req.body.toString();

  if (!verifyWebhookSignature(rawBody, signature, process.env.NUEFORM_WEBHOOK_SECRET)) {
    console.error('Invalid webhook signature');
    return res.status(401).send('Invalid signature');
  }

  const payload = JSON.parse(rawBody);
  console.log('Verified webhook:', payload.event, payload.responseId);

  // Process the webhook asynchronously
  processWebhookAsync(payload);

  res.status(200).send('OK');
});

Express.js उपयोग करते समय, raw request body access करने के लिए आपको express.raw() या express.text() उपयोग करना होगा। यदि आप express.json() उपयोग करते हैं, body parse और re-serialize होगी, जो signed string से different string produce कर सकती है --- जिससे verification fail होगी।

Python

python
import hashlib
import hmac

def verify_webhook_signature(raw_body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode('utf-8'),
        raw_body,
        hashlib.sha256
    ).hexdigest()

    # Use timing-safe comparison
    return hmac.compare_digest(expected, signature)


# Flask example
from flask import Flask, request, abort

app = Flask(__name__)

@app.route('/webhooks/nueform', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-NueForm-Signature', '')
    raw_body = request.get_data()

    if not verify_webhook_signature(raw_body, signature, WEBHOOK_SECRET):
        abort(401, 'Invalid signature')

    payload = request.get_json()
    print(f"Verified webhook: {payload['event']} {payload['responseId']}")

    # Process asynchronously (e.g., enqueue to Celery)
    process_webhook.delay(payload)

    return 'OK', 200

Go

go
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"io"
	"net/http"
)

func verifyWebhookSignature(body []byte, signature string, secret string) bool {
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write(body)
	expected := hex.EncodeToString(mac.Sum(nil))

	// hmac.Equal performs a constant-time comparison
	return hmac.Equal([]byte(expected), []byte(signature))
}

func webhookHandler(w http.ResponseWriter, r *http.Request) {
	body, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, "Failed to read body", http.StatusBadRequest)
		return
	}
	defer r.Body.Close()

	signature := r.Header.Get("X-NueForm-Signature")
	if !verifyWebhookSignature(body, signature, webhookSecret) {
		http.Error(w, "Invalid signature", http.StatusUnauthorized)
		return
	}

	// Process webhook payload
	w.WriteHeader(http.StatusOK)
	w.Write([]byte("OK"))
}

PHP

php
<?php

function verifyWebhookSignature(string $rawBody, string $signature, string $secret): bool {
    $expected = hash_hmac('sha256', $rawBody, $secret);

    // Use timing-safe comparison
    return hash_equals($expected, $signature);
}

// Usage
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_NUEFORM_SIGNATURE'] ?? '';
$secret = getenv('NUEFORM_WEBHOOK_SECRET');

if (!verifyWebhookSignature($rawBody, $signature, $secret)) {
    http_response_code(401);
    echo 'Invalid signature';
    exit;
}

$payload = json_decode($rawBody, true);
error_log("Verified webhook: {$payload['event']} {$payload['responseId']}");

// Process the webhook
processWebhook($payload);

http_response_code(200);
echo 'OK';

Ruby

ruby
require 'openssl'
require 'json'

def verify_webhook_signature(raw_body, signature, secret)
  expected = OpenSSL::HMAC.hexdigest('SHA256', secret, raw_body)

  # Use timing-safe comparison
  Rack::Utils.secure_compare(expected, signature)
end

# Sinatra example
post '/webhooks/nueform' do
  raw_body = request.body.read
  signature = request.env['HTTP_X_NUEFORM_SIGNATURE'] || ''

  unless verify_webhook_signature(raw_body, signature, ENV['NUEFORM_WEBHOOK_SECRET'])
    halt 401, 'Invalid signature'
  end

  payload = JSON.parse(raw_body)
  logger.info "Verified webhook: #{payload['event']} #{payload['responseId']}"

  # Process asynchronously
  WebhookProcessorJob.perform_async(payload)

  status 200
  body 'OK'
end

Timing-Safe Comparison

ऊपर के सभी code samples signatures check करते समय timing-safe comparison (constant-time comparison भी कहा जाता है) उपयोग करते हैं। यह एक security best practice है जो timing attacks prevent करती है।

Timing attack string comparison में लगने वाले समय को measure करके काम करता है। Naive == comparison first mismatched character मिलते ही false return करता है, इसलिए attacker response latency measure करके signature का एक-एक character learn कर सकता है।

Timing-safe comparison functions हमेशा same amount of time लेती हैं चाहे कितने भी characters match करें, जिससे यह attack infeasible हो जाता है।

LanguageFunction
Node.jscrypto.timingSafeEqual()
Pythonhmac.compare_digest()
Gohmac.Equal()
PHPhash_equals()
RubyRack::Utils.secure_compare()

HMAC signatures compare करने के लिए कभी ===, ==, या .equals() उपयोग न करें। हमेशा अपनी language का built-in timing-safe comparison function उपयोग करें।

Verification Fail होने पर क्या करें

यदि signature verification fail होती है, आपका endpoint:

  1. 401 Unauthorized status code return करे। Payload process न करें।
  2. Failure log करे debugging के लिए। Request IP, timestamp, और (optionally) received signature शामिल करें।
  3. अपना secret expose न करे error messages या logs में।
  4. Common causes investigate करे:
    • गलत secret। सुनिश्चित करें कि आप current webhook secret उपयोग कर रहे हैं। यदि आपने हाल ही में regenerate किया, अपना verification code update करें।
    • Body transformation। सुनिश्चित करें कि आप raw request body पर verify कर रहे हैं, parsed-and-reserialized version पर नहीं। Middleware जो आपके verification code से पहले JSON parse करता है, failures का सबसे common cause है।
    • Encoding issues। Raw body को UTF-8 bytes के रूप में treat किया जाना चाहिए। सुनिश्चित करें कि आपका framework unexpected encoding transformations apply नहीं कर रहा।
    • Proxy modification। यदि reverse proxy (जैसे, Cloudflare, nginx) request body modify कर रहा है, signature match नहीं करेगा। अपने proxy को body unchanged pass करने के लिए configure करें।

Troubleshooting Checklist

IssueSolution
Signature कभी match नहीं करताVerify करें कि आप raw body bytes read कर रहे हैं, parsed JSON object नहीं
Signature अचानक match करना बंद हो गयाCheck करें कि आपका webhook secret regenerate तो नहीं हुआ
Locally match करता है लेकिन production में नहींProxy या CDN body transformation check करें
X-NueForm-Signature header missing हैसुनिश्चित करें कि आपका framework custom headers preserve करता है (कुछ X- prefixed headers strip कर देते हैं)

Next Steps

  • Testing --- Locally signature verification test करें
  • Payloads --- जिस payload format को verify कर रहे हैं उसे समझें
  • Events --- Event types के बारे में जानें
अंतिम अपडेट: 6 अप्रैल 2026