Verifying blob storage callback

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');
}

There’s another community post with 404 debugging tips that might be helpful. Please give these solutions a try and let us know how it goes.

A human should be around soon to offer more advice. But you can also get helpful information quickly by asking v0.

Hi, welcome to the Vercel Community!

The x-vercel-signature is for verifying webhooks dispatched FROM vercel, for example to react to when new deployments are created or canceled. Instructions to get that secret are here Setting Up Webhooks. You’re correct that it’s limited to Pro/Enterprise but unless you are trying to react to one of those events specifically then this might not be what you’re looking for.

The @vercel/blob package reads your BLOB_READ_WRITE_TOKEN internally for authentication, so as long as you have that set then you can consider the handleUpload function to be properly authenticated.

All that’s left is to secure your POST endpoint, which comes down to who will be using it. If this is part of your app, and logged in users are uploading spreadsheets through your app interface, then Clerk’s await auth.protect() at the top of the handler should work.

If not, could you tell me a little more about how files are being uploaded here and I can give you more info on how to secure your endpoint?

Hi Jacob, appreciate the quick response. From what I can gather, two requests to the /api/upload endpoint occur:

  1. From the client (assuming handleUploadUrl == '/api/upload' passed to upload)
    async function handleUpload(file: File) {
        setIsUploading(true);
        setFileData({
            name: file.name,
            size: file.size,
        });

        await upload(file.name, file, {
            access: 'public',
            handleUploadUrl: '/api/upload',
            onUploadProgress: (progressEvent) => {
                setProgress(progressEvent.percentage);
            },
        });

        setIsUploading(false);
        setFileData({ name: '', size: 0 });
    }
  1. The callback FROM Vercel to /api/upload to signal onUploadComplete.

The first call from the client is properly authenticated, but the callback from Vercel to the endpoint is not authenticated. I observed this by adding logs to the middleware and log the auth status and request for both calls.

In the first call to /api/upload (from the client):

  • auth contains user and session information.
  • request contains Clerk cookie information.
  • auth inside of the route handler also contains authenticated user information

In the second call to /api/upload (from Vercel):

  • auth contains NO user and session information (everything is null)
  • request contains NO Clerk cookie information, but DOES contain the following Vercel cookies and headers:

Vercel cookies in request:

{
    "_vercel_jwt": {
        "name": "_vercel_jwt",
        "value": "****"
    }
}

Vercel headers in request:

x-vercel-signature: '*****'

Because there is no Clerk cookie information propagated from Vercel back to my service, authenticating with Clerk in the callback (i.e., for indicating upload complete) isn’t possible. This is what led me down the path of verifying the signature that Vercel sends along with the callback request.

Ah, thanks for the additional context!

I’ve looked into this and it seems the upload completed handler takes care of verifying the signature itself, based on the BLOB_READ_WRITE_TOKEN in the environment, so there’s no additional validation you need to do on your end. The relevant verification lines from the vercel blob package are here

    case 'blob.upload-completed': {
      const signatureHeader = 'x-vercel-signature';
      const signature = (
        'credentials' in request
          ? (request.headers.get(signatureHeader) ?? '')
          : (request.headers[signatureHeader] ?? '')
      ) as string;


      if (!signature) {
        throw new BlobError('Missing callback signature');
      }


      const isVerified = await verifyCallbackSignature({
        token: resolvedToken,
        signature,
        body: JSON.stringify(body),
      });

      if (!isVerified) {
        throw new BlobError('Invalid callback signature');
      }
2 Likes

Ah! Thank you so much for your help! Makes perfect sense. Should’ve looked at the source :man_facepalming:.

2 Likes

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.