Secure webhook verification with minimal overhead
npm install @leakguard/webhookš Secure webhook verification with minimal overhead
Verify incoming webhooks from providers like Stripe, GitHub, Zapier, and more with production-grade security and framework-agnostic design.
typescript
// ā Vulnerable to timing attacks
if (providedSignature !== expectedSignature) {
return res.status(401).send('Invalid');
}// ā Improper signature parsing
const signature = header.split('=')[1]; // Breaks on complex formats
// ā Missing replay protection
// No timestamp validation or duplicate detection
// ā
LeakGuard handles all of this correctly
`Installation
`bash
npm install @leakguard/webhookPeer dependencies for specific frameworks
npm install express @types/express # For Express
npm install fastify # For Fastify
npm install @nestjs/common @nestjs/core # For NestJS
`Quick Start
$3
`typescript
import express from 'express';
import { createStripeStrategy, expressWebhookVerify, expressRawBody } from '@leakguard/webhook';const app = express();
// Important: Use rawBody middleware for HMAC verification
app.use('/webhooks', expressRawBody());
// Verify Stripe webhooks
app.use('/webhooks/stripe', expressWebhookVerify({
strategies: [createStripeStrategy(process.env.STRIPE_WEBHOOK_SECRET!)]
}));
app.post('/webhooks/stripe', (req, res) => {
// Webhook is verified ā
const event = req.body;
console.log('Verified Stripe event:', event.type);
res.sendStatus(200);
});
`$3
`typescript
import { Controller, Post, UseGuards, MiddlewareConsumer, Module } from '@nestjs/common';
import {
WebhookVerificationGuard,
RawBodyMiddleware,
createStripeStrategy
} from '@leakguard/webhook';@Controller('webhooks')
export class WebhookController {
@Post('stripe')
@UseGuards(new WebhookVerificationGuard({
strategies: [createStripeStrategy(process.env.STRIPE_WEBHOOK_SECRET!)]
}))
handleStripeWebhook(@Req() req: Request) {
// Webhook is verified ā
console.log('Verification metadata:', req.webhookVerification);
return { received: true };
}
}
@Module({
controllers: [WebhookController]
})
export class WebhookModule {
configure(consumer: MiddlewareConsumer) {
// Apply raw body middleware to webhook routes
consumer.apply(RawBodyMiddleware).forRoutes('webhooks/*');
}
}
`$3
`typescript
import fastify from 'fastify';
import { fastifyWebhookVerify, createGitHubStrategy } from '@leakguard/webhook';const app = fastify();
// Register webhook verification plugin
await app.register(fastifyWebhookVerify, {
webhook: {
strategies: [createGitHubStrategy(process.env.GITHUB_WEBHOOK_SECRET!)],
routes: ['/webhooks/github']
}
});
app.post('/webhooks/github', async (request, reply) => {
// Webhook is verified ā
const payload = request.body;
console.log('GitHub event:', payload);
return { received: true };
});
`Verification Strategies
$3
Verify cryptographic signatures using HMAC algorithms:
`typescript
import { HMACStrategy } from '@leakguard/webhook';const strategy = new HMACStrategy({
secret: 'your-webhook-secret',
algorithm: 'sha256', // sha1, sha256, sha512
headerName: 'x-signature-256',
signaturePrefix: 'sha256=',
tolerance: 300 // 5 minutes timestamp tolerance
});
`$3
Verify static tokens or API keys:
`typescript
import { TokenStrategy } from '@leakguard/webhook';const strategy = new TokenStrategy({
token: 'your-webhook-token',
headerName: 'authorization', // or 'x-api-key', etc.
queryParam: 'token' // Optional: also check query params
});
// Supports Bearer token format
// Header: "Authorization: Bearer your-webhook-token"
`$3
Restrict webhooks to specific IP addresses or CIDR ranges:
`typescript
import { IPWhitelistStrategy } from '@leakguard/webhook';const strategy = new IPWhitelistStrategy({
allowedIPs: [
'192.168.1.1', // Single IP
'10.0.0.0/8', // CIDR range
'172.16.0.0/12'
],
trustProxy: true // Trust X-Forwarded-For headers
});
`$3
Implement your own verification logic:
`typescript
import { CustomStrategy } from '@leakguard/webhook';const strategy = new CustomStrategy(async (request) => {
// Your custom logic here
const isValid = await yourVerificationLogic(request);
return {
verified: isValid,
error: isValid ? undefined : 'Custom verification failed',
metadata: { customField: 'value' }
};
});
`Provider Presets
Pre-configured strategies for popular webhook providers:
$3
`typescript
import { createStripeStrategy } from '@leakguard/webhook';const strategy = createStripeStrategy(
process.env.STRIPE_WEBHOOK_SECRET!,
300 // Optional: timestamp tolerance in seconds
);
// Handles Stripe's signature format:
// stripe-signature: t=1640995200,v1=abc123...
`$3
`typescript
import {
createGitHubStrategy,
createGitHubLegacyStrategy
} from '@leakguard/webhook';// Modern GitHub webhooks (SHA-256)
const strategy = createGitHubStrategy(process.env.GITHUB_WEBHOOK_SECRET!);
// Legacy GitHub webhooks (SHA-1)
const legacyStrategy = createGitHubLegacyStrategy(process.env.GITHUB_WEBHOOK_SECRET!);
`$3
`typescript
import { createZapierStrategy } from '@leakguard/webhook';const strategy = createZapierStrategy(process.env.ZAPIER_TOKEN!);
`$3
`typescript
import { createWebflowStrategy } from '@leakguard/webhook';const strategy = createWebflowStrategy(process.env.WEBFLOW_SECRET!);
`$3
`typescript
import { createSlackStrategy } from '@leakguard/webhook';const strategy = createSlackStrategy(
process.env.SLACK_SIGNING_SECRET!,
300 // Timestamp tolerance
);
`$3
`typescript
import { createDiscordStrategy, createTwilioStrategy } from '@leakguard/webhook';// Discord webhooks (IP allowlist)
const discordStrategy = createDiscordStrategy();
// Twilio webhooks (IP allowlist)
const twilioStrategy = createTwilioStrategy();
`$3
`typescript
import { createShopifyStrategy, createSquareStrategy, createLemonSqueezyStrategy } from '@leakguard/webhook';// Shopify webhooks
const shopifyStrategy = createShopifyStrategy(process.env.SHOPIFY_SECRET!);
// Square webhooks
const squareStrategy = createSquareStrategy(process.env.SQUARE_SIGNATURE_KEY!);
// LemonSqueezy webhooks
const lemonSqueezyStrategy = createLemonSqueezyStrategy(process.env.LEMONSQUEEZY_SECRET!);
`$3
`typescript
import {
createGitLabStrategy,
createBitbucketStrategy,
createCircleCIStrategy,
createVercelStrategy,
createNetlifyStrategy
} from '@leakguard/webhook';// GitLab webhooks
const gitlabStrategy = createGitLabStrategy(process.env.GITLAB_TOKEN!);
// Bitbucket webhooks
const bitbucketStrategy = createBitbucketStrategy(process.env.BITBUCKET_SECRET!);
// CircleCI webhooks
const circleciStrategy = createCircleCIStrategy(process.env.CIRCLECI_SECRET!);
// Vercel webhooks
const vercelStrategy = createVercelStrategy(process.env.VERCEL_SECRET!);
// Netlify webhooks
const netlifyStrategy = createNetlifyStrategy(process.env.NETLIFY_SECRET!);
`$3
`typescript
import {
createLinearStrategy,
createNotionStrategy,
createAsanaStrategy,
createAirtableStrategy,
createCalendlyStrategy,
createFigmaStrategy
} from '@leakguard/webhook';// Linear webhooks
const linearStrategy = createLinearStrategy(process.env.LINEAR_SECRET!);
// Notion webhooks
const notionStrategy = createNotionStrategy(process.env.NOTION_SECRET!);
// Asana webhooks
const asanaStrategy = createAsanaStrategy(process.env.ASANA_SECRET!);
// Airtable webhooks
const airtableStrategy = createAirtableStrategy(process.env.AIRTABLE_SECRET!);
// Calendly webhooks
const calendlyStrategy = createCalendlyStrategy(process.env.CALENDLY_SECRET!);
// Figma webhooks
const figmaStrategy = createFigmaStrategy(process.env.FIGMA_SECRET!);
`$3
`typescript
import {
createSendGridStrategy,
createMailgunStrategy,
createIntercomStrategy,
createHubSpotStrategy
} from '@leakguard/webhook';// SendGrid webhooks
const sendgridStrategy = createSendGridStrategy(process.env.SENDGRID_SECRET!);
// Mailgun webhooks
const mailgunStrategy = createMailgunStrategy(process.env.MAILGUN_SIGNING_KEY!);
// Intercom webhooks
const intercomStrategy = createIntercomStrategy(process.env.INTERCOM_SECRET!);
// HubSpot webhooks
const hubspotStrategy = createHubSpotStrategy(process.env.HUBSPOT_SECRET!);
`$3
`typescript
import {
createSalesforceStrategy,
createZendeskStrategy,
createDocuSignStrategy,
createDropboxStrategy,
createBoxStrategy
} from '@leakguard/webhook';// Salesforce webhooks
const salesforceStrategy = createSalesforceStrategy(process.env.SALESFORCE_SECRET!);
// Zendesk webhooks
const zendeskStrategy = createZendeskStrategy(process.env.ZENDESK_SECRET!);
// DocuSign webhooks
const docusignStrategy = createDocuSignStrategy(process.env.DOCUSIGN_SECRET!);
// Dropbox webhooks
const dropboxStrategy = createDropboxStrategy(process.env.DROPBOX_SECRET!);
// Box webhooks
const boxStrategy = createBoxStrategy(process.env.BOX_SECRET!);
`$3
`typescript
import { createProviderStrategies } from '@leakguard/webhook';const strategies = createProviderStrategies({
stripe: { secret: process.env.STRIPE_SECRET! },
github: { secret: process.env.GITHUB_SECRET! },
shopify: { secret: process.env.SHOPIFY_SECRET! },
linear: { secret: process.env.LINEAR_SECRET! },
sendgrid: { secret: process.env.SENDGRID_SECRET! },
vercel: { secret: process.env.VERCEL_SECRET! }
});
`Configuration Options
$3
Combine multiple verification strategies:
`typescript
const config = {
strategies: [
createStripeStrategy(stripeSecret),
new TokenStrategy({ token: backupToken }),
new IPWhitelistStrategy({ allowedIPs: ['trusted-ip'] })
],
// Require ALL strategies to pass (default: false)
requireAll: true,
// Stateless timestamp validation (mitigates many replay attacks)
timestampValidation: {
timestampHeader: 'x-timestamp',
tolerance: 300,
timestampFormat: 'unix' // or 'iso'
},
// Event handlers
onSuccess: (request) => {
console.log('Webhook verified successfully');
},
onFailure: (result, request) => {
console.error('Webhook verification failed:', result.error);
}
};
`$3
#### Express Options
`typescript
const expressConfig = {
strategies: [/ your strategies /],
skipRoutes: ['/health', '/metrics'], // Skip verification
skipMethods: ['GET', 'OPTIONS'], // Skip HTTP methods
onFailure: (error, req, res, next) => {
// Custom error handling
res.status(401).json({
error: 'Webhook verification failed',
details: error
});
},
onSuccess: (req, res, next) => {
// Custom success handling
req.customField = 'verified';
next();
}
};
`#### NestJS Options
`typescript
const nestConfig = {
strategies: [/ your strategies /],
skipRoutes: ['/health'],
skipMethods: ['GET']
};// Use as Guard
@UseGuards(new WebhookVerificationGuard(nestConfig))
// Use as Middleware
export class ConfiguredMiddleware extends WebhookVerificationMiddleware {
constructor() {
super(nestConfig);
}
}
`#### Fastify Options
`typescript
const fastifyConfig = {
webhook: {
strategies: [/ your strategies /],
routes: ['/webhooks/*'], // Apply only to specific routes
onFailure: (error, request, reply) => {
reply.code(401).send({ error });
},
onSuccess: (request, reply) => {
// Custom success logic
}
}
};
`Stateless Security Architecture
$3
This package is designed for modern deployment patterns:
- ā
Serverless functions (AWS Lambda, Vercel, Cloudflare Workers)
- ā
Kubernetes autoscaling with ephemeral pods
- ā
Docker containers without persistent storage
- ā
Multi-region deployments without shared state
$3
What we provide ā
- Timestamp validation: Rejects requests older than X seconds
- Signature verification: Cryptographic integrity protection
- IP allowlisting: Restrict to known sources
- Timing-safe comparisons: Prevent timing attacks
`typescript
const config = {
strategies: [createStripeStrategy(secret)],
// Stateless replay mitigation
timestampValidation: {
timestampHeader: 'x-timestamp',
tolerance: 300, // 5 minutes - adjust based on your needs
timestampFormat: 'unix'
}
};
`What we DON'T provide ā
- Perfect replay protection: Requires external storage (Redis/DB)
- Request deduplication: Would break stateless architecture
- Global nonce tracking: Incompatible with autoscaling
$3
If your application requires perfect replay protection:
`typescript
// Option 1: Add your own request tracking
const processedRequests = new Set(); // Or Redis/DBapp.use('/webhook', (req, res, next) => {
const requestId = createRequestId(req);
if (processedRequests.has(requestId)) {
return res.status(409).send('Duplicate request');
}
processedRequests.add(requestId);
next();
});
// Option 2: Use provider-specific replay protection
// Many providers (Stripe, GitHub) include their own replay protection
// Option 3: Accept the tradeoff
// For many use cases, timestamp validation is sufficient
`$3
| Approach | Replay Protection | Scalability | Complexity |
|----------|------------------|-------------|------------|
| Stateless (LeakGuard) | Good | Excellent | Low |
| Redis/DB tracking | Perfect | Good | Medium |
| Provider built-in | Perfect | Excellent | Low |
Security Best Practices
$3
`typescript
// ā
Correct - Raw body preserved
app.use('/webhooks', rawBody());
app.use('/webhooks', webhookVerify({ ... }));// ā Wrong - Body already parsed
app.use(express.json());
app.use('/webhooks', webhookVerify({ ... }));
`$3
`typescript
// ā
Force HTTPS in production
app.use((req, res, next) => {
if (req.header('x-forwarded-proto') !== 'https') {
return res.redirect(https://${req.header('host')}${req.url});
}
next();
});
`$3
`typescript
import rateLimit from 'express-rate-limit';// ā
Rate limit webhook endpoints
const webhookLimiter = rateLimit({
windowMs: 15 60 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
app.use('/webhooks', webhookLimiter);
`$3
`typescript
// ā
Separate secrets for each provider
const strategies = [
createStripeStrategy(process.env.STRIPE_WEBHOOK_SECRET),
createGitHubStrategy(process.env.GITHUB_WEBHOOK_SECRET),
// Don't reuse secrets across providers
];
`$3
`typescript
// ā
Enable timestamp validation for time-sensitive webhooks
const config = {
strategies: [strategy],
timestampValidation: {
timestampHeader: 'x-timestamp',
tolerance: 300 // 5 minutes
}
};
`Performance
$3
- HMAC (small payload): ~50,000 ops/sec
- HMAC (large payload): ~5,000 ops/sec
- Token verification: ~100,000+ ops/sec
- IP allowlist: ~150,000+ ops/sec
- Multiple strategies: ~20,000 ops/sec
$3
1. Use specific strategies: Avoid unnecessary verification steps
2. Order strategies by speed: Fast strategies first when using
requireAll: false
3. Enable replay protection selectively: Only for time-sensitive webhooks
4. Use IP allowlisting when possible: Fastest verification method`typescript
// ā
Optimized configuration
const config = {
strategies: [
// Fast strategies first
new IPWhitelistStrategy({ allowedIPs: ['trusted-range'] }),
new TokenStrategy({ token: 'backup-token' }),
// Slower HMAC verification last
new HMACStrategy({ secret: 'hmac-secret' })
],
requireAll: false // Exit early on first success
};
`Troubleshooting
$3
#### 1. HMAC Verification Fails
`typescript
// Check these common issues:// ā
Ensure raw body is available
console.log('Raw body type:', typeof request.rawBody);
console.log('Raw body length:', request.rawBody?.length);
// ā
Check signature header format
console.log('Signature header:', request.headers['x-signature']);
// ā
Verify secret is correct
console.log('Secret (first 4 chars):', secret.substring(0, 4));
// ā
Check algorithm matches provider
const strategy = new HMACStrategy({
secret: 'correct-secret',
algorithm: 'sha256', // Make sure this matches provider
headerName: 'x-hub-signature-256',
signaturePrefix: 'sha256='
});
`#### 2. Missing Headers
`typescript
// ā
Handle missing headers gracefully
const config = {
strategies: [strategy],
onFailure: (result, request) => {
console.log('Available headers:', Object.keys(request.headers));
console.log('Failure reason:', result.error);
}
};
`#### 3. Timestamp Issues
`typescript
// ā
Debug timestamp validation
const config = {
timestampValidation: {
timestampHeader: 'x-timestamp',
tolerance: 600, // Increase tolerance temporarily
timestampFormat: 'unix'
},
onFailure: (result) => {
console.log('Current time:', Math.floor(Date.now() / 1000));
console.log('Request timestamp:', / extract from headers /);
}
};
`Error Handling
$3
`typescript
// Express custom error handling
app.use('/webhooks', webhookVerify({
strategies: [strategy],
onFailure: (error, req, res, next) => {
// Log for debugging
console.error('Webhook verification failed:', {
error,
headers: req.headers,
ip: req.ip,
url: req.url
});
// Return generic error (don't leak verification details)
res.status(401).json({
error: 'Unauthorized',
timestamp: new Date().toISOString()
});
}
}));// NestJS exception handling
@UseGuards(new WebhookVerificationGuard(config))
@Post('webhook')
async handleWebhook(@Req() req: Request) {
try {
// Process webhook
return { success: true };
} catch (error) {
// Webhook was verified, but processing failed
throw new InternalServerErrorException('Webhook processing failed');
}
}
`API Reference
$3
`typescript
interface WebhookRequest {
headers: Record;
body: Buffer | string;
rawBody?: Buffer;
ip?: string;
}interface VerificationResult {
verified: boolean;
error?: string;
metadata?: Record;
}
interface WebhookConfig {
strategies: VerificationStrategy[];
requireAll?: boolean;
timestampValidation?: TimestampValidationOptions;
onFailure?: (result: VerificationResult, request: WebhookRequest) => void;
onSuccess?: (request: WebhookRequest) => void;
}
`$3
-
HMACStrategy(options: HMACOptions)
- TokenStrategy(options: TokenOptions)
- IPWhitelistStrategy(options: IPWhitelistOptions)
- CustomStrategy(verifyFn: CustomVerificationFunction)$3
-
createStripeStrategy(secret: string, tolerance?: number)
- createGitHubStrategy(secret: string)
- createZapierStrategy(token: string)
- createWebflowStrategy(secret: string)
- createSlackStrategy(secret: string, tolerance?: number)
- createDiscordStrategy()
- createTwilioStrategy()$3
-
expressWebhookVerify(config: ExpressWebhookOptions)
- expressRawBody()
- fastifyWebhookVerify
- WebhookVerificationGuard (NestJS)
- WebhookVerificationMiddleware (NestJS)Examples
$3
`typescript
import express from 'express';
import {
createStripeStrategy,
createGitHubStrategy,
createZapierStrategy,
expressWebhookVerify,
expressRawBody
} from '@leakguard/webhook';const app = express();
// Global raw body middleware
app.use('/webhooks', expressRawBody());
// Stripe webhooks
app.use('/webhooks/stripe', expressWebhookVerify({
strategies: [createStripeStrategy(process.env.STRIPE_WEBHOOK_SECRET!)]
}));
app.post('/webhooks/stripe', (req, res) => {
const event = req.body;
console.log('Stripe event:', event.type);
switch (event.type) {
case 'payment_intent.succeeded':
// Handle payment success
break;
case 'customer.subscription.deleted':
// Handle subscription cancellation
break;
}
res.sendStatus(200);
});
// GitHub webhooks
app.use('/webhooks/github', expressWebhookVerify({
strategies: [createGitHubStrategy(process.env.GITHUB_WEBHOOK_SECRET!)]
}));
app.post('/webhooks/github', (req, res) => {
const event = req.headers['x-github-event'];
const payload = req.body;
console.log('GitHub event:', event);
if (event === 'push') {
// Handle repository push
}
res.sendStatus(200);
});
// Zapier webhooks (token-based)
app.use('/webhooks/zapier', expressWebhookVerify({
strategies: [createZapierStrategy(process.env.ZAPIER_TOKEN!)]
}));
app.post('/webhooks/zapier', (req, res) => {
console.log('Zapier payload:', req.body);
res.sendStatus(200);
});
`$3
`typescript
import {
HMACStrategy,
TokenStrategy,
IPWhitelistStrategy,
expressWebhookVerify
} from '@leakguard/webhook';// Multi-layer verification
const highSecurityConfig = {
strategies: [
// Layer 1: IP allowlist
new IPWhitelistStrategy({
allowedIPs: ['trusted-cidr-range/24'],
trustProxy: true
}),
// Layer 2: Token verification
new TokenStrategy({
token: process.env.BACKUP_TOKEN!,
headerName: 'x-api-key'
}),
// Layer 3: HMAC signature
new HMACStrategy({
secret: process.env.HMAC_SECRET!,
algorithm: 'sha256',
headerName: 'x-signature-256'
})
],
// Require ALL strategies to pass
requireAll: true,
// Strict replay protection
replayProtection: {
timestampHeader: 'x-timestamp',
tolerance: 60, // 1 minute window
timestampFormat: 'unix'
},
onFailure: (result, request) => {
// Log security events
console.error('Security violation:', {
error: result.error,
ip: request.ip,
timestamp: new Date().toISOString(),
headers: request.headers
});
}
};
app.use('/webhooks/critical', expressWebhookVerify(highSecurityConfig));
``MIT - see LICENSE for details.