Goal: I’m trying to verify the x-vercel-signature
header on the blob storage callback, but it’s not clear which secret to use for verification. I’m basing my implementation off the example in the documentation: Webhooks API Reference
I’m using Clerk for authentication and by default it protects non-public routes in the middleware. However, in order to allow Vercel to post back to my service, I have to make the upload API route public. Due to this, I need to authorize the request in the route handler. My approach was to follow what the documentation above is doing and validate the x-vercel-signature
header. In the documentation, they are using an INTEGRATION_SECRET
, and it’s not obvious where that comes from. My interpretation of the docs is that maybe this is a webhook secret or something? If that’s the case, according to the docs, webhooks are a service only available to Pro and Enterprise accounts (currently on a Hobby account).
I’ve tried doing the verification against the BLOB_READ_WRITE_TOKEN
to no avail. Any help would be appreciated. Below is the route handler I have if that’s helpful:
import { currentUser } from '@clerk/nextjs/server';
import { handleUpload, type HandleUploadBody } from '@vercel/blob/client';
import crypto from 'crypto';
import { NextResponse } from 'next/server';
export async function POST(request: Request): Promise<NextResponse> {
const body = await authenticateBody(request);
if (!body) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
try {
const jsonResponse = await handleUpload({
body,
request,
onBeforeGenerateToken: async () => {
console.log('*** Generating token for upload');
return {
allowedContentTypes: [
'image/*',
'text/csv',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
],
maximumSizeInBytes: 50 * 1024 * 1024, // 50MB
};
},
onUploadCompleted: async ({ blob, tokenPayload }) => {
console.log('*** blob upload completed', blob, tokenPayload);
},
});
return NextResponse.json(jsonResponse);
} catch (error) {
console.error('*** Upload error:', error);
return NextResponse.json(
{ error: (error as Error).message },
{ status: 400 }
);
}
}
async function authenticateBody(request: Request): Promise<HandleUploadBody | null> {
const signature = request.headers.get('x-vercel-signature');
const user = await currentUser();
if (!user && !signature) {
return null;
}
const rawBody = await request.text();
const rawBodyBuffer = Buffer.from(rawBody, 'utf-8');
if (user) {
return JSON.parse(rawBody) as HandleUploadBody;
} else {
const { BLOB_READ_WRITE_TOKEN } = process.env;
if (!BLOB_READ_WRITE_TOKEN) {
return null;
}
const bodySignature = sha1(rawBodyBuffer, BLOB_READ_WRITE_TOKEN);
console.log('*** BLOB_READ_WRITE_TOKEN', BLOB_READ_WRITE_TOKEN);
console.log('*** signature', signature);
console.log('*** bodySignature', bodySignature);
console.log('*** bodySignature === signature', bodySignature === signature);
if (bodySignature === signature) {
return JSON.parse(rawBody) as HandleUploadBody;
}
return null;
}
}
function sha1(data: Buffer, secret: string): string {
return crypto.createHmac('sha1', secret).update(data).digest('hex');
}