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:
| Header | Description |
|---|---|
X-YorAuth-Signature | HMAC-SHA256 signature of the request body. Format: sha256=<hex> |
X-YorAuth-Event | The event type (e.g. user.created) |
X-YorAuth-Delivery-Id | UUID of this delivery attempt |
X-YorAuth-Timestamp | Unix timestamp (seconds) of when the delivery was sent |
Content-Type | Always application/json |
Signature Format
The X-YorAuth-Signature header contains:
sha256=<hex-encoded HMAC-SHA256 digest>
The signature is computed over the raw JSON request body using your webhook secret as the HMAC key.
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.
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
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]);
}
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).
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');
}
}
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:
- Validate the timestamp — Reject requests where
X-YorAuth-Timestampis more than 5 minutes in the past or future. - Track delivery IDs — Optionally store processed
X-YorAuth-Delivery-Idvalues 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
2xxas quickly as possible (within 30 seconds). If your processing is slow, acknowledge the webhook immediately and handle the event asynchronously. - Return
200 OK(or any2xxstatus) even for event types you do not handle — this prevents unnecessary retries. - Do not return a
2xxif your handler failed internally — returning a5xxallows YorAuth to retry the delivery. - Implement idempotent handlers. The same event may be delivered more than once due to retries.