> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gu1.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Segurança de Webhook

> Proteja seus endpoints de webhook com verificação de assinatura e melhores práticas — com eventos webhook gu1 para integração downstream em tempo real.

## Visão Geral

Proteger seus endpoints de webhook é crítico para garantir que as solicitações de webhook venham do Gu1 e não de atores maliciosos. Este guia cobre como verificar assinaturas de webhook, implementar melhores práticas de segurança e evitar erros de segurança comuns.

## Verificação de Assinatura

Gu1 assina todas as solicitações de webhook com uma assinatura HMAC SHA-256 usando seu secret de webhook. A assinatura é enviada no header `X-Webhook-Signature`, permitindo que você verifique se a solicitação é autêntica.

### Como Funciona a Verificação de Assinatura

1. **Gu1 gera uma assinatura**: Ao enviar um webhook, Gu1 cria um hash HMAC SHA-256 do corpo raw da solicitação usando seu secret de webhook
2. **Assinatura é enviada no header**: A assinatura é incluída no header `X-Webhook-Signature`
3. **Seu servidor recalcula**: Seu endpoint recalcula a assinatura usando o mesmo secret e corpo raw
4. **Comparar assinaturas**: Se as assinaturas corresponderem, o webhook é autêntico

<Warning>
  Sempre verifique assinaturas de webhook em produção. Sem verificação, qualquer pessoa pode enviar webhooks falsos para seu endpoint e potencialmente comprometer seu sistema.
</Warning>

## Exemplos de Verificação de Assinatura

### Node.js (Express)

```javascript theme={null}
const express = require('express');
const crypto = require('crypto');

const app = express();

// IMPORTANTE: Armazenar corpo raw para verificação de assinatura
app.use(express.json({
  verify: (req, res, buf) => {
    req.rawBody = buf.toString('utf8');
  }
}));

app.post('/webhooks/gu1', async (req, res) => {
  try {
    // Obter assinatura do header
    const signature = req.headers['x-webhook-signature'];
    const webhookSecret = process.env.GU1_WEBHOOK_SECRET;

    // Verificar assinatura
    if (!verifySignature(req.rawBody, signature, webhookSecret)) {
      console.error('Invalid webhook signature');
      return res.status(401).json({ error: 'Invalid signature' });
    }

    // Processar webhook
    const { event, timestamp, payload } = req.body;
    await handleWebhook(event, payload);

    // Retornar 200 para confirmar recebimento
    res.status(200).json({ success: true });
  } catch (error) {
    console.error('Webhook error:', error);
    res.status(500).json({ error: error.message });
  }
});

/**
 * Verificar assinatura HMAC SHA-256
 */
function verifySignature(rawBody, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  return signature === expectedSignature;
}

app.listen(3000);
```

<Note>
  **Crítico**: Você deve verificar a assinatura usando o **corpo raw da solicitação** antes de ser analisado como JSON. Se você verificar contra o corpo JSON analisado (por exemplo, `JSON.stringify(req.body)`), a assinatura não corresponderá porque a formatação JSON pode diferir.
</Note>

### Python (Flask)

```python theme={null}
from flask import Flask, request, jsonify
import hmac
import hashlib
import os
import logging

app = Flask(__name__)

@app.route('/webhooks/gu1', methods=['POST'])
def gu1_webhook():
    try:
        # Obter assinatura do header
        signature = request.headers.get('X-Webhook-Signature')
        webhook_secret = os.getenv('GU1_WEBHOOK_SECRET')

        # Obter corpo raw para verificação de assinatura
        raw_body = request.get_data(as_text=True)

        # Verificar assinatura
        if not verify_signature(raw_body, signature, webhook_secret):
            logging.error('Invalid webhook signature')
            return jsonify({'error': 'Invalid signature'}), 401

        # Analisar payload
        payload = request.json
        event = payload.get('event')
        data = payload.get('payload')

        logging.info(f'Received webhook: {event}')

        # Processar o webhook
        handle_webhook(event, data)

        return jsonify({
            'success': True,
            'message': 'Webhook received'
        }), 200

    except Exception as e:
        logging.error(f'Webhook error: {e}')
        return jsonify({
            'success': False,
            'error': str(e)
        }), 500

def verify_signature(raw_body: str, signature: str, secret: str) -> bool:
    """Verificar assinatura HMAC SHA-256"""
    expected_signature = hmac.new(
        secret.encode('utf-8'),
        raw_body.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(signature, expected_signature)

def handle_webhook(event: str, data: dict):
    # Processar webhook baseado no tipo de evento
    pass

if __name__ == '__main__':
    app.run(port=3000)
```

