Verifying HMAC signature from webhook in nodeJS

Solved
chris_pappen
Shopify Partner
14 0 4

Hello guys,

I am developing an public Shopify app using the Shopify CLI. Therefore I am using NodeJS + Koa + NextJS for the backend as this was preinstalled from Shopify CLI.

I managed to register a webhook. As Shopify suggests it is necessary to verify the signature. I use this package to do that. However, I'm struggling with implementing it as a proper middleware for this path. Right now, it works but the actual code through: 

await handle(ctx.req, ctx.res);

 is not working. I am expecting a console output but nothing is showing up.

Any ideas what I am doing wrong here?

 

Screenshot 2020-12-06 at 20.07.15.png

Thanks,

Chris

Accepted Solution (1)

Accepted Solutions
_JCC_
Shopify Staff
Shopify Staff
170 24 32

This is an accepted solution.

@chris_pappen ,

Happy to have a look at this for you.

I've done some testing with success, using your details as a guide and wanted to confirm a couple of things.

In your example, are trying to test if the HMAC fails? You mentioned your expecting console output but the screenshot provided would only output console if the Webhook HMAC signature validation fails. As a test I provided an invalid api secret and the console output Access Denied was printed and the 401 was thrown and returned to Shopify.

I hope this helps you out, if not please don't hesitate to reply with any questions or points of clarification.

Regards,

John

John C | Developer Support @ Shopify 
 - Was my reply helpful? Click Like to let me know! 
 - Was your question answered? Mark it as an Accepted Solution
 - To learn more visit Shopify.dev or the Shopify Web Design and Development Blog

View solution in original post

Replies 8 (8)
_JCC_
Shopify Staff
Shopify Staff
170 24 32

This is an accepted solution.

@chris_pappen ,

Happy to have a look at this for you.

I've done some testing with success, using your details as a guide and wanted to confirm a couple of things.

In your example, are trying to test if the HMAC fails? You mentioned your expecting console output but the screenshot provided would only output console if the Webhook HMAC signature validation fails. As a test I provided an invalid api secret and the console output Access Denied was printed and the 401 was thrown and returned to Shopify.

I hope this helps you out, if not please don't hesitate to reply with any questions or points of clarification.

Regards,

John

John C | Developer Support @ Shopify 
 - Was my reply helpful? Click Like to let me know! 
 - Was your question answered? Mark it as an Accepted Solution
 - To learn more visit Shopify.dev or the Shopify Web Design and Development Blog

chris_pappen
Shopify Partner
14 0 4

Hello John,

please excuse my late reply ... holidays and stuff 😉

When I talked about not getting the console output I was referring to the console output in the handler. Sorry for my unclear language! Please see the image attached with the handler.

Screenshot 2020-12-27 at 21.57.12.png

If the HMAC fails an error is thrown and the catch statement fires. That works fine. But if the verification is correct the handler is not executed for some weird reason. I am really stuck on this ... any feedback highly appreciated!

 

Thanks in advance! 🙂

 

Best,

Chris

chris_pappen
Shopify Partner
14 0 4

Hi John,

I debugged a little further and I can see that the handler is not executed for a reason. So it's not related to the verification itself. 

Thanks for your help!

 

Chris

dustinvn
New Member
2 0 0

I got a same problem with verifying webhook HMAC (nodejs). 

I tried to use both SECRET KEY and SIGNED WEBHOOK KEY but it's not worked.

This is my code and dump data. Please help.

Screen Shot 2021-07-17 at 4.51.14 PM.png

