NueForm

Verificação de assinatura de webhook

Como verificar assinaturas de webhook do NueForm usando HMAC-SHA256, com exemplos de código em Node.js, Python, Go, PHP e Ruby.

Toda requisição de webhook do NueForm inclui uma assinatura criptográfica que permite verificar que a requisição é autêntica e não foi adulterada. Você deve sempre verificar essa assinatura antes de processar os dados do webhook.

Como a assinatura funciona

Quando o NueForm despacha um webhook, ele:

  1. Serializa o payload como uma string JSON.
  2. Calcula um hash HMAC-SHA256 dessa string JSON usando seu segredo do webhook como chave.
  3. Codifica o hash como uma string hexadecimal em letras minúsculas.
  4. Envia o digest hexadecimal no cabeçalho HTTP X-NueForm-Signature.

Do seu lado, você executa o mesmo cálculo sobre o corpo bruto da requisição e compara o resultado com o valor do cabeçalho. Se coincidirem, a requisição é genuína.

O cabeçalho de assinatura

text
X-NueForm-Signature: 5d41402abc4b2a76b9719d911017c592a3f6e7d4b9c1d5e8f2a7b3c6d9e0f1a2

O valor do cabeçalho é uma string hexadecimal de 64 caracteres (a saída do HMAC-SHA256).

Seu segredo do webhook

Seu segredo do webhook é uma string hexadecimal de 64 caracteres gerada a partir de 32 bytes aleatórios. Ele é único da sua conta e compartilhado entre todos os seus endpoints de webhook (tanto por formulário quanto globais).

Obtendo seu segredo

Busque seu segredo atual pela API:

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

Resposta:

json
{
  "secret": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
}

Se você ainda não tiver um segredo, o NueForm gera um automaticamente na primeira requisição.

Regenerando seu segredo

Se o seu segredo for comprometido, regenere-o imediatamente:

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

Regenerar seu segredo invalida imediatamente o antigo. Todas as entregas de webhook a partir desse momento usarão o novo segredo. Atualize seu código de verificação antes ou imediatamente depois de regenerar para evitar rejeitar webhooks válidos.

Exemplos de código de verificação

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

Ao usar Express.js, você precisa usar express.raw() ou express.text() para acessar o corpo bruto da requisição. Se você usar express.json(), o corpo será interpretado e serializado novamente, o que pode produzir uma string diferente da que foi assinada --- fazendo a verificação falhar.

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

Comparação em tempo constante

Todos os exemplos de código acima usam comparação em tempo constante (também chamada de comparação timing-safe) ao verificar assinaturas. Essa é uma boa prática de segurança que previne ataques de temporização.

Um ataque de temporização funciona medindo quanto tempo uma comparação de strings leva. Uma comparação ingênua com == retorna false assim que encontra o primeiro caractere divergente, então um invasor poderia descobrir um caractere da assinatura por vez medindo a latência das respostas.

Funções de comparação em tempo constante sempre levam o mesmo tempo, independentemente de quantos caracteres coincidem, tornando esse ataque inviável.

LinguagemFunção
Node.jscrypto.timingSafeEqual()
Pythonhmac.compare_digest()
Gohmac.Equal()
PHPhash_equals()
RubyRack::Utils.secure_compare()

Nunca use ===, == ou .equals() para comparar assinaturas HMAC. Sempre use a função de comparação em tempo constante nativa da sua linguagem.

O que fazer se a verificação falhar

Se a verificação de assinatura falhar, seu endpoint deve:

  1. Retornar um código de status 401 Unauthorized. Não processe o payload.
  2. Registrar a falha para depuração. Inclua o IP da requisição, o timestamp e (opcionalmente) a assinatura recebida.
  3. Não expor seu segredo em mensagens de erro ou logs.
  4. Investigar as causas comuns:
    • Segredo errado. Certifique-se de estar usando o segredo do webhook atual. Se você o regenerou recentemente, atualize seu código de verificação.
    • Transformação do corpo. Garanta que você está verificando o corpo bruto da requisição, não uma versão interpretada e serializada novamente. Middleware que faz o parse do JSON antes de o seu código de verificação executar é a causa mais comum de falhas.
    • Problemas de codificação. O corpo bruto deve ser tratado como bytes UTF-8. Garanta que seu framework não aplique transformações de codificação inesperadas.
    • Modificação por proxy. Se um proxy reverso (por exemplo, Cloudflare, nginx) estiver modificando o corpo da requisição, a assinatura não vai coincidir. Configure seu proxy para repassar o corpo sem alterações.

Checklist de solução de problemas

ProblemaSolução
A assinatura nunca coincideVerifique se você está lendo os bytes brutos do corpo, não um objeto JSON interpretado
A assinatura parou de coincidir de repenteVerifique se seu segredo do webhook foi regenerado
A assinatura coincide localmente, mas não em produçãoVerifique se há transformação do corpo por proxy ou CDN
O cabeçalho X-NueForm-Signature está ausenteGaranta que seu framework preserve cabeçalhos personalizados (alguns removem cabeçalhos com prefixo X-)

Próximos passos

  • Testes --- Teste a verificação de assinatura localmente
  • Payloads --- Entenda o formato do payload que você está verificando
  • Eventos --- Conheça os tipos de evento
Última atualização: 20 de julho de 2026