<Tip>
  Use `hmac.compare_digest()` em vez de `==` para comparar assinaturas em Python. Esta função realiza uma comparação segura contra timing que previne ataques de timing.
</Tip>

### Go (Gin)

```go theme={null}
package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "io/ioutil"
    "log"
    "net/http"
    "os"

    "github.com/gin-gonic/gin"
)

type WebhookPayload struct {
    Event     string                 `json:"event"`
    Timestamp string                 `json:"timestamp"`
    Payload   map[string]interface{} `json:"payload"`
}

func main() {
    r := gin.Default()
    r.POST("/webhooks/gu1", handleWebhook)
    r.Run(":3000")
}

func handleWebhook(c *gin.Context) {
    // Ler corpo raw para verificação de assinatura
    rawBody, err := ioutil.ReadAll(c.Request.Body)
    if err != nil {
        c.JSON(500, gin.H{"error": "Failed to read body"})
        return
    }

    // Verificar assinatura
    signature := c.GetHeader("X-Webhook-Signature")
    webhookSecret := os.Getenv("GU1_WEBHOOK_SECRET")

    if !verifySignature(rawBody, signature, webhookSecret) {
        log.Println("Invalid webhook signature")
        c.JSON(401, gin.H{"error": "Invalid signature"})
        return
    }

    // Analisar payload
    var payload WebhookPayload
    if err := json.Unmarshal(rawBody, &payload); err != nil {
        c.JSON(400, gin.H{"error": "Invalid JSON"})
        return
    }

    log.Printf("Received webhook: %s", payload.Event)

    // Processar webhook
    handleWebhookEvent(payload)

    c.JSON(200, gin.H{
        "success": true,
        "message": "Webhook received",
    })
}

func verifySignature(rawBody []byte, signature, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(rawBody)
    expectedSignature := hex.EncodeToString(mac.Sum(nil))

    return hmac.Equal([]byte(signature), []byte(expectedSignature))
}

func handleWebhookEvent(payload WebhookPayload) {
    // Processar webhook baseado no tipo de evento
}
```

## Corpo Raw vs JSON Analisado

Um erro comum é verificar a assinatura usando o objeto JSON analisado em vez do corpo raw da solicitação. Isso sempre falhará porque a formatação JSON pode diferir.

<CodeGroup>
  ```javascript Errado - Falhará theme={null}
  // ERRADO: Verificando corpo analisado
  app.post('/webhooks', express.json(), (req, res) => {
    const signature = req.headers['x-webhook-signature'];

    // Isso NÃO funcionará - JSON.stringify pode formatar diferentemente
    const body = JSON.stringify(req.body);
    const expectedSignature = crypto
      .createHmac('sha256', secret)
      .update(body)
      .digest('hex');

    if (signature === expectedSignature) {
      // Sempre falhará
    }
  });
  ```

  ```javascript Correto - Funcionará theme={null}
  // CORRETO: Verificando corpo raw
  app.use(express.json({
    verify: (req, res, buf) => {
      req.rawBody = buf.toString('utf8');
    }
  }));

  app.post('/webhooks', (req, res) => {
    const signature = req.headers['x-webhook-signature'];

    // Isso funciona - usando corpo raw
    const expectedSignature = crypto
      .createHmac('sha256', secret)
      .update(req.rawBody)
      .digest('hex');

    if (signature === expectedSignature) {
      // Assinatura verificada com sucesso
    }
  });
  ```
