NueForm से प्रत्येक webhook request में cryptographic signature शामिल होता है जो आपको verify करने देता है कि request authentic है और इसमें tampering नहीं हुई है। Webhook data process करने से पहले आपको हमेशा इस signature को verify करना चाहिए।
Signing कैसे काम करता है
जब NueForm webhook dispatch करता है, यह:
- Payload को JSON string के रूप में serialize करता है।
- आपके webhook secret को key के रूप में उपयोग करके उस JSON string का HMAC-SHA256 hash compute करता है।
- Hash को lowercase hexadecimal string के रूप में encode करता है।
- Hex digest को
X-NueForm-SignatureHTTP header में भेजता है।
आपकी तरफ, आप raw request body पर same computation perform करते हैं और अपने result को header value से compare करते हैं। यदि वे match करते हैं, request genuine है।
Signature Header
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 करें:
curl https://app.nueform.com/api/v1/webhooks/secret \
-H "Authorization: Bearer nf_your_api_key"
Response:
{
"secret": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
}
यदि आपके पास अभी तक secret नहीं है, NueForm पहली request पर स्वचालित रूप से generate करता है।
अपना Secret Regenerate करना
यदि आपका secret compromise हो जाता है, तुरंत regenerate करें:
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
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
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
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
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
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 हो जाता है।
| Language | Function |
|---|---|
| Node.js | crypto.timingSafeEqual() |
| Python | hmac.compare_digest() |
| Go | hmac.Equal() |
| PHP | hash_equals() |
| Ruby | Rack::Utils.secure_compare() |
HMAC signatures compare करने के लिए कभी ===, ==, या .equals() उपयोग न करें। हमेशा अपनी language का built-in timing-safe comparison function उपयोग करें।
Verification Fail होने पर क्या करें
यदि signature verification fail होती है, आपका endpoint:
401 Unauthorizedstatus code return करे। Payload process न करें।- Failure log करे debugging के लिए। Request IP, timestamp, और (optionally) received signature शामिल करें।
- अपना secret expose न करे error messages या logs में।
- 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
| Issue | Solution |
|---|---|
| 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 कर देते हैं) |