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: https://vercel.com/docs/webhooks/webhooks-api#securing-webhooks
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');}