</CodeGroup>

### O que a Gu1 assina (e o que não assina)

A Gu1 calcula `HMAC-SHA256(secret, raw_request_body)` e envia o digest hexadecimal em `X-Webhook-Signature`.

| Entra na assinatura                                   | Não entra                                                                         |
| ----------------------------------------------------- | --------------------------------------------------------------------------------- |
| Bytes exatos do body do POST HTTP (string JSON UTF-8) | `X-Webhook-Timestamp`, `X-Webhook-ID`, `X-Webhook-Event` ou qualquer outro header |
|                                                       | Query params ou path da URL                                                       |
|                                                       | Campos que seu middleware adiciona ou remove depois de ler o body                 |

<Note>
  Use comparação **timing-safe** ao validar a assinatura (por exemplo `crypto.timingSafeEqual` no Node.js). Veja a seção de boas práticas abaixo.
</Note>

### Histórico de webhooks no dashboard

O **monitor de webhooks** exibe o payload para debug e suporte. Essa visualização **não** é a string byte a byte que foi assinada no envio. Pretty-print, copiar da UI ou `JSON.stringify()` em um objeto parseado pode gerar uma string diferente e fazer uma entrega correta parecer inválida.

Para depurar uma falha, compare o header `X-Webhook-Signature` recebido pelo seu endpoint com o HMAC calculado sobre o **raw body daquele mesmo request HTTP**. Não re-verifique apenas com o JSON do dashboard.

### Falhas intermitentes de assinatura

Se a verificação funciona em alguns eventos mas em outros retorna `401 Invalid signature` **com o mesmo secret e endpoint**, as causas mais comuns são:

1. **JSON parseado e re-serializado** — `JSON.stringify(req.body)` após `express.json()` (ou equivalente) não reproduz de forma confiável os bytes do body da Gu1. Payloads diferentes (ordem de chaves, forma aninhada, formatação numérica) podem falhar só às vezes.
2. **Middleware que altera o body** — deduplicar arrays, remover campos `null`, ordenar chaves ou normalizar strings antes de verificar muda o input assinado.
3. **Secret incorreto** — secret regenerado no dashboard enquanto o valor antigo permanece no seu ambiente.
4. **Header de assinatura ausente** — se o webhook não tiver secret configurado, a Gu1 pode omitir `X-Webhook-Signature`; tratar header ausente como assinatura inválida é esperado.

<Warning>
  A verificação de assinatura é **opcional**, mas recomendada. A Gu1 ainda entrega webhooks se você não verificar; o `401` é retornado pelo **seu** servidor quando sua lógica de verificação rejeita o request.
</Warning>

## Padrões de Idempotência

Webhooks podem ser entregues mais de uma vez devido a problemas de rede, timeouts ou tentativas. Implemente idempotência para garantir que você processe cada webhook apenas uma vez.

### Idempotência Baseada em Banco de Dados

Armazene IDs de webhook processados no seu banco de dados:

```javascript theme={null}
async function handleWebhook(webhook) {
  const webhookId = `${webhook.payload.entityId}_${webhook.event}_${webhook.timestamp}`;

  // Verificar se já foi processado
  const alreadyProcessed = await db.webhookLog.findUnique({
    where: { webhookId }
  });

  if (alreadyProcessed) {
    console.log('Webhook already processed, skipping');
    return; // Pular duplicado
  }

  // Processar webhook
  await processWebhookEvent(webhook);

  // Marcar como processado
  await db.webhookLog.create({
    data: {
      webhookId,
      event: webhook.event,
      processedAt: new Date(),
      payload: webhook
    }
  });
}
```

### Idempotência Baseada em Cache

Para webhooks de alto volume, use um cache como Redis:

