# Webhook Authentication

Gensail uses HMAC signatures to secure webhook deliveries. This allows you to verify that webhook payloads genuinely originate from Gensail and haven't been tampered with.

## Overview

When webhook authentication is enabled, every webhook request includes an `X-Signature` header containing a Stripe-style HMAC signature. Your server should verify this signature before processing the webhook payload.

## Signature Format

The `X-Signature` header uses this format:

```
X-Signature: t=1734789600,v1=a3b2c1d4e5f6789abc123def456...
```

| Component | Description |
|  --- | --- |
| `t` | Unix timestamp (seconds) when the signature was generated |
| `v1` | HMAC-SHA256 signature in hexadecimal format |


## How Signatures Are Computed

Gensail computes the signature using:

1. **Signed Payload**: `{timestamp}.{json_body}`
2. **Algorithm**: HMAC-SHA256
3. **Key**: Your webhook secret (provided by Gensail)


```
signature = HMAC-SHA256(
    key = webhook_secret,
    message = "{timestamp}.{json_body}"
)
```

## Verification Steps

To verify a webhook signature:

1. **Extract** the timestamp (`t`) and signature (`v1`) from the `X-Signature` header
2. **Reconstruct** the signed payload: `{timestamp}.{raw_request_body}`
3. **Compute** your own HMAC-SHA256 using your webhook secret
4. **Compare** your computed signature with the received `v1` value
5. **Check** that the timestamp is within tolerance (recommended: 5 minutes)


## Code Examples

### Python

```python
import hmac
import hashlib
import time

def verify_webhook(payload_bytes: bytes, signature_header: str, secret: str) -> bool:
    """
    Verify Gensail webhook signature.

    Args:
        payload_bytes: Raw request body (bytes)
        signature_header: X-Signature header value
        secret: Your webhook secret

    Returns:
        True if signature is valid, False otherwise
    """
    try:
        # Parse header: "t=123,v1=abc..."
        parts = dict(p.split("=", 1) for p in signature_header.split(","))
        timestamp = int(parts["t"])
        received_sig = parts["v1"]
    except (ValueError, KeyError):
        return False  # Invalid header format

    # Check timestamp (5 minute tolerance)
    if abs(time.time() - timestamp) > 300:
        return False  # Signature too old (replay protection)

    # Compute expected signature
    signed_payload = f"{timestamp}.{payload_bytes.decode('utf-8')}"
    expected_sig = hmac.new(
        secret.encode(),
        signed_payload.encode(),
        hashlib.sha256
    ).hexdigest()

    # Constant-time comparison (prevents timing attacks)
    return hmac.compare_digest(received_sig, expected_sig)


# Flask example
from flask import Flask, request, jsonify

app = Flask(__name__)
WEBHOOK_SECRET = "your_webhook_secret_here"

@app.route('/webhook', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-Signature', '')
    payload = request.get_data()

    if not verify_webhook(payload, signature, WEBHOOK_SECRET):
        return jsonify({'error': 'Invalid signature'}), 401

    # Process the verified webhook
    data = request.get_json()
    # ... your processing logic ...

    return jsonify({'status': 'received'}), 200
```

### Node.js

```javascript
const crypto = require('crypto');

function verifyWebhook(payloadString, signatureHeader, secret) {
    try {
        // Parse header: "t=123,v1=abc..."
        const parts = Object.fromEntries(
            signatureHeader.split(',').map(p => p.split('='))
        );
        const timestamp = parseInt(parts.t);
        const receivedSig = parts.v1;

        // Check timestamp (5 minute tolerance)
        const now = Math.floor(Date.now() / 1000);
        if (Math.abs(now - timestamp) > 300) {
            return false; // Signature too old
        }

        // Compute expected signature
        const signedPayload = `${timestamp}.${payloadString}`;
        const expectedSig = crypto
            .createHmac('sha256', secret)
            .update(signedPayload)
            .digest('hex');

        // Constant-time comparison
        return crypto.timingSafeEqual(
            Buffer.from(receivedSig),
            Buffer.from(expectedSig)
        );
    } catch (e) {
        return false;
    }
}

// Express example
const express = require('express');
const app = express();

const WEBHOOK_SECRET = 'your_webhook_secret_here';

app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
    const signature = req.headers['x-signature'] || '';
    const payload = req.body.toString();

    if (!verifyWebhook(payload, signature, WEBHOOK_SECRET)) {
        return res.status(401).json({ error: 'Invalid signature' });
    }

    // Process the verified webhook
    const data = JSON.parse(payload);
    // ... your processing logic ...

    res.json({ status: 'received' });
});
```

### Ruby

