Webhook Validation with Node / Express.js

Nico4
Shopify Expert
25 0 10

Having a hard time validating the Webhook with node js (using express.js)

My middleware before handling the request with a typical express router.

app.use('/webhook', 
	bodyParser.json({
		limit: '50mb',
		verify: function(req, res, buf) {
			req.rawbody = buf;
		}
	}), 
routes.webhook
);

The payload seems to be received properly with valid headers and payload, But something might be wrong in the validation code.


const message = JSON.stringify(req.rawbody);
const digest = crypto.createHmac('SHA256', config.getShopifySecretKey())
	.update(message)
	.digest('base64');
	
return callback(digest === req.headers['X-Shopify-Hmac-Sha256']);

Any help would be greatly appreciated!

Replies 17 (17)

Nico4
Shopify Expert
25 0 10

Anyone?

Jonathan_H
Shopify Partner
15 0 5

I'm also having this issue, my latest search is this gist

https://gist.github.com/andjosh/5c4f0244914adfd312e4

If I'm reading it right is says to use bodyParser to read the body as text rather than application/json -I guess that is what you are doing with the rawbody read???

Jamie_D_
Shopify Staff (Retired)
533 1 92

You can find a working implementation here, in shopify-express

To learn more visit the Shopify Help Center or the Community Blog.

Jonathan_H
Shopify Partner
15 0 5

I'm a bit lost with this - I'vetried getting the hmac using the req body in a node session using the commands as outlined but can't figure out how the req body gets converted to rawdata?

I also tried using https://www.npmjs.com/package/express-shopify-webhooks but it just keeps giving me 403 errors

Is there a document showing how shopify generates the hmac? why is is done differently from the OAuth hmac ? 

Jonathan_H
Shopify Partner
15 0 5

I got it working for my app. Here is a summary of the actions I took:

In my main app.js I add the raw body to the req with bodyparser.json for any route that starts with '/webhooks'

app.use(bodyParser.json({
  type:'*/*',
  limit: '50mb',
  verify: function(req, res, buf) {
      if (req.url.startsWith('/webhooks')){
        req.rawbody = buf;
      }
  }
 })
);


then in my routes file I'm adding a validation function to the webhooks/app/uninstalled route:

router.post('/webhooks/app/uninstalled',validateWebhook, appcontroller.uninstalled);

This is the function that performs the validation, if the calculated hmac matches what is sent in the header then the next() function is called, otherwise send a forbidden status 403 

function validateWebhook (req,res,next){
    generated_hash = crypto
        .createHmac('sha256', config.SHOPIFY_SHARED_SECRET)
        .update(Buffer.from(req.rawbody))
        .digest('base64');
    if (generated_hash == req.headers['x-shopify-hmac-sha256']) {
        next()
    } else {
        res.sendStatus(403)
    }
}

 

mariojuano
Shopify Partner
6 0 0

thank you @Jonathan_H  your code works perfect!

jgok
Visitor
2 0 0

Do you have this working with HapiJs ? I'm stucked for the last two days with it. Here's what I have now:

  newOrder: {
    payload: {
      output: 'data',
      parse: false
    },
    pre: [
      {
        method: (request, reply) => {
          const hmac = request.headers['x-shopify-hmac-sha256'];
          let generatedHash = crypto.createHmac('sha256', utils.SHOPIFY_API_SECRET)
            .update(request.payload.toString())
            .digest('base64');

          if (generatedHash == request.headers['x-shopify-hmac-sha256']) {
            console.log("VALIDATED")
          } else {
            console.log("ALWAYS ENTERS HERE")
          }
        }
      }
    ],
    handler: function(request, reply) {
      reply().code(200);
    },
    auth: false,
    notes: 'Shopifys new order webhook',
    tags: ['api'],
    id: 'newOrder'
  }

HapiJs version is 14.0.0

Priti
Tourist
6 0 1

doesn't work ! 