```javascript theme={null}
const Redis = require('ioredis');
const redis = new Redis();

async function handleWebhook(webhook) {
  const webhookId = `${webhook.payload.entityId}_${webhook.event}_${webhook.timestamp}`;

  // Tentar definir a chave com NX (apenas se não existir)
  const result = await redis.set(
    `webhook:${webhookId}`,
    '1',
    'EX', 86400, // Expira após 24 horas
    'NX'
  );

  if (!result) {
    console.log('Webhook already processed, skipping');
    return; // Já processado
  }

  // Processar webhook
  await processWebhookEvent(webhook);
}
```

```python theme={null}
import redis
import json

redis_client = redis.Redis(host='localhost', port=6379, db=0)

def handle_webhook(webhook):
    webhook_id = f"{webhook['payload']['entityId']}_{webhook['event']}_{webhook['timestamp']}"

    # Tentar definir a chave com NX (apenas se não existir)
    result = redis_client.set(
        f"webhook:{webhook_id}",
        "1",
        ex=86400,  # Expira após 24 horas
        nx=True
    )

    if not result:
        print('Webhook already processed, skipping')
        return  # Já processado

    # Processar webhook
    process_webhook_event(webhook)
```

## Melhores Práticas de Segurança

<AccordionGroup>
  <Accordion title="1. Sempre Verificar Assinaturas">
    Nunca pule a verificação de assinatura em ambientes de produção. Esta é sua defesa principal contra webhooks falsos.

    ```javascript theme={null}
    // Sempre verificar a assinatura
    if (!verifySignature(req.rawBody, signature, secret)) {
      return res.status(401).json({ error: 'Invalid signature' });
    }
    ```
  </Accordion>

  <Accordion title="2. Usar Apenas HTTPS">
    Configure seus endpoints de webhook para usar apenas HTTPS. Rejeite solicitações HTTP:

    ```javascript theme={null}
    app.post('/webhooks', (req, res) => {
      // Verificar se a solicitação é HTTPS
      if (req.protocol !== 'https') {
        return res.status(403).json({ error: 'HTTPS required' });
      }

      // Processar webhook...
    });
    ```
  </Accordion>

  <Accordion title="3. Validar Tipos de Eventos">
    Processe apenas tipos de eventos que você está esperando:

    ```javascript theme={null}
    const ALLOWED_EVENTS = [
      'entity.created',
      'entity.updated',
      'entity.status_changed',
      'kyc.validation_approved'
    ];

    if (!ALLOWED_EVENTS.includes(webhook.event)) {
      console.warn('Unknown event type:', webhook.event);
      return res.status(400).json({ error: 'Unknown event type' });
    }
    ```
  </Accordion>

  <Accordion title="4. Implementar Limitação de Taxa">
    Proteja seu endpoint contra abuso com limitação de taxa:

    ```javascript theme={null}
    const rateLimit = require('express-rate-limit');

    const webhookLimiter = rateLimit({
      windowMs: 1 * 60 * 1000, // 1 minuto
      max: 100, // Limitar cada IP a 100 solicitações por minuto
      message: 'Too many webhook requests'
    });

    app.post('/webhooks', webhookLimiter, handleWebhook);
    ```
  </Accordion>

  <Accordion title="5. Armazenar Secrets de Webhook com Segurança">
    Nunca codifique secrets de webhook. Use variáveis de ambiente ou gerenciamento de secrets:

    ```javascript theme={null}
    // Bom - variável de ambiente
    const secret = process.env.GU1_WEBHOOK_SECRET;

    // Ruim - codificado
    const secret = 'abc123'; // Nunca faça isso!
    ```

    Para produção, use um gerenciador de secrets:

    * AWS Secrets Manager
    * HashiCorp Vault
    * Azure Key Vault
    * Google Secret Manager
  </Accordion>

  <Accordion title="6. Validar Estrutura do Payload">
    Valide a estrutura do payload do webhook antes de processar:

    ```javascript theme={null}
    function isValidEntityPayload(payload) {
      return (
        payload &&
        payload.entity &&
        payload.entity.id &&
        payload.entity.type
      );
    }

    if (!isValidEntityPayload(webhook.payload)) {
      return res.status(400).json({ error: 'Invalid payload structure' });
    }
    ```
  </Accordion>

  <Accordion title="7. Usar Comparações Seguras Contra Timing">
    Ao comparar assinaturas, use funções de comparação seguras contra timing para prevenir ataques de timing:

    ```javascript theme={null}
    const crypto = require('crypto');

    function verifySignature(rawBody, signature, secret) {
      const expectedSignature = crypto
        .createHmac('sha256', secret)
        .update(rawBody)
        .digest('hex');

      // Comparação segura contra timing
      return crypto.timingSafeEqual(
        Buffer.from(signature),
        Buffer.from(expectedSignature)
      );
    }
    ```
  </Accordion>

  <Accordion title="8. Implementar Logs de Webhook">
    Registre todas as tentativas de webhook para auditoria e depuração:

    ```javascript theme={null}
    async function logWebhook(webhook, success, error = null) {
      await db.webhookLog.create({
        data: {
          event: webhook.event,
          timestamp: webhook.timestamp,
          success,
          error: error?.message,
          payload: webhook,
          processedAt: new Date()
        }
      });
    }
    ```
  </Accordion>

  <Accordion title="9. Retornar 200 Rapidamente">
    Responda com código de status 200 rapidamente para prevenir tentativas. Processe trabalho pesado assincronamente:

    ```javascript theme={null}
    app.post('/webhooks', async (req, res) => {
      // Verificar assinatura primeiro
      if (!verifySignature(req.rawBody, signature, secret)) {
        return res.status(401).json({ error: 'Invalid signature' });
      }

      // Confirmar imediatamente
      res.status(200).json({ received: true });

      // Processar assincronamente
      processWebhook(req.body).catch(console.error);
    });
    ```
  </Accordion>

  <Accordion title="10. Implementar Lógica de Tentativa">
    Se o processamento de webhook falhar, armazene para tentativa:

    ```javascript theme={null}
    try {
      await processWebhook(webhook);
    } catch (error) {
      // Armazenar webhook falhado para tentativa
      await db.failedWebhook.create({
        data: {
          webhook,
          error: error.message,
          retryCount: 0,
          nextRetryAt: new Date(Date.now() + 5 * 60 * 1000) // Tentar em 5 minutos
        }
      });

      // Ainda retornar 200 para prevenir tentativas do Gu1
      res.status(200).json({ success: false });
    }
    ```
  </Accordion>
