logo
logo

Docs Webhook Overview Assinatura das Mensagens

Visão geral

Essa doc apresenta um guia sobre o que é a assinatura de uma mensagem e como fazer a validação dessa assinatura que vem no header X-IFood-Signature em todas as requests feitas pelos nossos serviços de webhook.

O primeiro ponto que temos que entender é a necessidade desse header. Como as URLs dos webhooks das integrações são expostas para a internet, a integração precisa de algum mecanismo seguro para saber se uma request veio realmente do iFood. A escolha do iFood foi a de implementar um header de validação da request. A isso chamamos de assinatura da mensagem.

Nota: Apesar de ser uma validação interna da integração, rejeitar requests com assinaturas inválidas faz parte do processo de homologação do webhook no iFood, e podemos enviar eventos com assinatura inválida de modo aleatório para validar se a integração continua de acordo com nossos critérios de homologação. De qualquer maneira, essa validação é de interesse da integração para evitar fraudes. Temos auditoria dos eventos entregues, tentativas de entregas e eventos descartados.

O webhook implementa um mecanismo de keyed-hash message authentication code (HMAC) com SHA256 para gerar uma assinatura para cada mensagem enviada, fazendo o encode final em hexadecimal.

Essa assinatura é gerada usando o client secret da sua aplicação (o mesmo utilizado para gerar tokens) encontrado no portal do desenvolvedor na página do aplicativo na aba "Credenciais". A assinatura é enviada através do header X-IFood-Signature em cada requisição.

Para validar essa assinatura, a integração deve gerar o HMAC da mensagem recebida usando o secret (armazenado em local seguro) e comparar com a assinatura recebida usando um algoritmo de comparação seguro (exemplo).

Algumas bibliotecas com implementações seguras do algoritmo existem como parte da biblioteca padrão da maioria das linguagens de programação amplamente usadas:

Atenção

Como campos podem ser adicionados em qualquer evento a qualquer momento sem que isso seja uma breaking change, a validação da mensagem deve ser feita antes do conteúdo dela ser transformado em um objeto da linguagem. Isso significa utilizar o byte array do body "as-is" para a geração da assinatura de comparação, sem nenhum tipo de transformação.

Isso também é importante pois como estamos lidando com JSONs, {"prop1": "value1", "prop2": "value2"} é equivalente a {"prop2": "value2", "prop1": "value1"} para os parsers/encoders pois a ordenação das propriedade não faz parte da definição de um JSON, mas o byte array formado pelos dois objetos são diferentes. Também é possível que alguns caracteres sejam encodados de maneiras diferentes dependendo da biblioteca ou linguagem sendo utilizada.

Todos os exemplos são válidos para o mesmo JSON, mas com diferentes formatações e devem ser suportados pela sua integração:

{
    "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"
}

Os exemplos mostram como a formatação afeta a assinatura:

  • Sem espaços ou quebra de linha: X-IFood-Signature: 6f9ed23a7b505a3b6907c5f6eb2ad1b056fbf35a643d365a9a072ed7aabca153. Payload:
{"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"}
  • Com espaços, sem quebra de linha: 'X-IFood-Signature: cf7e092c9148a48f5ee5f12b947f46b331eac6bf0745e1e1d0f3df722e219df3'. Payload:
{ "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" }
  • Com espaços, e quebra de linha: 'X-IFood-Signature: adf5446334f754e73588f3ae10b308306307f0c797f7f678912d740c6deddf6a'. Payload:
{
    "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"
}
  • Com propriedades em ordem diferente: X-IFood-Signature: e2d26f22f89932ff3d23a699031b22d6f30323501430dc08c3a971dd875e23b5. Payload:
{"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"}

Segue um snippet Java de como fazer a validação da mensagem:

private String bytesToHexString(byte[] bytes) {
    var sb = new StringBuilder();
    for (var b : bytes) {
        var hex = String.format("%02x", b);
        sb.append(hex);
    }
    return sb.toString();
}

private boolean verifyHmacSHA256(String secret, String data, String expectedSignature) {
    try {
        var mac = Mac.getInstance("HmacSHA256");
        var secretKeySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
        mac.init(secretKeySpec);
        var hmacBytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
        return bytesToHexString(hmacBytes).equals(expectedSignature);
    } catch (NoSuchAlgorithmException | InvalidKeyException e) {
        return false;
    }
}

Segue um snippet de um endpoint em golang:

http.HandleFunc("/webhook", func(w http.ResponseWriter, r *http.Request) {
  body, err := io.ReadAll(r.Body)
  if err != nil {
    fmt.Printf("could not read body")
    w.WriteHeader(400)
    return
  }

  headerSignature := r.Header.Get("X-IFood-Signature")

  signature, err := hex.DecodeString(headerSignature)
  if err != nil {
    panic(err)
  }

  hasher := hmac.New(sha256.New, SECRET)
  hasher.Write(body)
  expected := hasher.Sum(nil)

  if !hmac.Equal(expected, signature) {
    fmt.Printf("invalid signature")
    w.WriteHeader(401)
    return
  }

  // Message validated...
})