Buscar na documentação
ctrl+4K
Módulos
Authentication
Merchant
Catalog
Order
Events
Logistics
Shipping
Review
Financial
Soluções

Validação de assinatura

Todas as requests de webhook incluem o header X-IFood-Signature para garantir autenticidade. Valide sempre antes de processar eventos.
Obrigatório para homologação: Rejeite requests com assinatura inválida. O iFood testa sua integração enviando eventos com assinaturas incorretas.

URLs de webhook ficam expostas na internet. Sem validação, qualquer pessoa pode enviar eventos falsos para seu endpoint.Proteção contra:
  • Fraudes e ataques
  • Eventos forjados por terceiros
  • Manipulação de dados de pedidos
O iFood audita todas as tentativas de entrega. Validação correta evita processar dados maliciosos.
O iFood usa HMAC-SHA256 para assinar cada request:
  1. Gera assinatura: HMAC do body usando seu client_secret
  2. Codifica em hex: Valor final em formato hexadecimal
  3. Envia no header: X-IFood-Signature: <assinatura>
Você deve:
  1. Ler byte array bruto do body (sem transformações)
  2. Gerar HMAC usando seu client_secret
  3. Comparar com assinatura recebida (comparação segura)

Acesse Developer Portal → Meus Apps → [seu app] → Credenciais e copie o client_secret (mesmo usado para gerar tokens).
Segurança: Armazene o secret de forma segura (variável de ambiente, secret manager). Nunca commite no código.

❌ Erro comum:
# NÃO FAÇA ISSO
event = request.json  # Parse antes de validar
validate_signature(event)  # Assinatura pode estar errada!
✅ Correto:
# FAÇA ISSO
raw_body = request.data  # Byte array bruto
validate_signature(raw_body, request.headers['X-IFood-Signature'])
event = json.loads(raw_body)  # Parse só depois de validar

Por quê?

JSON {"a":1,"b":2} e {"b":2,"a":1} são equivalentes, mas geram byte arrays diferentes. Frameworks podem reordenar propriedades ou adicionar/remover espaços antes de você acessar o conteúdo.Compare assinaturas usando função constant-time para evitar timing attacks. ❌ Inseguro:
if generated_signature == received_signature:  # Vulnerável a timing attack
✅ Seguro:
import hmac
if hmac.compare_digest(generated_signature, received_signature):  # Constant-time

Exemplos de código

Python
import hmac
import hashlib

def validate_signature(secret: str, body: bytes, signature: str) -> bool:
    """
    Valida assinatura do webhook.
    
    Args:
        secret: Client secret do aplicativo
        body: Byte array bruto do request body
        signature: Valor do header X-IFood-Signature
    
    Returns:
        True se assinatura é válida
    """
    expected = hmac.new(
        secret.encode('utf-8'),
        body,
        hashlib.sha256
    ).hexdigest()
    
    return hmac.compare_digest(expected, signature)

# Uso no Flask
@app.route('/webhook', methods=['POST'])
def webhook():
    signature = request.headers.get('X-IFood-Signature')
    
    if not validate_signature(CLIENT_SECRET, request.data, signature):
        return 'Invalid signature', 401
    
    event = request.json
    # ... processar evento
    return '', 202
Bibliotecas
Node.js
const crypto = require('crypto');

function validateSignature(secret, body, signature) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(body)
    .digest('hex');
  
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}

// Uso no Express
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-ifood-signature'];
  
  if (!validateSignature(CLIENT_SECRET, req.body, signature)) {
    return res.status(401).send('Invalid signature');
  }
  
  const event = JSON.parse(req.body);
  // ... processar evento
  res.status(202).send();
});
Use express.raw() para preservar byte array.
Bibliotecas
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 WebhookSignature {
    
    private String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }
    
    public boolean validateSignature(String secret, String body, String signature) {
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKey = new SecretKeySpec(
                secret.getBytes(StandardCharsets.UTF_8),
                "HmacSHA256"
            );
            mac.init(secretKey);
            
            byte[] hmacBytes = mac.doFinal(body.getBytes(StandardCharsets.UTF_8));
            String expected = bytesToHex(hmacBytes);
            
            return expected.equals(signature);
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            return false;
        }
    }
}
Bibliotecas
Go
package main

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

func validateSignature(secret string, body []byte, signature string) bool {
    h := hmac.New(sha256.New, []byte(secret))
    h.Write(body)
    expected := hex.EncodeToString(h.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(signature))
}

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    body, err := io.ReadAll(r.Body)
    if err != nil {
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    
    signature := r.Header.Get("X-IFood-Signature")
    
    if !validateSignature(CLIENT_SECRET, body, signature) {
        w.WriteHeader(http.StatusUnauthorized)
        return
    }
    
    // ... processar evento
    w.WriteHeader(http.StatusAccepted)
}
Bibliotecas:
Ruby

require 'openssl'

def validate_signature(secret, body, signature)
  expected = OpenSSL::HMAC.hexdigest('SHA256', secret, body)
  Rack::Utils.secure_compare(expected, signature)
