Skip to content
Last updated

Validar HMAC-SHA256

Todo webhook enviado pela Movvia inclui o header x-movvia-signature com a assinatura HMAC-SHA256 do payload. Sempre valide a assinatura antes de processar.

Como a assinatura é gerada

HMAC-SHA256(secret, payload_utf8) → hex string

Onde:

  • secret é o webhook_secret fornecido no onboarding.
  • payload_utf8 é o body JSON bruto (bytes), não o objeto deserializado.
Use o body bruto

Deserializar e re-serializar o JSON pode alterar a ordem das chaves e quebrar a verificação. Leia o body como bytes antes de qualquer parse.

Node.js

import crypto from 'crypto';
import express from 'express';

const app = express();

// Capturar body como Buffer antes do parse
app.use(express.json({
  verify: (req: any, _res, buf) => { req.rawBody = buf; }
}));

function verificarAssinatura(
  rawBody: Buffer,
  signature: string,
  secret: string
): boolean {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expected, 'hex')
  );
}

app.post('/webhook', (req: any, res) => {
  const sig = req.headers['x-movvia-signature'] as string;

  if (!sig || !verificarAssinatura(req.rawBody, sig, process.env.MV_WEBHOOK_SECRET!)) {
    return res.status(401).json({ erro: 'Assinatura inválida' });
  }

  // processar req.body com segurança
  console.log('Evento válido:', req.body.evento);
  res.status(200).send('OK');
});

Python

import hashlib
import hmac
import os
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ['MV_WEBHOOK_SECRET'].encode()

def verificar_assinatura(payload: bytes, signature: str) -> bool:
    expected = hmac.new(SECRET, payload, hashlib.sha256).hexdigest()
    return hmac.compare_digest(signature, expected)

@app.route('/webhook', methods=['POST'])
def webhook():
    sig = request.headers.get('x-movvia-signature', '')
    payload = request.get_data()  # body bruto

    if not verificar_assinatura(payload, sig):
        abort(401)

    evento = request.json
    print(f"Evento válido: {evento['evento']}")
    return 'OK', 200

Go

package main

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

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

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    sig := r.Header.Get("x-movvia-signature")
    body, _ := io.ReadAll(r.Body)

    if !verificarAssinatura(body, sig, os.Getenv("MV_WEBHOOK_SECRET")) {
        http.Error(w, "Assinatura inválida", http.StatusUnauthorized)
        return
    }

    w.WriteHeader(http.StatusOK)
}

Erros comuns

ProblemaCausa provávelSolução
Assinatura sempre inválidaBody deserializado antes da verificaçãoUsar body bruto (bytes)
Comparação insegura== em vez de timingSafeEqual/compare_digestSempre usar comparação em tempo constante
Secret incorretowebhook_secret diferente do client_secretSão credenciais distintas — verificar o correto no onboarding
Testar localmente

Use o endpoint de simulação do sandbox para gerar eventos reais com assinatura válida. Veja o Quickstart.