Webhook Signature Verification

Every delivery is signed with HMAC-SHA256 using the secret you set when creating the subscription. Verify the signature before processing any payload.

The signature header

AuthWorx sets three headers on every delivery:

HeaderExample valueDescription
X-Webhook-Signature sha256=a3b4c5d6e7f8... HMAC-SHA256 hex digest of the raw request body, prefixed with sha256=.
X-Webhook-Event member.joined The event name, for quick routing without parsing the body.
X-Delivery-Id del_01WXYZ Unique delivery ID — use for idempotency checks.

How the signature is computed

signature = "sha256=" + HMAC-SHA256(secret, rawRequestBody)

The HMAC is computed over the raw request body bytes before any JSON parsing. Do not use a parsed/re-serialized object — even a single added space will invalidate the signature.

🔒
Use constant-time comparison (timingSafeEqual) when comparing signatures. A naive === check is vulnerable to timing attacks that can leak the secret.

Node.js / Next.js

lib/webhook.ts
import crypto from 'crypto';

export function verifyWebhookSignature(
  rawBody: Buffer,
  signatureHeader: string,
  secret: string
): boolean {
  const expected = 'sha256=' +
    crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  try {
    return crypto.timingSafeEqual(
      Buffer.from(signatureHeader),
      Buffer.from(expected)
    );
  } catch {
    return false;  // buffers differ in length
  }
}
app/api/webhooks/authworx/route.ts
import { NextRequest } from 'next/server';
import { verifyWebhookSignature } from '@/lib/webhook';

export async function POST(req: NextRequest) {
  const rawBody = Buffer.from(await req.arrayBuffer());
  const sig = req.headers.get('x-webhook-signature') ?? '';

  if (!verifyWebhookSignature(rawBody, sig, process.env.AUTHWORX_WEBHOOK_SECRET!)) {
    return new Response('Invalid signature', { status: 401 });
  }

  const event = JSON.parse(rawBody.toString());

  switch (event.event) {
    case 'member.joined':
      // Provision the user in your database
      // await db.users.upsert({ email: event.data.user.email, orgId: event.orgId });
      break;

    case 'member.removed':
      // Revoke the user's access
      // await db.users.deactivate({ email: event.data.user.email, orgId: event.orgId });
      break;

    case 'user.login':
      // Log the login event
      break;

    default:
      // Unknown event — ignore safely
      break;
  }

  return new Response('OK', { status: 200 });
}

Python (Flask)

import hmac
import hashlib
from flask import Flask, request, abort

app = Flask(__name__)
WEBHOOK_SECRET = os.environ['AUTHWORX_WEBHOOK_SECRET']

@app.route('/webhooks/authworx', methods=['POST'])
def authworx_webhook():
    sig_header = request.headers.get('X-Webhook-Signature', '')
    expected = 'sha256=' + hmac.new(
        WEBHOOK_SECRET.encode(),
        request.get_data(),
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(sig_header, expected):
        abort(401)

    event = request.get_json()
    print(f"Received event: {event['event']}")
    return 'OK', 200

Idempotency

Due to retries, your endpoint may receive the same delivery more than once. Use the X-Delivery-Id header to detect and discard duplicates.

Node.js — idempotency check
const deliveryId = req.headers.get('x-delivery-id');

// Check if we've already processed this delivery
const alreadyProcessed = await cache.get(`webhook:${deliveryId}`);
if (alreadyProcessed) {
  return new Response('OK', { status: 200 });  // acknowledge and skip
}

// Process the event ...

// Mark as processed (store for at least 24 hours)
await cache.set(`webhook:${deliveryId}`, true, { ex: 86400 });

Security checklist