</AccordionGroup>

## Erros Comuns de Segurança a Evitar

<Warning>
  Evite estes erros comuns de segurança que podem comprometer seus endpoints de webhook:
</Warning>

### 1. Pular Verificação de Assinatura

```javascript theme={null}
// NÃO FAÇA ISSO - Sem verificação de assinatura
app.post('/webhooks', (req, res) => {
  // Processando webhook sem verificação
  handleWebhook(req.body);
  res.status(200).send('OK');
});
```

**Risco**: Qualquer pessoa pode enviar webhooks falsos para seu endpoint.

### 2. Verificar JSON Analisado Em Vez de Corpo Raw

```javascript theme={null}
// NÃO FAÇA ISSO - Verificando corpo analisado
app.post('/webhooks', express.json(), (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  const body = JSON.stringify(req.body); // Errado!

  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(body)
    .digest('hex');

  // Sempre falhará
});
```

**Risco**: A verificação de assinatura sempre falhará.

### 3. Usar HTTP Em Vez de HTTPS

```javascript theme={null}
// NÃO FAÇA ISSO - Aceitando HTTP
http://seuapp.com/webhooks
```

**Risco**: Payloads de webhook podem ser interceptados em trânsito.

### 4. Codificar Secrets

```javascript theme={null}
// NÃO FAÇA ISSO - Secret codificado
const secret = 'my-webhook-secret-123';
```

**Risco**: Secrets expostos no controle de versão ou logs.

### 5. Não Implementar Idempotência

