Security 9 min read

Webhook Authentication Methods: HMAC, OAuth, and API Keys

Your webhook endpoint is a public URL. Without proper authentication, anyone can send fake events. Learn how to secure it with HMAC signatures, OAuth, and API keys.

H

HookWatch Team

February 25, 2026

Every webhook endpoint is a publicly accessible URL. If someone discovers it, they can send fake events to your server. Without authentication, your application has no way to distinguish a legitimate webhook from a forged one.

This isn't a theoretical risk. Webhook URL patterns are predictable (/webhooks/stripe, /api/hooks/github), and attackers actively scan for them.

The Three Approaches

1. HMAC Signatures (Recommended)

HMAC (Hash-based Message Authentication Code) is the gold standard for webhook authentication. The sender signs each request using a shared secret, and your server verifies the signature before processing.

How it works:

Code
Provider                          Your Server
   │                                  │
   │  POST /webhooks/stripe           │
   │  X-Signature: sha256=abc123...   │
   │  Body: {"event": "payment"}      │
   │ ─────────────────────────────►   │
   │                                  │
   │                   Compute HMAC of body
   │                   using shared secret
   │                   Compare with header
   │                                  │
   │              200 OK              │
   │  ◄─────────────────────────────  │

Implementation:

Javascript
const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload, 'utf8')
    .digest('hex');

  const expectedBuffer = Buffer.from(expected, 'hex');
  const signatureBuffer = Buffer.from(signature, 'hex');

  if (expectedBuffer.length !== signatureBuffer.length) {
    return false;
  }

  return crypto.timingSafeEqual(expectedBuffer, signatureBuffer);
}

// Express middleware
app.post('/webhooks/stripe', (req, res) => {
  const signature = req.headers['x-signature']
    ?.replace('sha256=', '');

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

  const rawBody = req.rawBody; // Need raw body, not parsed
  if (!verifyWebhookSignature(rawBody, signature, WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Signature valid — process the event
  handleEvent(req.body);
  res.status(200).json({ received: true });
});

Critical details:

  • Use the raw body — verifying against the parsed/re-serialized body will fail because JSON serialization isn't deterministic
  • Use constant-time comparisoncrypto.timingSafeEqual prevents timing attacks
  • Store secrets securely — environment variables, not source code

Who uses HMAC:

  • Stripe (Stripe-Signature header)
  • GitHub (X-Hub-Signature-256 header)
  • Shopify (X-Shopify-Hmac-SHA256 header)
  • HookWatch (X-HookWatch-Signature header)

2. OAuth 2.0 Bearer Tokens

Some webhook providers authenticate using OAuth tokens. Instead of signing the payload, the sender includes a bearer token that your server validates against the provider's authorization server.

Javascript
app.post('/webhooks/provider', async (req, res) => {
  const token = req.headers.authorization?.replace('Bearer ', '');

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

  // Validate token with the provider
  const isValid = await validateToken(token);
  if (!isValid) {
    return res.status(401).json({ error: 'Invalid token' });
  }

  handleEvent(req.body);
  res.status(200).json({ received: true });
});

Pros:

  • Token rotation without changing webhook configuration
  • Fine-grained scopes and permissions
  • Standard protocol with library support

Cons:

  • Requires a network call to validate each webhook (unless using JWTs)
  • More complex setup than HMAC
  • Latency added by token validation

Who uses OAuth:

  • Microsoft Graph
  • Zoom
  • Salesforce

3. API Keys

The simplest approach: include a secret key as a header or query parameter. Your server checks that the key matches.

Javascript
app.post('/webhooks/service', (req, res) => {
  const apiKey = req.headers['x-api-key'];

  if (apiKey !== process.env.WEBHOOK_API_KEY) {
    return res.status(401).json({ error: 'Invalid API key' });
  }

  handleEvent(req.body);
  res.status(200).json({ received: true });
});

Pros:

  • Dead simple to implement
  • No cryptographic operations
  • Easy to rotate

Cons:

  • Key is sent in every request — if intercepted, an attacker can forge webhooks
  • No payload integrity — even with a valid key, the body could be tampered with in transit
  • Must use HTTPS (but you should always use HTTPS anyway)

Comparison

FactorHMACOAuthAPI Key
Payload integrityYesNoNo
Implementation complexityMediumHighLow
Network calls requiredNoneYes (unless JWT)None
Replay protectionWith timestampWith token expiryNo
Industry adoptionVery highMediumLow

Preventing Replay Attacks

Even with valid signatures, an attacker who captures a webhook request can replay it. Defend against this by checking the timestamp:

Javascript
function verifyTimestamp(timestamp, toleranceSeconds = 300) {
  const webhookTime = new Date(timestamp).getTime();
  const now = Date.now();
  const difference = Math.abs(now - webhookTime);

  return difference <= toleranceSeconds * 1000;
}

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

  // Reject webhooks older than 5 minutes
  if (!verifyTimestamp(timestamp)) {
    return res.status(401).json({ error: 'Webhook too old' });
  }

  // Include timestamp in HMAC verification
  const signedPayload = timestamp + '.' + req.rawBody;
  if (!verifyWebhookSignature(signedPayload, signature, secret)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  handleEvent(req.body);
  res.status(200).json({ received: true });
});

Testing Authentication with HookWatch

The HookWatch Sandbox includes a built-in Signature Tester that lets you:

  1. Enter your webhook signing secret
  2. Paste or craft a payload
  3. Generate the correct HMAC signature
  4. Verify your handler accepts it

This makes it easy to test your verification logic without setting up the full provider integration.

Bash
# Use the CLI to send a signed test webhook
hookwatch endpoints create \
  --name "Signed Endpoint" \
  --destination "https://myapp.com/webhooks" \
  --signing-secret "whsec_your_secret_here"

Recommendations

  1. Always use HMAC signatures when the provider supports it
  2. Verify the raw body — never the parsed version
  3. Use constant-time comparison to prevent timing attacks
  4. Check timestamps to prevent replay attacks
  5. Rotate secrets periodically and when team members leave
  6. Log verification failures — they could indicate an attack or a misconfiguration
  7. Always use HTTPS — regardless of which authentication method you use
Tags: securityauthenticationhmacoauthwebhooks

Share this article

Ready to try HookWatch?

Start monitoring your webhooks in minutes. No credit card required.

Start Free Today