GitHub webhooks use HMAC-SHA256 with the X-Hub-Signature-256 header. The same raw body problem applies — Next.js parses JSON before you can verify the signature.
// app/api/webhooks/github/route.ts
import { createHmac, timingSafeEqual } from 'crypto';
import { NextResponse } from 'next/server';
const secret = process.env.GITHUB_WEBHOOK_SECRET!;
export async function POST(request: Request) {
const body = await request.text(); // raw body first
const sig = request.headers.get('x-hub-signature-256');
if (!sig) {
return NextResponse.json({ error: 'Missing X-Hub-Signature-256' }, { status: 400 });
}
const expected = 'sha256=' + createHmac('sha256', secret).update(body).digest('hex');
// Use timingSafeEqual to prevent timing attacks
const sigBuf = Buffer.from(sig);
const expBuf = Buffer.from(expected);
if (sigBuf.length !== expBuf.length || !timingSafeEqual(sigBuf, expBuf)) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
}
const payload = JSON.parse(body);
const event = request.headers.get('x-github-event');
switch (event) {
case 'push':
// handle push
break;
case 'pull_request':
// handle PR
break;
case 'issues':
// handle issues
break;
}
return NextResponse.json({ received: true });
}
timingSafeEqual for signature comparison. A simple === comparison leaks timing information that can be used to forge signatures.// pages/api/webhooks/github.ts
import { buffer } from 'micro';
import { createHmac, timingSafeEqual } from 'crypto';
export const config = { api: { bodyParser: false } };
const secret = process.env.GITHUB_WEBHOOK_SECRET!;
export default async function handler(req, res) {
const rawBody = (await buffer(req)).toString('utf8');
const sig = req.headers['x-hub-signature-256'] as string;
if (!sig) return res.status(400).json({ error: 'Missing signature' });
const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex');
const sigBuf = Buffer.from(sig);
const expBuf = Buffer.from(expected);
if (sigBuf.length !== expBuf.length || !timingSafeEqual(sigBuf, expBuf)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = req.headers['x-github-event'];
const payload = JSON.parse(rawBody);
res.json({ received: true });
}
Repository → Settings → Webhooks → Edit → Secret # Add the same value as GITHUB_WEBHOOK_SECRET in your .env # GitHub hashes the payload with this secret using HMAC-SHA256
Generate the complete GitHub webhook handler for your framework.
Open WebhookFix →