```javascript theme={null}
// NÃO FAÇA ISSO - Processando duplicados
app.post('/webhooks', (req, res) => {
  // Sem verificação de duplicados
  await createUser(req.body.entity);
  res.status(200).send('OK');
});
```

**Risco**: Webhooks duplicados criarão registros duplicados.

### 6. Expor Erros aos Clientes

```javascript theme={null}
// NÃO FAÇA ISSO - Expondo erros internos
app.post('/webhooks', async (req, res) => {
  try {
    await processWebhook(req.body);
  } catch (error) {
    res.status(500).json({
      error: error.stack, // Expondo stack trace
      query: error.query  // Expondo query de banco de dados
    });
  }
});
```

**Risco**: Vazamento de informações internas para atacantes.

### 7. Não Validar Tipos de Eventos

```javascript theme={null}
// NÃO FAÇA ISSO - Processando eventos desconhecidos
app.post('/webhooks', (req, res) => {
  // Aceitando qualquer tipo de evento
  handleAnyEvent(req.body.event, req.body.payload);
});
```

**Risco**: Atacantes podem enviar tipos de eventos arbitrários.

### 8. Usar Secrets Fracos

```javascript theme={null}
// NÃO FAÇA ISSO - Secret fraco
const secret = '123456';
```

**Risco**: Secrets podem ser quebrados por força bruta.

**Solução**: Use secrets fortes e gerados aleatoriamente (pelo menos 32 caracteres).

## Testando Segurança de Webhook

### Testar Assinaturas Inválidas

```bash theme={null}
# Testar com assinatura inválida
curl -X POST https://seuapp.com/webhooks \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Signature: invalid-signature" \
  -d '{"event":"entity.created","payload":{}}'

# Deve retornar 401 Unauthorized
```

### Testar Ataques de Replay

Envie o mesmo webhook duas vezes e verifique idempotência:

```bash theme={null}
# Enviar webhook uma vez
curl -X POST https://seuapp.com/webhooks \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Signature: valid-signature" \
  -d '{"event":"entity.created","timestamp":"2025-01-07T10:00:00Z","payload":{"entity":{"id":"123"}}}'

# Enviar mesmo webhook novamente
curl -X POST https://seuapp.com/webhooks \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Signature: valid-signature" \
  -d '{"event":"entity.created","timestamp":"2025-01-07T10:00:00Z","payload":{"entity":{"id":"123"}}}'

# Segunda solicitação deve ser ignorada (idempotência)
```

## Monitoramento e Alertas

Configure monitoramento para segurança de webhook:

```javascript theme={null}
// Rastrear métricas de webhook
const metrics = {
  totalReceived: 0,
  signatureVerified: 0,
  signatureFailed: 0,
  processed: 0,
  failed: 0
};

app.post('/webhooks', (req, res) => {
  metrics.totalReceived++;

  const signature = req.headers['x-webhook-signature'];
  if (!verifySignature(req.rawBody, signature, secret)) {
    metrics.signatureFailed++;

    // Alertar se muitas falhas de assinatura
    if (metrics.signatureFailed > 10) {
      alertSecurityTeam('High webhook signature failure rate');
    }

    return res.status(401).json({ error: 'Invalid signature' });
  }

  metrics.signatureVerified++;
  // Processar webhook...
});
```

## Próximos Passos

<CardGroup cols={2}>
  <Card title="Configuração de Webhook" icon="gear" href="/pt/webhooks/configuration">
    Aprenda como configurar webhooks
  </Card>

  <Card title="Eventos de Entidades" icon="user" href="/pt/webhooks/events/entity-events">
    Lidar com eventos de ciclo de vida de entidades
  </Card>

  <Card title="Eventos KYC" icon="id-card" href="/pt/webhooks/events/kyc-events">
    Processar eventos de verificação KYC
  </Card>

  <Card title="Eventos de Regras" icon="gavel" href="/pt/webhooks/events/rule-events">
    Responder a regras de conformidade
  </Card>
</CardGroup>
