Max Schmitt

July 16 2021

Node.js: Validating Shopify Webhooks with Express

When integrating with a 3rd party via webhooks, you always want to make sure that incoming requests are actually coming the service you're integrating with.

Otherwise bad actors can impersonate e.g. Shopify and spam your system with useless or fraudulent data.

We can validate that incoming webhooks are from Shopify by checking the signature of their requests. To do this, we first need to grab Shopify's signature secret.

Finding the Right Shopify Signature Secret

There are two places where you might get the signature secret from:

For Stores: Notification Settings

If you set up your webhooks using Shopify's UI for a single store, then you should use the signature secret displayed at the bottom of your store's notification settings:

Screenshot of the webhook signature secret in Shopify notification settings

For Apps: App Settings

If your webhooks are for a Shopify app that is used by multiple stores, you will need to use the "Shared Secret" from your app's settings page:

Screenshot of the shared secret in Shopify app settings

Creating an Express Middleware for Validating Incoming Shopify Webhooks

The following Express middleware can be used for incoming webhooks from Shopify and validate that they were actually made by Shopify.

It assumes that your signature secret is stored in an environment variable called SHOPIFY_SIGNATURE_SECRET.

JS

const crypto = require('crypto')
const SHOPIFY_SIGNATURE_SECRET = process.env.SHOPIFY_SIGNATURE_SECRET
if (!SHOPIFY_SIGNATURE_SECRET) {
throw new Error('Please provide process.env.SHOPIFY_SIGNATURE_SECRET')
}
function validateShopifySignature() {
return async (req, res, next) => {
try {
const rawBody = req.rawBody
if (typeof rawBody == 'undefined') {
throw new Error(
'validateShopifySignature: req.rawBody is undefined. Please make sure the raw request body is available as req.rawBody.'
)
}
const hmac = req.headers['x-shopify-hmac-sha256']
const hash = crypto.createHmac('sha256', SHOPIFY_SIGNATURE_SECRET).update(rawBody).digest('base64')
const signatureOk = crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(hmac))
if (!signatureOk) {
res.status(403)
res.send('Unauthorized')
return
}
next()
} catch (err) {
next(err)
}
}
}

In order to get this middleware to work, there is one last step required:

Exposing req.rawBody

The middleware above accesses req.rawBody which doesn't exist on Express' request-object by default.

Most Express APIs have the following line of code (or similar) somewhere:

JS

app.use(express.json({ limit: '50mb' }))

We need to modify so that it keeps the raw HTTP request body around:

JS

app.use(
express.json({
limit: '50mb',
verify: (req, res, buf) => {
req.rawBody = buf
},
})
)

That's it!

You can now use the middleware introduced in this article to validate incoming Shopify webhooks. This way you can be sure that those requests actually come from Shopify and not somebody else. :)

JS

app.post('/webhooks/shopify/product-creation', validateShopifySignature(), (req, res, next) => {
// ...
})