FulfillmentService -> callback_url How are you handling authentication?

Solved

FulfillmentService -> callback_url How are you handling authentication?

Mfred
Visitor
2 0 1

Maybe I missed something, but it seems to me I cannot intuitively add basic auth or additional query string parameters to the callback url. 

 

For example - If I add a key to my callback, shopify appends the data (see: double-question mark) like so:

 

https://dev.cannonwms.com/shopify/fulfillment?_KEY=0421008445828ceb46f496700a5fa65e/fetch_stock.json?max_retries=3&shop=cannonwms-2.myshopify.com&timestamp=1658498963

 

Having shopify append like this forces me to have to parse out my _KEY for the "fetch_stock.json" before I can validate it.  Inconvenient, but it works.

 

Ultimately - I would prefer to add a basic auth context to the callback url.  However, every time I put it into the callback url in this format - it removes the authentication:

 

https://username:password@dev.cannonwms.com/shopify/fulfillment/

 

 

How are you handling authentication from the callback urls?  What am I missing here?

Accepted Solution (1)

Nicholas_P
Shopify Partner
30 3 25

This is an accepted solution.

I don't think you can in the way that you'd like to do it.

 

If the intent is to answer "what shop are we talking about, because I have multiple clients?", they send the shop query parameter (or a X-Shopify-Shop-Domain header, depending on the callback).

 

If the intent is to answer "so any idiot on the Internet can hit this callback, how do I make sure it is actually Shopify calling me?", you have to look at the X-Shopify-Hmac-Sha256 header that they send. You have to compute a HMAC256 hash using the "Shared Secret" from when you set up the app and the request payload (on fetch_stock.json, this is a GET request so there is no request body, and instead you use the query parameters sorted in alphabetical order). If they match, then you have to assume that Shopify is calling you, and if they don't match, then you should reject the request. In that sense, that's your authentication.

 

For fetch_stock.json, here's some C# code that does it that you can extrapolate to your language of choice:

 

            var requestHmac = request.Headers["X-Shopify-Hmac-Sha256"];
            var requestPayload = string.IsNullOrWhiteSpace(sku)
                ? $"max_retries={request.QueryString["max_retries"]}&shop={shop}&timestamp={request.QueryString["timestamp"]}"
                : $"max_retries={request.QueryString["max_retries"]}&shop={shop}&sku={SanitizeShopifySku(Uri.EscapeDataString(sku))}&timestamp={request.QueryString["timestamp"]}";
            var keyBytes = Encoding.UTF8.GetBytes(sharedSecret);
            var dataBytes = Encoding.UTF8.GetBytes(requestPayload);

            using (var algorithm = new HMACSHA256(keyBytes))
            {
                var hmacBytes = algorithm.ComputeHash(dataBytes);
                var computedHmac = Convert.ToBase64String(hmacBytes);

                if (computedHmac != requestHmac)
                {
                    log.Error($"The HMAC value is not correct. Expected: '{requestHmac}'. Computed: '{computedHmac}'.");

                    response.StatusCode = 403;
                    response.StatusDescription = "The HMAC value is not correct.";
                    return;
                }
            }

        private static string SanitizeShopifySku(string sku)
        {
            // Shopify does it as a plus sign
            return sku.Replace("%20", "+");
        }

 

I hope that helps.

View solution in original post

Replies 3 (3)

Nicholas_P
Shopify Partner
30 3 25

This is an accepted solution.

I don't think you can in the way that you'd like to do it.

 

If the intent is to answer "what shop are we talking about, because I have multiple clients?", they send the shop query parameter (or a X-Shopify-Shop-Domain header, depending on the callback).

 

If the intent is to answer "so any idiot on the Internet can hit this callback, how do I make sure it is actually Shopify calling me?", you have to look at the X-Shopify-Hmac-Sha256 header that they send. You have to compute a HMAC256 hash using the "Shared Secret" from when you set up the app and the request payload (on fetch_stock.json, this is a GET request so there is no request body, and instead you use the query parameters sorted in alphabetical order). If they match, then you have to assume that Shopify is calling you, and if they don't match, then you should reject the request. In that sense, that's your authentication.

 

For fetch_stock.json, here's some C# code that does it that you can extrapolate to your language of choice:

 

            var requestHmac = request.Headers["X-Shopify-Hmac-Sha256"];
            var requestPayload = string.IsNullOrWhiteSpace(sku)
                ? $"max_retries={request.QueryString["max_retries"]}&shop={shop}&timestamp={request.QueryString["timestamp"]}"
                : $"max_retries={request.QueryString["max_retries"]}&shop={shop}&sku={SanitizeShopifySku(Uri.EscapeDataString(sku))}&timestamp={request.QueryString["timestamp"]}";
            var keyBytes = Encoding.UTF8.GetBytes(sharedSecret);
            var dataBytes = Encoding.UTF8.GetBytes(requestPayload);

            using (var algorithm = new HMACSHA256(keyBytes))
            {
                var hmacBytes = algorithm.ComputeHash(dataBytes);
                var computedHmac = Convert.ToBase64String(hmacBytes);

                if (computedHmac != requestHmac)
                {
                    log.Error($"The HMAC value is not correct. Expected: '{requestHmac}'. Computed: '{computedHmac}'.");

                    response.StatusCode = 403;
                    response.StatusDescription = "The HMAC value is not correct.";
                    return;
                }
            }

        private static string SanitizeShopifySku(string sku)
        {
            // Shopify does it as a plus sign
            return sku.Replace("%20", "+");
        }

 

I hope that helps.

Mfred
Visitor
2 0 1

AH HMAC - that's right.  Thank you, this should work.  I was hoping to make it more compatible with the API system on the other side by making it pass a key, or auth to me in some fashion.  

PavelKutsenko
Shopify Partner
2 0 0

In case it will help someone. In Node JS and express the fetch_stock verification looks like this.

const hmac = req.headers['x-shopify-hmac-sha256'];

const formattedQueryString = new URLSearchParams(req.query)
.toString()
.split('&')
.map((x) => queryString.unescape(x))
.sort()
.join('&');

const hash = computeSignature(formattedQueryString, process.env.SHOPIFY_API_SECRET!);

if (hash !== hmac) {
logger.error(createHttpError.BadRequest("Couldn't verify incoming Callback request!"));
return res.status(StatusCodes.OK).send();
}

 

import crypto from 'crypto';

export const computeSignature = (data: string, secret: string) => {
return crypto.createHmac('sha256', secret).update(data, 'utf8').digest('base64');
};