```ruby
require 'openssl'
require 'json'

def verify_webhook(payload, signature_header, secret)
  begin
    # Parse header: "t=123,v1=abc..."
    parts = signature_header.split(',').map { |p| p.split('=', 2) }.to_h
    timestamp = parts['t'].to_i
    received_sig = parts['v1']

    # Check timestamp (5 minute tolerance)
    return false if (Time.now.to_i - timestamp).abs > 300

    # Compute expected signature
    signed_payload = "#{timestamp}.#{payload}"
    expected_sig = OpenSSL::HMAC.hexdigest('sha256', secret, signed_payload)

    # Constant-time comparison
    ActiveSupport::SecurityUtils.secure_compare(received_sig, expected_sig)
  rescue
    false
  end
end

# Rails controller example
class WebhooksController < ApplicationController
  skip_before_action :verify_authenticity_token

  WEBHOOK_SECRET = ENV['WEBHOOK_SECRET']

  def receive
    signature = request.headers['X-Signature']
    payload = request.raw_post

    unless verify_webhook(payload, signature, WEBHOOK_SECRET)
      render json: { error: 'Invalid signature' }, status: :unauthorized
      return
    end

    # Process the verified webhook
    data = JSON.parse(payload)
    # ... your processing logic ...

    render json: { status: 'received' }
  end
end
```

### PHP

```php
<?php

function verifyWebhook(string $payload, string $signatureHeader, string $secret): bool {
    // Parse header: "t=123,v1=abc..."
    $parts = [];
    foreach (explode(',', $signatureHeader) as $part) {
        [$key, $value] = explode('=', $part, 2);
        $parts[$key] = $value;
    }

    $timestamp = (int)($parts['t'] ?? 0);
    $receivedSig = $parts['v1'] ?? '';

    // Check timestamp (5 minute tolerance)
    if (abs(time() - $timestamp) > 300) {
        return false;
    }

    // Compute expected signature
    $signedPayload = "{$timestamp}.{$payload}";
    $expectedSig = hash_hmac('sha256', $signedPayload, $secret);

    // Constant-time comparison
    return hash_equals($receivedSig, $expectedSig);
}

// Usage example
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
$secret = getenv('WEBHOOK_SECRET');

if (!verifyWebhook($payload, $signature, $secret)) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}

// Process the verified webhook
$data = json_decode($payload, true);
// ... your processing logic ...

echo json_encode(['status' => 'received']);
```

## Security Best Practices

### 1. Always Verify Signatures

Never process webhook payloads without verifying the signature first. This protects against:

- **Spoofing**: Attackers sending fake webhooks
- **Tampering**: Modified payloads in transit
- **Replay attacks**: Re-sending old webhooks (use timestamp validation)


### 2. Use Constant-Time Comparison

Always use constant-time string comparison functions to prevent timing attacks:

- Python: `hmac.compare_digest()`
- Node.js: `crypto.timingSafeEqual()`
- Ruby: `ActiveSupport::SecurityUtils.secure_compare()`
- PHP: `hash_equals()`


### 3. Validate Timestamps

Reject signatures older than 5 minutes to prevent replay attacks. This is especially important if an attacker captures a valid webhook.

### 4. Store Secrets Securely

- Use environment variables, not hardcoded values
- Never commit secrets to version control
- Rotate secrets periodically


### 5. Use HTTPS

Always use HTTPS for your webhook endpoint to encrypt data in transit.

## Troubleshooting

### "Signature mismatch" Error

1. **Check the secret**: Ensure you're using the correct webhook secret
2. **Raw body**: Use the raw request body, not parsed JSON
3. **Encoding**: Ensure UTF-8 encoding for both payload and secret
4. **No modifications**: Don't modify the payload before verification


### "Signature expired" Error

1. **Server time**: Ensure your server's clock is synchronized (use NTP)
2. **Tolerance**: Consider increasing tolerance if network latency is high
3. **Timestamp format**: The `t` value is Unix seconds, not milliseconds


### No X-Signature Header

1. **Configuration**: Verify webhook authentication is enabled for your workspace
2. **Contact support**: Ensure your webhook secret is properly configured


## Getting Your Webhook Secret

Your webhook secret is provided when your workspace is configured. Contact your Gensail administrator or check your workspace settings in the Gensail platform.

If you need to rotate your webhook secret, contact [support@gensail.com](mailto:support@gensail.com).

## Testing

To test your webhook verification locally:

```bash
# Generate a test signature
SECRET="your_webhook_secret"
TIMESTAMP=$(date +%s)
PAYLOAD='{"test": "data"}'
SIGNATURE=$(echo -n "${TIMESTAMP}.${PAYLOAD}" | openssl dgst -sha256 -hmac "$SECRET" | cut -d' ' -f2)

# Send test webhook
curl -X POST http://localhost:3000/webhook \
  -H "Content-Type: application/json" \
  -H "X-Signature: t=${TIMESTAMP},v1=${SIGNATURE}" \
  -d "${PAYLOAD}"
```

## Support

For questions about webhook authentication, contact [support@gensail.com](mailto:support@gensail.com).