Skip to main content

Webhook Security

Secure your webhook endpoint to prevent unauthorized access and ensure data integrity.

Signature Verification

Every webhook request includes a signature header that you should verify:

Headers

HeaderDescription
X-FYATU-SignatureHMAC-SHA256 signature of the payload
X-FYATU-TimestampUnix timestamp when webhook was sent

Verification Process

  1. Get your encryption key from the dashboard
  2. Compute HMAC-SHA256 of the raw request body
  3. Compare with the provided signature

Code Examples

const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, encryptionKey) {
  const expectedSignature = crypto
    .createHmac('sha256', encryptionKey)
    .update(JSON.stringify(payload))
    .digest('hex');

  // Use timing-safe comparison to prevent timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expectedSignature, 'hex')
  );
}

// Express middleware
app.post('/webhooks/fyatu', express.json(), (req, res) => {
  const signature = req.headers['x-fyatu-signature'];
  const encryptionKey = process.env.FYATU_ENCRYPTION_KEY;

  if (!signature) {
    return res.status(401).json({ error: 'Missing signature' });
  }

  if (!verifyWebhookSignature(req.body, signature, encryptionKey)) {
    console.error('Invalid webhook signature');
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Signature valid - process the webhook
  console.log('Verified webhook:', req.body.event);
  res.status(200).send('OK');

  processWebhookAsync(req.body);
});

Timestamp Validation

Prevent replay attacks by checking the timestamp:
const MAX_AGE_SECONDS = 300; // 5 minutes

function isTimestampValid(timestamp) {
  const now = Math.floor(Date.now() / 1000);
  const webhookTime = parseInt(timestamp, 10);

  return Math.abs(now - webhookTime) < MAX_AGE_SECONDS;
}

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

  if (!isTimestampValid(timestamp)) {
    return res.status(401).json({ error: 'Webhook too old' });
  }

  // Continue with signature verification...
});

IP Whitelisting

For additional security, whitelist FYATU’s IP addresses:
const FYATU_IPS = [
  '203.0.113.10',
  '203.0.113.11',
  // Get current IPs from dashboard
];

function isAllowedIP(clientIP) {
  return FYATU_IPS.includes(clientIP);
}

app.post('/webhooks/fyatu', (req, res) => {
  const clientIP = req.ip || req.connection.remoteAddress;

  if (!isAllowedIP(clientIP)) {
    console.warn('Webhook from unknown IP:', clientIP);
    return res.status(403).json({ error: 'IP not allowed' });
  }

  // Continue processing...
});
Contact support to get the current list of FYATU webhook IPs.

HTTPS Requirement

Always use HTTPS for your webhook endpoint:
  • Encrypts data in transit
  • Prevents man-in-the-middle attacks
  • Required for production
Nginx Configuration
server {
    listen 443 ssl;
    server_name api.yourcompany.com;

    ssl_certificate /etc/ssl/certs/your-cert.pem;
    ssl_certificate_key /etc/ssl/private/your-key.pem;

    location /webhooks/fyatu {
        proxy_pass http://localhost:3000;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

Error Handling

Handle verification failures gracefully:
app.post('/webhooks/fyatu', (req, res) => {
  try {
    // Verify signature
    if (!verifySignature(req)) {
      // Log for investigation
      console.error('Signature verification failed', {
        ip: req.ip,
        headers: req.headers,
        body: req.body
      });

      // Don't reveal details to potential attacker
      return res.status(401).send('Unauthorized');
    }

    // Process webhook
    processWebhook(req.body);
    res.status(200).send('OK');

  } catch (error) {
    console.error('Webhook processing error:', error);

    // Return 500 so FYATU retries
    res.status(500).send('Internal error');
  }
});

Security Checklist

Verify webhook signatures using HMAC-SHA256
Use timing-safe comparison for signatures
Validate timestamp to prevent replay attacks
Use HTTPS for all webhook endpoints
Consider IP whitelisting for additional security
Store encryption key securely (environment variable)
Log failed verification attempts for monitoring
Don’t expose detailed error messages to callers

Rotating Encryption Keys

If you need to rotate your encryption key:
  1. Generate a new key in the dashboard
  2. Update your webhook handler to accept both old and new signatures
  3. Once all webhooks use the new key, remove the old key
function verifyWithRotation(payload, signature, keys) {
  // Try current key first, then old key
  for (const key of keys) {
    if (verifyWebhookSignature(payload, signature, key)) {
      return true;
    }
  }
  return false;
}

const ENCRYPTION_KEYS = [
  process.env.FYATU_ENCRYPTION_KEY,        // Current
  process.env.FYATU_ENCRYPTION_KEY_OLD,    // Previous (during rotation)
].filter(Boolean);