Skip to main content

Security

Every webhook request YorAuth sends is signed with HMAC-SHA256. Verifying this signature lets you confirm the payload came from YorAuth and has not been modified in transit.

Always verify webhook signatures. Store your webhook secret securely on your server and verify the X-YorAuth-Signature header on every incoming webhook request to prevent spoofing. See Security Best Practices.

Request Headers

Each webhook delivery includes these headers:

HeaderDescription
X-YorAuth-SignatureHMAC-SHA256 signature of the request body. Format: sha256=<hex>
X-YorAuth-EventThe event type (e.g. user.created)
X-YorAuth-Delivery-IdUUID of this delivery attempt
X-YorAuth-TimestampUnix timestamp (seconds) of when the delivery was sent
Content-TypeAlways application/json

Signature Format

The X-YorAuth-Signature header contains:

text
sha256=<hex-encoded HMAC-SHA256 digest>

The signature is computed over the raw JSON request body using your webhook secret as the HMAC key.

text
HMAC-SHA256(request_body_bytes, webhook_secret)

Verifying Signatures

To verify a webhook, recompute the HMAC-SHA256 of the raw request body using your webhook secret and compare it to the X-YorAuth-Signature header value using a constant-time comparison.

Always use a constant-time string comparison to prevent timing attacks. Do not use == or === for comparing signatures.

javascript
const crypto = require('crypto');

function verifyWebhookSignature(req, secret) {
  const signatureHeader = req.headers['x-yorauth-signature'];
  if (!signatureHeader) {
    throw new Error('Missing X-YorAuth-Signature header');
  }

  // req.body must be the raw Buffer — not a parsed object.
  // With Express: app.use(express.raw({ type: 'application/json' }))
  const rawBody = req.body;

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

  // Constant-time comparison
  const sigBuffer = Buffer.from(signatureHeader, 'utf8');
  const expectedBuffer = Buffer.from(expectedSignature, 'utf8');

  if (sigBuffer.length !== expectedBuffer.length) {
    throw new Error('Signature mismatch');
  }

  if (!crypto.timingSafeEqual(sigBuffer, expectedBuffer)) {
    throw new Error('Signature mismatch');
  }

  return true;
}

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

app.post('/webhooks/yorauth', express.raw({ type: 'application/json' }), (req, res) => {
  try {
    verifyWebhookSignature(req, process.env.YORAUTH_WEBHOOK_SECRET);
  } catch (err) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const event = JSON.parse(req.body);
  console.log('Received event:', event.event);

  // Handle the event ...

  res.status(200).json({ received: true });
});
php
<?php

function verifyWebhookSignature(string $rawBody, string $signatureHeader, string $secret): bool
{
    if (empty($signatureHeader)) {
        return false;
    }

    $expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);

    // hash_equals() is a constant-time comparison
    return hash_equals($expected, $signatureHeader);
}

// Example in a Laravel controller
use Illuminate\Http\Request;

public function handle(Request $request): \Illuminate\Http\JsonResponse
{
    $rawBody = $request->getContent();
    $signature = $request->header('X-YorAuth-Signature');
    $secret = config('services.yorauth.webhook_secret');

    if (!verifyWebhookSignature($rawBody, $signature, $secret)) {
        return response()->json(['error' => 'Invalid signature'], 401);
    }

    $payload = $request->json()->all();
    $event = $payload['event'];

    // Handle the event ...

    return response()->json(['received' => true]);
}
python
import hmac
import hashlib

def verify_webhook_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
    if not signature_header:
        return False

    expected = 'sha256=' + hmac.new(
        secret.encode('utf-8'),
        raw_body,
        hashlib.sha256
    ).hexdigest()

    # hmac.compare_digest() is a constant-time comparison
    return hmac.compare_digest(expected, signature_header)

# Flask example
from flask import Flask, request, abort
import json
import os

app = Flask(__name__)

@app.route('/webhooks/yorauth', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-YorAuth-Signature', '')
    secret = os.environ['YORAUTH_WEBHOOK_SECRET']

    if not verify_webhook_signature(request.get_data(), signature, secret):
        abort(401)

    payload = request.get_json()
    print('Received event:', payload['event'])

    # Handle the event ...

    return {'received': True}

Timestamp Validation

To protect against replay attacks, validate that the delivery timestamp is within an acceptable window (we recommend 5 minutes).

javascript
function verifyTimestamp(req, toleranceSeconds = 300) {
  const timestamp = parseInt(req.headers['x-yorauth-timestamp'], 10);
  if (isNaN(timestamp)) {
    throw new Error('Missing or invalid X-YorAuth-Timestamp header');
  }

  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - timestamp) > toleranceSeconds) {
    throw new Error('Webhook timestamp is too old — possible replay attack');
  }
}
php
function verifyTimestamp(string $timestampHeader, int $toleranceSeconds = 300): bool
{
    $timestamp = filter_var($timestampHeader, FILTER_VALIDATE_INT);

    if ($timestamp === false) {
        return false;
    }

    return abs(time() - $timestamp) <= $toleranceSeconds;
}

Replay Attack Prevention

A replay attack occurs when a legitimate webhook payload is captured and re-sent later. To prevent this:

  1. Validate the timestamp — Reject requests where X-YorAuth-Timestamp is more than 5 minutes in the past or future.
  2. Track delivery IDs — Optionally store processed X-YorAuth-Delivery-Id values in a cache (e.g. Redis) and reject duplicate deliveries. Delivery IDs are UUIDs that are unique per attempt.

Storing Your Webhook Secret

The signing secret is returned once at webhook creation and once after each secret rotation. Treat it like a password:

  • Store it as an environment variable or in a secrets manager (e.g. AWS Secrets Manager, HashiCorp Vault).
  • Never log the secret or expose it in client-side code.
  • Never commit it to version control.
  • If a secret is compromised, rotate it immediately via the dashboard or the rotate-secret API endpoint.

HTTPS Requirement

YorAuth only sends webhooks to HTTPS endpoints. Plain HTTP is rejected at webhook creation time. Ensure your endpoint has a valid TLS certificate from a trusted CA.

Best Practices

  • Respond with 2xx as quickly as possible (within 30 seconds). If your processing is slow, acknowledge the webhook immediately and handle the event asynchronously.
  • Return 200 OK (or any 2xx status) even for event types you do not handle — this prevents unnecessary retries.
  • Do not return a 2xx if your handler failed internally — returning a 5xx allows YorAuth to retry the delivery.
  • Implement idempotent handlers. The same event may be delivered more than once due to retries.