Join us for an upcoming Shopify Partner webinar on February 27, 2024. Discover the latest Checkout Extensibility features, and deep dive on improvements to Shopify Functions and Web Pixels. Register now for either the 10am EST or 2pm EST sessions.

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 😕