Topics covering webhook creation & management, event handling, Pub/Sub, and Eventbridge, in Shopify apps.
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:
/**
* @Anonymous string $rawBody
* @Anonymous 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!
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×tamp=1652271953
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
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:
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
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 😕