const rawBody = await getRawBody(request); return { BadRequestError: request aborted...

what is shopify requirement  with request.body ??? Can you share a sample request 

VinayIndoria
New Member
4 0 0

Thanks
Jonathan H your code worked for me!

wrs-alex
Shopify Partner
4 0 0

Hello there, how did you get it to work? 

 

I am using the below atm. But couldn't get it to work. 


 
let jsonString = JSON.stringify(req.body);

const generated_hash = crypto
.createHmac('sha256', secret)
.update(jsonString)
.digest('base64');
 
jgok
Visitor
2 0 0

No luck yet with this issue. Tried this suggested solution but didn't work either.

 

 

ken_smas
Tourist
4 0 1

I found a solution that works, given you can get the raw body buffer from the request.

In express & bodyparser you can do that with this piece of code:

app.use(
  bodyParser.json({
    verify: (req, res, buf) => {
      req.rawBody = buf;
    },
  })
);

Then generate the hmac signature and compare it by converting the raw bodybuffer to a string, signing it, and finaly comparing the output to the header.

const verify = async (hmacHeader, rawBody) => {
  if (!hmacHeader || !rawBody) {
    return false;
  }

  const data = rawBody.toString('utf8');
  const hmac = await crypto
    .createHmac('sha256', VERIFICATION_KEY)
    .update(data)
    .digest('base64');

  // safe comparator.
  let valid = true;
  hmacHeader.split('').forEach((element, index) => {
    if (element !== hmac[index]) {
      valid = false;
    }
  });
  return valid;
};
itsgookit
Shopify Partner
13 0 4

My problem is that I'm using the wrong key.

Using webhook secret key instead of using Shopify API secret key lol

krumholz
Shopify Partner
6 0 1

@ken_smas or @itsgookit, have either of you posted your solutions on Github or would you mind sharing them here? I'm having trouble with getting the hmac's to be the same. I'm partially wondering if one of my node packages is causing an issue.

ken_smas
Tourist
4 0 1

@krumholz   Make certain that you are using the correct key first and foremost. you get the correct key by using the key in the "shared secret" field in a private app.

secondly, the hmac signature has to be done with the raw request body, and not the parsed json data. any json parsing will replace the body, making validation imposible.

The code i use is the same shown in my last post.

here is a list of npm packages invoked on the request before the point of verification:

express,
bodyParser,
cors,
crypto

verification functions:

const verify = async (hmacHeader, rawBody) => {
  if (!hmacHeader || !rawBody) {
    return false;
  }

  const data = rawBody.toString('utf8');
  const hmac = await crypto
    .createHmac('sha256', VERIFICATION_KEY)
    .update(data)
    .digest('base64');

// safe comparator.
let valid = true;
hmacHeader.split('').forEach((element, index) => {
  if (element !== hmac[index]) {
    valid = false;
    }
  });
  return valid;
};
 
And here is the invocation of the function:
 
const verified = await verify(
  req.headers['x-shopify-hmac-sha256'],
  req.rawBody    // takes the raw body extracted before bodyparsing
);
 
Saving the raw body, must be done before any other bodyparser type function. so put this right after cors, but before everything else.
app.use(
  bodyParser.json({
    verify: (req, res, buf) => {
    req.rawBody = buf; 
    },
  })
);
 
hope it helps.
Lixon_Louis
Shopify Partner
1193 35 267

Thanks to @ken_smas . I finally made it work.  Is async/await is a best practice here?

const express = require("express")
const bodyParser = require("body-parser")
const crypto = require("crypto")
const secretKey  = "xxxxx"
const app = express()
const PORT = 3000

 
 app.use(
  bodyParser.json({
    verify: (req, res, buf) => {
      req.rawBody = buf;
    },
  })
);
 
app.post("/webhook", (req, res) => {
 
  const data = req.rawBody
  const hmacHeader  = req.get('X-Shopify-Hmac-Sha256')

  const hmac = crypto
    .createHmac('sha256', secretKey)
    .update(data, 'utf8', 'hex')
    .digest('base64');

    if (hmacHeader === hmac) {
      console.log('Phew, it came from Shopify!')
      res.sendStatus(200)
      console.log(req.body)

      }else{
      console.log('Danger! Not from Shopify!')
      res.sendStatus(403)
    }

})

app.listen(PORT, () => console.log(` Server running on port ${PORT}`))

 

Lixon_Louis
Shopify Partner
1193 35 267

updated the code based on https://github.com/Shopify/shopify-express/blob/master/middleware/webhooks.js

app.post("/webhook", async(req, res) => {
 
  const data = await req.rawBody
  const hmacHeader  = await req.get('X-Shopify-Hmac-Sha256')

  try{
    const hmac = crypto
    .createHmac('sha256', secretKey)
    .update(data, 'utf8', 'hex')
    .digest('base64');

      if (hmacHeader === hmac) {
        console.log('Phew, it came from Shopify!')
        res.sendStatus(200)
        console.log(req.body)

      }else{
        console.log('Danger! Not from Shopify!')
        res.sendStatus(403)
      }
  }catch(error){
    console.log(error)
  }
   
})