verifyWebhookSignature return false

73 Views Asked by At
api.use(async (ctx, next) => {
    if (ctx.path === '/webhook') {
        const sig = ctx.request.headers['stripe-signature'];
        unparsed = Symbol.for('unparsedBody')
        try {
            event = stripe.webhooks.constructEvent(ctx.request.body[unparsed], sig, endpointSecret);
        }
        catch (err) {
            console.log("error", err)
        }

the construct event function report this error: StripeSignatureVerificationError: No signatures found matching the expected signature for payload.

So i am sure that the endpointSecret is correct, i passed the unparsedBody as well. so i am guessing it's signature error, so i used this function to verify the signature:

function verifyWebhookSignature(signatureHeader, payload, secret) {
    try {
      // Step 1: Extract the timestamp and signatures
      const elements = signatureHeader.split(',');
      let timestamp = null;
      const signatures = {};
      for (const element of elements) {
        const [key, value] = element.split('=');
        if (key === 't') {
          timestamp = parseInt(value, 10);
        } else if (key.startsWith('v0')) {
          signatures['v0'] = value;
        }
      }
  
      // If there's no timestamp or signature, or if the timestamp is too old, return false.
      console.log("timestamp: ", timestamp)
      if (!timestamp || (Date.now() / 1000 - timestamp) > 300) {
        return false;
      }

      // Step 2: Prepare the signed_payload string
      const signedPayload = `${timestamp}.${payload}`;
  
      console.log("signedPayload: ", signedPayload)

      // Step 3: Determine the expected signature
      const expectedSignature = crypto
        .createHmac('sha256', secret)
        .update(signedPayload)
        .digest('hex');
  
    console.log("expectedSignature: ", expectedSignature)   
    console.log("signatures[v0]: ", signatures['v0'])
      // Step 4: Compare the signatures
      if (signatures['v0']) {
        console.log(100)
        return crypto.timingSafeEqual(
          Buffer.from(expectedSignature, 'hex'),
          Buffer.from(signatures['v0'], 'hex')
        );
      } else {
        console.log(888)
        return false;
      }
    } catch (error) {
        console.log(999)
      // Handle any exceptions or errors
      console.error(`Signature verification failed: ${error}`);
      return false;
    }
  }

It turns out that this line:

        return crypto.timingSafeEqual(
          Buffer.from(expectedSignature, 'hex'),
          Buffer.from(signatures['v0'], 'hex')
        );

always return false.

I print most of the data, and if you need any extra data, plz feel free to let me know.

Anyone know why? Thank you very much!

1

There are 1 best solutions below

1
Corey Clavette On

Signature verification could be going wrong in a couple different places here, as far as I can tell.

These issues are often caused by a few situations:

  1. The signature is not in the event at all. I assume this is not the case, since you put log lines to confirm this, so onto the next one
  2. The webhook secret you have in your code is not the same as the one in the dashboard. You can double-check this by navigating to the Dashboard's Webhook page and clicking on the webhook endpoint you're working with. From there you'll see some clickable text that says "reveal" under the "Signing secret". Confirm that the signing secret there is the same as the one you use in your code. You should have a line that looks like this where you can copy/paste it --> const endpointSecret = 'whsec_...';
  3. The encoding on the string with the event data is not set to UTF-8. Stripe treats everything as UTF-8 in the API and the string Stripe would have signed ends up being different than the one you see. You need to enforce the encoding in your code if you're not already.
  4. The data you sign is different from the data Stripe signed. Often, this means that you're using a framework that tried to be helpful and parsed the event data as JSON. Because of this, when you calculate your own signature, you do this on a string that is not exactly the same. For example the order of properties might have changed or the indentation would be different. For the signatures to match, you need to calculate it on the exact same raw string as Stripe did. For this, you need to ensure that you get the raw body of the HTTP request that Stripe sent you, without any interference by your code or framework in the middle.