DATA
{"id":820982911946154500,"email":"jon@doe.ca","closed_at":null,"created_at":"2021-07-17T16:54:18+07:00","updated_at":"2021-07-17T16:54:18+07:00","number":234,"note":null,"token":"123456abcd","gateway":null,"test":true,"total_price":"255","subtotal_price":"245","total_weight":0,"total_tax":"0","taxes_included":false,"currency":"VND","financial_status":"voided","confirmed":false,"total_discounts":"5","total_line_items_price":"250","cart_token":null,"buyer_accepts_marketing":true,"name":"#9999","referring_site":null,"landing_site":null,"cancelled_at":"2021-07-17T16:54:18+07:00","cancel_reason":"customer","total_price_usd":null,"checkout_token":null,"reference":null,"user_id":null,"location_id":null,"source_identifier":null,"source_url":null,"processed_at":null,"device_id":null,"phone":null,"customer_locale":"en","app_id":null,"browser_ip":null,"landing_site_ref":null,"order_number":1234,"discount_applications":[{"type":"manual","value":"5.0","value_type":"fixed_amount","allocation_method":"each","target_selection":"explicit","target_type":"line_item","description":"Discount","title":"Discount"}],"discount_codes":[],"note_attributes":[],"payment_gateway_names":["visa","bogus"],"processing_method":"","checkout_id":null,"source_name":"web","fulfillment_status":"pending","tax_lines":[],"tags":"","contact_email":"jon@doe.ca","order_status_url":"https://ducstore3.myshopify.com/55670145208/orders/123456abcd/authenticate?key=abcdefg","presentment_currency":"VND","total_line_items_price_set":{"shop_money":{"amount":"250","currency_code":"VND"},"presentment_money":{"amount":"250","currency_code":"VND"}},"total_discounts_set":{"shop_money":{"amount":"5","currency_code":"VND"},"presentment_money":{"amount":"5","currency_code":"VND"}},"total_shipping_price_set":{"shop_money":{"amount":"10","currency_code":"VND"},"presentment_money":{"amount":"10","currency_code":"VND"}},"subtotal_price_set":{"shop_money":{"amount":"245","currency_code":"VND"},"presentment_money":{"amount":"245","currency_code":"VND"}},"total_price_set":{"shop_money":{"amount":"255","currency_code":"VND"},"presentment_money":{"amount":"255","currency_code":"VND"}},"total_tax_set":{"shop_money":{"amount":"0","currency_code":"VND"},"presentment_money":{"amount":"0","currency_code":"VND"}},"line_items":[{"id":487817672276298560,"variant_id":null,"title":"Aviator sunglasses","quantity":1,"sku":"SKU2006-001","variant_title":null,"vendor":null,"fulfillment_service":"manual","product_id":788032119674292900,"requires_shipping":true,"taxable":true,"gift_card":false,"name":"Aviator sunglasses","variant_inventory_management":null,"properties":[],"product_exists":true,"fulfillable_quantity":1,"grams":100,"price":"90","total_discount":"0","fulfillment_status":null,"price_set":{"shop_money":{"amount":"90","currency_code":"VND"},"presentment_money":{"amount":"90","currency_code":"VND"}},"total_discount_set":{"shop_money":{"amount":"0","currency_code":"VND"},"presentment_money":{"amount":"0","currency_code":"VND"}},"discount_allocations":[],"duties":[],"admin_graphql_api_id":"gid://shopify/LineItem/487817672276298554","tax_lines":[]},{"id":976318377106520300,"variant_id":null,"title":"Mid-century lounger","quantity":1,"sku":"SKU2006-020","variant_title":null,"vendor":null,"fulfillment_service":"manual","product_id":788032119674292900,"requires_shipping":true,"taxable":true,"gift_card":false,"name":"Mid-century lounger","variant_inventory_management":null,"properties":[],"product_exists":true,"fulfillable_quantity":1,"grams":1000,"price":"160","total_discount":"5","fulfillment_status":null,"price_set":{"shop_money":{"amount":"160","currency_code":"VND"},"presentment_money":{"amount":"160","currency_code":"VND"}},"total_discount_set":{"shop_money":{"amount":"5","currency_code":"VND"},"presentment_money":{"amount":"5","currency_code":"VND"}},"discount_allocations":[{"amount":"5","discount_application_index":0,"amount_set":{"shop_money":{"amount":"5","currency_code":"VND"},"presentment_money":{"amount":"5","currency_code":"VND"}}}],"duties":[],"admin_graphql_api_id":"gid://shopify/LineItem/976318377106520349","tax_lines":[]}],"fulfillments":[],"refunds":[],"total_tip_received":"0.0","original_total_duties_set":null,"current_total_duties_set":null,"admin_graphql_api_id":"gid://shopify/Order/820982911946154508","shipping_lines":[{"id":271878346596884000,"title":"Generic Shipping","price":"10","code":null,"source":"shopify","phone":null,"requested_fulfillment_service_id":null,"delivery_category":null,"carrier_identifier":null,"discounted_price":"10","price_set":{"shop_money":{"amount":"10","currency_code":"VND"},"presentment_money":{"amount":"10","currency_code":"VND"}},"discounted_price_set":{"shop_money":{"amount":"10","currency_code":"VND"},"presentment_money":{"amount":"10","currency_code":"VND"}},"discount_allocations":[],"tax_lines":[]}],"billing_address":{"first_name":"Bob","address1":"123 Billing Street","phone":"555-555-BILL","city":"Billtown","zip":"K2P0B0","province":"Kentucky","country":"United States","last_name":"Biller","address2":null,"company":"My Company","latitude":null,"longitude":null,"name":"Bob Biller","country_code":"US","province_code":"KY"},"shipping_address":{"first_name":"Steve","address1":"123 Shipping Street","phone":"555-555-SHIP","city":"Shippington","zip":"40003","province":"Kentucky","country":"United States","last_name":"Shipper","address2":null,"company":"Shipping Company","latitude":null,"longitude":null,"name":"Steve Shipper","country_code":"US","province_code":"KY"},"customer":{"id":115310627314723950,"email":"john@test.com","accepts_marketing":false,"created_at":null,"updated_at":null,"first_name":"John","last_name":"Smith","orders_count":0,"state":"disabled","total_spent":"0.00","last_order_id":null,"note":null,"verified_email":true,"multipass_identifier":null,"tax_exempt":false,"phone":null,"tags":"","last_order_name":null,"currency":"VND","accepts_marketing_updated_at":null,"marketing_opt_in_level":null,"admin_graphql_api_id":"gid://shopify/Customer/115310627314723954","default_address":{"id":715243470612851200,"customer_id":115310627314723950,"first_name":null,"last_name":null,"company":null,"address1":"123 Elm St.","address2":null,"city":"Ottawa","province":"Ontario","country":"Canada","zip":"K2H7A8","phone":"123-123-1234","name":"","province_code":"ON","country_code":"CA","country_name":"Canada","default":true}}}
GENHASH
PfsCje6LHCUq2nOFKvxRs5+UOZ4PP+3QZHMyytEi28Q=
HMAC
NzopH6tMXh4ZUfBg5ZNY6aVgdQeNt5NjT32ebRFvX8c=

 

