HMAC validation fails when request body is empty

DK_andy
Shopify Partner
1 0 2

Hello,

we are developing a fulfillment app and we have some problems with the HMAC validation.

We implemented the needed endpoints as described here:
https://shopify.dev/api/admin-rest/2021-10/resources/fulfillmentservice#top
and we validate the request as described here:
https://shopify.dev/apps/webhooks/configuration/https#step-5-verify-the-webhook

The validation suceeds as it should if there is data in the incoming request body but if the request body is empty (e.g. GET fetch_stock), the verification always fails.

 

Our validation function looks like this:

 

/**
 * @param string $rawBody
 * @param string $hmac
 * @Return bool return true if callback could be verified
 * @throws UninitializedContextException
 */
public static function validateCallbackHmac(string $rawBody, string $hmac): bool
{
    Context::throwIfUninitialized();

    if (!hash_equals($hmac, base64_encode(hash_hmac('sha256', $rawBody, Context::$API_SECRET_KEY, true)))) {
        throw new InvalidCallbackException('Could not validate callback HMAC');
    }
    return true;
}

 

 

Thank you in advance!

Replies 4 (4)

m8th
Shopify Partner
13 5 11

You need to use the query string instead of body.

 

example with fetch_stock.json request:

<?php

echo $_SERVER['QUERY_STRING'];
// max_retries=3&shop=hkdev.myshopify.com&timestamp=1652271953
RTFM!

hauraki
Shopify Partner
3 0 0

Were you able to solve this? We are also facing the question how to verify requests to callback_url/fetch_stock, it's always failing at the moment. We are extracting the relevant query parameters, sort them, and calculate the digest - no luck so far

 

 

def verify(hmac, params)
   params = params.slice(:max_retries, :shop, :sku, :timestamp)
   
   digest = OpenSSL::HMAC.hexdigest(
      OpenSSL::Digest.new("sha256"),
      SHOPIFY_API_SECRET_KEY,
      CGI.unescape(params.sort.to_h.to_query)
   )
   
   ActiveSupport::SecurityUtils.secure_compare(hmac, digest)
end

 

  

m8th
Shopify Partner
13 5 11

hi hauraki

 

i'm not familar with ruby; in the example app from shopify i found the part with hmac validation, maybe it helps for you:

 

reference: https://github.com/Shopify/example-ruby-app/blob/67a0decc5eb550f3a9228eda53925c3afd40dfe9/02%20Charg...

    def validate_hmac(hmac,request)
      h = request.params.reject{|k,_| k == 'hmac' || k == 'signature'}
      query = URI.escape(h.sort.collect{|k,v| "#{k}=#{v}"}.join('&'))
      digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), API_SECRET, query)

      unless (hmac == digest)
        return [403, "Authentication failed. Digest provided was: #{digest}"]
      end
    end

 

RTFM!
hauraki
Shopify Partner
3 0 0

Hi M8th, thanks for the code snippet. What we are doing is very similar, except that your example escapes the query string before digesting, while we unescape it. I tried escaping, but that also does not seem to solve the problem 😕