Node, body-parser and Shopify webhooks verification

Introduction: Node + express + body-parser + Shopify?

Are you using Node/express/body-parser and Shopify, and (you would like to use) its webhooks?

If the answer is YES to the questions above, I am going to suggest here a way to solve a common problem.

NOTE: This and much more is covered in the Coding Shopify webhooks and API usage: a step-by-step guide class on Skillshare!

The problem

Body-parser does an amazing work for us: it parses incoming request bodies, and we can then easily use the req.body property according to the result of this operation.

But, there are some cases in which we need to access the raw body in order to perform some specific operations.

Verifying Shopify webhooks is one of these cases.

Shopify: verifying webhooks

Shopify states that:

Webhooks created through the API by a Shopify App are verified by calculating a digital signature. Each webhook request includes a base64-encoded X-Shopify-Hmac-SHA256 header, which is generated using the app’s shared secret along with the data sent in the request.

We will not go into all the details of the process: for example, Shopify uses two different secrets to generate the digital signature: one for the webhooks created in the Admin –> Notifications interface, and one for the ones created through APIs. Anyway, only the key is different but the process is the same: so, I will assume that we have created our webhooks all in the same way, using JSON as object format.

A function to verify a webhook

We will now code a simple function that we will use to verify the webhook.

The first function’s param is the HMAC from the request headers, the second one is the raw body of the request.

function verify_webhook(hmac, rawBody) {
    // Retrieving the key
    const key = process.env.SHOPIFY_WEBHOOK_VERIFICATION_KEY;
    /* Compare the computed HMAC digest based on the shared secret 
     * and the request contents
    */
    const hash = crypto
          .createHmac('sha256', key)
          .update(rawBody, 'utf8', 'hex')
          .digest('base64');
    return(hmac === hash);
}

We use the HMAC retrieved from the request headers (X-Shopify-Hmac-Sha256), we retrieve the key stored in the .env file and loaded with the dotenv module, we compute the HMAC digest according to the algorithm specified in Shopify documentation, and we compare them.

We use the crypto module in order use some specific functions that we need to compute our HMAC digest.

Crypto is a module that:

provides cryptographic functionality that includes a set of wrappers for OpenSSL’s hash, HMAC, cipher, decipher, sign, and verify functions.

NOTE: it’s now a built-in Node module.

Retrieving the raw body

You have probably a line like this one, with or without customization, in your code:

// Express app
const app = express();
app.use(bodyParser.json());

 

So your req.body property contains a parsed object representing the request body.

We need to find a way to “look inside the middleware” and to add somehow the information regarding the request status, using our just-defined function: is it verified or not?

Now, we know that the json() function of body-parser accepts an optional options object, and we are very interesting to one of the possible options: verify.

The verify option, if supplied, is called as verify(req, res, buf, encoding), where buf is a Buffer of the raw request body and encoding is the encoding of the request. The parsing can be aborted by throwing an error.

That’s it, we are going to use it like this.

Let’s change the json() call in this way:

// Express app
const app = express();
app.use(bodyParser.json({verify: verify_webhook_request}));

And we create the function verify_webhook_request() according to the signature documented before:

function verify_webhook_request(req, res, buf, encoding) {
  if (buf && buf.length) {
    const rawBody = buf.toString(encoding || 'utf8');
    const hmac = req.get('X-Shopify-Hmac-Sha256');
    req.custom_shopify_verified = verify_webhook(hmac, rawBody);
  } else {
    req.custom_shopify_verified = false;
  }
}

Basically, we check if the buffer is empty: in such case, we consider the message not verified.

Otherwise, we retrieve the raw body using the toString() method of the buf object using the passed encoding (default to UTF-8).

We retrieve the X-Shopify-Hmac-Sha256.

After we have computed the verify_webhook() function, we store its value in a custom property of the req object.

Now, in our webhook code we can check req.custom_shopify_verified in order to be sure that the request is verified. If this is the case, we can go on with our code using the req.body object as usual!

Another idea could be to stop the parsing process: we could do this throwing an error during the verify_webhook_request function.

 

Conclusion

Please leave your comment if you want to share other ways to accomplish the same task, or generically your opinion.

 

NOTE: Remember that this and much more is covered in the Coding Shopify webhooks and API usage: a step-by-step guide class on Skillshare!

  1. Stefan says:

    Lorenzo,

    Thank you for your explanation and example! I am glad that I found your post!

    Groet,

    Stefan

  2. Orlando says:

    I have a problem with my app. I am developing an app in node.js until yesterday everything worked correctly, today it does not let you authenticate … according to what we find is that the hmac that shopify sends, with which it is built from the store url + timestamp + secret-key. Please help…

    This is my code, functional until yesterday

    signIn(
    hmac: string,
    shop: string,
    timestamp: string,
    code?: string,
    ): Promise {
    return new Promise(
    (
    resolve: (result: LoginUserDto) => void,
    reject: (reason: ErrorResult) => void,
    ): void => {
    this.userRepository.getUserByEmail(shop).then((user: User) => {
    if (!user) {
    let userDto: CreateUserDto = {
    shopUrl: shop,
    };
    let loginUserDto: LoginUserDto = {
    newUser: true,
    redirect: ”,
    };
    const state = nonce();
    const redirectUrl = redirectAddress;
    const installUrl =
    ‘https://’ +
    shop +
    ‘/admin/oauth/authorize?client_id=’ +
    apiKey +
    ‘&scope=’ +
    scopes +
    ‘&state=’ +
    state +
    ‘&redirect_uri=’ +
    redirectUrl;
    loginUserDto.redirect = installUrl;
    resolve(loginUserDto);
    } else {
    if (shop && hmac) {
    let loginUserDto: LoginUserDto = user;
    //Validate request is from Shopify
    let query: any = {
    shop: shop,
    timestamp: timestamp,
    };
    const map = Object.assign({}, query);
    const message = querystring.stringify(map);
    const providedHmac = Buffer.from(hmac, ‘utf-8’);
    const generatedHash = Buffer.from(
    crypto
    .createHmac(‘sha256’, apiSecret)
    .update(message)
    .digest(‘hex’),
    ‘utf-8’,
    );
    let hashEquals = false;

    try {
    hashEquals = crypto.timingSafeEqual(
    generatedHash,
    providedHmac,
    );
    } catch (e) {
    hashEquals = false;
    }

    if (!hashEquals) {
    console.log(‘hmac failed’);
    let loginUserDto: LoginUserDto = user;
    loginUserDto.newUser = false;
    loginUserDto.hmac = false;
    loginUserDto.redirect =
    ‘https://’ + shop + ‘/admin’;
    resolve(loginUserDto);
    } else {
    let loginUserDto: LoginUserDto = user;
    loginUserDto.newUser = false;
    loginUserDto.hmac = true;
    resolve(loginUserDto);
    }

    const accessTokenRequestUrl =
    ‘https://’ + shop + ‘/admin/oauth/access_token’;
    const accessTokenPayload = {
    client_id: apiKey,
    client_secret: apiSecret,
    code,
    };
    } else {
    let loginUserDto: LoginUserDto = user;
    loginUserDto.newUser = false;
    loginUserDto.hmac = false;
    loginUserDto.redirect =
    ‘https://’ + shop + ‘/admin/apps’;
    resolve(loginUserDto);
    }
    resolve(user);
    }
    }); /*.catch((error) => {
    reject(new InternalServerErrorResult(ErrorCode.GeneralError, error));
    });*/
    },
    );
    }

Leave a Comment

Your email address will not be published. Required fields are marked *