gianf
New Member
3 0 0

Hi @dustinvn have you found a solution ? I am having the same issue right now

thordzcode
Shopify Partner
1 0 1
theschoolofux
Shopify Partner
10 0 2

Had the same issue. Using request.rawBody instead of request.body helped:

 

import Router from "koa-router";
import koaBodyParser from "koa-bodyparser";
import crypto from "crypto";

...

koaServer.use(koaBodyParser()); 

...

koaRouter.post(
    "/webhooks/<yourwebhook>",
    verifyShopifyWebhooks,
    async (ctx) => {
      try {
        ctx.res.statusCode = 200;
      } catch (error) {
        console.log(`Failed to process webhook: ${error}`);
      }
    }
);

...

async function verifyShopifyWebhooks(ctx, next) {
  const generateHash = crypto
    .createHmac("sha256", process.env.SHOPIFY_WEBHOOKS_KEY) // that's not your Shopify API secret key, but the key under Webhooks section in your admin panel (<yourstore>.myshopify.com/admin/settings/notifications) where it says "All your webhooks will be signed with [SHOPIFY_WEBHOOKS_KEY] so you can verify their integrity
    .update(ctx.request.rawBody, "utf-8")
    .digest("base64");

  if (generateHash !== shopifyHmac) {
    ctx.throw(401, "Couldn't verify Shopify webhook HMAC");
  } else {
    console.log("Successfully verified Shopify webhook HMAC");
  }
  await next();
}
Sergei Golubev | Devigner @ The School of UX | schoolofux.com
EricWVGG
Shopify Partner
1 0 0

Hey everyone. Here's another example of a script that worked for me, cribbed together from information here and with various packages. This limits the use of the rawBody middleware to the one path (it was causing problems with my other endpoints).

Thanks everyone for your help.

 

 

 

import express from 'express'
import contentType from 'content-type'
import * as crypto from 'crypto'
import fetch from 'node-fetch'
import getRawBody from 'raw-body'
import 'dotenv/config'

const app = express()

const generateRawBody = (req, res, next) => {
  getRawBody(
    req,
    {
      length: req.headers['content-length'],
      limit: '99mb',
      encoding: contentType.parse(req).parameters.charset,
    },
    function (err, string) {
      if (err) return next(err)
      req.rawBody = string
      next()
    }
  )
}

app.post('/deploy', [generateRawBody], async (req, res, next) => {
  const rawBody = req.rawBody
  const hmac = req.get('X-Shopify-Hmac-Sha256')
  try {
    const hash = crypto
      .createHmac('sha256', process.env.SHOPIFY_WEBHOOK_VERIFY)
      .update(rawBody, 'utf8', 'hex')
      .digest('base64')
    if (hash !== hmac) {
      throw new Error('failed hash')
    }
    // A WINNER IS YOU. DO STUFF HERE.
    res.send('okay')
  } catch (err) {
    next(err)
    res.sendStatus(400)
  }
})

app.listen(process.env.API_PORT, () => {
  console.log(`Server running at: http://localhost:${process.env.API_PORT}/`)
})