end

# Uso no Sinatra/Rails
post '/webhook' do
  body = request.body.read
  signature = request.env['HTTP_X_IFOOD_SIGNATURE']
  
  unless validate_signature(CLIENT_SECRET, body, signature)
    halt 401, 'Invalid signature'
  end
  
  event = JSON.parse(body)
  # ... processar evento
  status 202
end
Bibliotecas: A mesma mensagem pode chegar com formatações diferentes. Sua validação deve suportar todas.Exemplo: Evento PLACED com diferentes formatações (mesmo conteúdo, assinaturas diferentes):Sem espaços
{"code":"PLC","createdAt":"2023-02-20T18:19:03.20162269Z","fullCode":"PLACED","id":"a38ba215-f949-4b2c-982a-0582a9d0c10e","merchantId":"cad65e8f-6fc6-438a-b159-e64a902a6b9a","orderId":"2c97e104-35ed-4c18-85d7-854a40b6b9e3"}
X-IFood-Signature: 6f9ed23a7b505a3b6907c5f6eb2ad1b056fbf35a643d365a9a072ed7aabca153 Com espaços

{ "code":"PLC", "createdAt":"2023-02-20T18:19:03.20162269Z", "fullCode":"PLACED", "id":"a38ba215-f949-4b2c-982a-0582a9d0c10e", "merchantId":"cad65e8f-6fc6-438a-b159-e64a902a6b9a", "orderId":"2c97e104-35ed-4c18-85d7-854a40b6b9e3" }
X-IFood-Signature: cf7e092c9148a48f5ee5f12b947f46b331eac6bf0745e1e1d0f3df722e219df3Com quebras de linha
{
    "code":"PLC",
    "createdAt":"2023-02-20T18:19:03.20162269Z",
    "fullCode":"PLACED",
    "id":"a38ba215-f949-4b2c-982a-0582a9d0c10e",
    "merchantId":"cad65e8f-6fc6-438a-b159-e64a902a6b9a",
    "orderId":"2c97e104-35ed-4c18-85d7-854a40b6b9e3"
}
X-IFood-Signature: adf5446334f754e73588f3ae10b308306307f0c797f7f678912d740c6deddf6aPropriedades reordenadas

{"merchantId":"cad65e8f-6fc6-438a-b159-e64a902a6b9a","orderId":"2c97e104-35ed-4c18-85d7-854a40b6b9e3","code":"PLC","createdAt":"2023-02-20T18:19:03.20162269Z","fullCode":"PLACED","id":"a38ba215-f949-4b2c-982a-0582a9d0c10e"}
X-IFood-Signature: e2d26f22f89932ff3d23a699031b22d6f30323501430dc08c3a971dd875e23b5

Teste com curl

# Usando secret "dummysecret"
curl -X POST http://localhost:8080/webhook \
  -H 'X-IFood-Signature: 6f9ed23a7b505a3b6907c5f6eb2ad1b056fbf35a643d365a9a072ed7aabca153' \
  -H 'Content-Type: application/json' \
  -d '{"code":"PLC","createdAt":"2023-02-20T18:19:03.20162269Z","fullCode":"PLACED","id":"a38ba215-f949-4b2c-982a-0582a9d0c10e","merchantId":"cad65e8f-6fc6-438a-b159-e64a902a6b9a","orderId":"2c97e104-35ed-4c18-85d7-854a40b6b9e3"}'
Esperado: 202 Accepted

Teste com assinatura inválida

curl -X POST http://localhost:8080/webhook \
  -H 'X-IFood-Signature: invalid_signature_here' \
  -H 'Content-Type: application/json' \
  -d '{"code":"PLC","id":"123"}'
Esperado: 401 Unauthorized

Problema: Assinatura sempre inválida

Causa 1: Validando após parse
# ❌ Errado
event = request.json
validate_signature(json.dumps(event), signature)  # JSON pode estar reordenado!

# ✅ Correto
validate_signature(request.data, signature)  # Byte array bruto
Causa 2: Client secret erradoVerifique se está usando o secret correto no Developer Portal (aba Credenciais).Causa 3: Encoding incorreto Use sempre UTF-8:
secret.encode('utf-8')  # ✅
secret.encode('ascii')  # ❌

Problema: Funciona localmente, falha em produção

Causa: Framework/proxy modificando body antes de você acessar.
Solução
  • Flask: Use reques.data (não request.json)
  • Express: Use express.raw({ type: 'application/json' })
  • Rails: Use request.body.read
  • Desabilite middlewares que modificam o body
Antes de homologar, cofirme:
  • Valida assinatura ANTES de fazer parse do JSON
  • Usa byte array bruto do body (sem transformações)
  • Usa comparação segura (constant-time)
  • Rejeita requests com assinatura inválida (401)
  • Client secret armazenado de forma segura
  • Testa com diferentes formatações (espaços, quebras, ordem)
  • Funciona em ambiente de produção (não só local)
Esta página foi útil?
Avalie sua experiência no novo Developer portal: