skip to Main Content

I followed exactly the code from the shopify docs (Shopify app with Node and Express) for integrating app with express but it seems that I still hitting HMAC Validation Failed.

const map = Object.assign({}, req.query);
delete map['signature'];
delete map['hmac'];
const message = querystring.stringify(map);
const providedHmac = Buffer.from(hmac, 'utf-8');
const generatedHash = Buffer.from(
  crypto
    .createHmac('sha256',this.configService.get('SHOPIFY_API_SECRET'))
    .update(message)
    .digest('hex'),
  'utf-8'
);
let hashEquals = false;
// timingSafeEqual will prevent any timing attacks. Arguments must be buffers
try {
  hashEquals = crypto.timingSafeEqual(generatedHash, providedHmac)
  // timingSafeEqual will return an error if the input buffers are not the same length.
} catch (e) {
  hashEquals = false;
};

if (!hashEquals) {
  return res.status(400).send('HMAC validation failed');
}

I’m expecting the code above to work and do not return the error.

2

Answers


  1. I had the same problem and I think it might be related how the nonce is created that is used for the state parameter, if we leave this blank when redirecting the verification is successful. From the beginning I created a nonce like this:

    const nonce: string = crypto.randomBytes(16).toString('base64');
    

    And then if I used this nonce in the state parameter the hmac verification in the callback always failed, when I changed it to:

    const nonce: string = crypto.randomBytes(16).toString('hex');
    

    everything is working as expected:

    @Get('auth/callback')
    callback(@Request() req) {
      // req.state should be your previously created nonce
      const { hmac, ...params } = req;
      if (this.validateHmac(hmac, params)) {
        // Validation success
        return 'OK';
      }
    }
    
    private validateHmac(hmac: string, data: string): boolean {
      const hash: string = Buffer.from(
        crypto.createHmac('SHA256', this.appSecret)
          .update(Buffer.from(data, 'utf8'))
          .digest('hex'),
        'utf-8'
      );
      return crypto.timingSafeEqual(hash, Buffer.from(hmac, 'utf-8'));
    }
    
    Login or Signup to reply.
  2. This should work for your Express application.

      // Packages should be downloaded - npm i query-string crypto -S
      import queryString from "query-string";
      import crypto from "crypto";
      
      ....
    
      const queryObj = Object.assign({}, req.query);
      const { signature: _ signature, hmac, ...map } = queryObj;
    
      const orderedMap = Object.keys(map)
        .sort((value1, value2) => value1.localeCompare(value2))
        .reduce((accum: any, key: string) => {
          accum[key] = map[key];
          return accum;
        }, {});
    
      const message = queryString.stringify(orderedMap);
      const generatedHash = crypto.createHmac("sha256", SHOPIFY_CLIENT_SECRET).update(message).digest("hex");
    
      // Safe Compare
      const aLen = Buffer.byteLength(generatedHash);
      const bLen = Buffer.byteLength(hmac);
    
      if (aLen !== bLen) {
        return res.status(400).send('HMAC validation failed');
      }
    
      // Turn strings into buffers with equal length
      // to avoid leaking the length
      const buffA = Buffer.alloc(aLen, 0, "utf8");
      buffA.write(stringA);
      const buffB = Buffer.alloc(bLen, 0, "utf8");
      buffB.write(stringB);
    
      const valid = crypto.timingSafeEqual(buffA, buffB);
      if(!valid) {
          return res.status(400).send('HMAC validation failed');
      }
    
      ....
    
    

    Based on koa repository.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search