4
votes

I have set up an endpoint to receive webhook requests from Shopify.

The requests from Shopify include an HMAC header that is created from a shared secret key and the body of the request.

I need to calculate the HMAC on my server and match it to the value in the request header to ensure that the request is authentic.

I can't seem to create the appropriate mechanism in .NET to create a matching HMAC value.

My algorithm at this point is as follows:

public static string CreateHash(string data)
    {
        string sharedSecretKey = "MY_KEY";

        byte[] keyBytes = Encoding.UTF8.GetBytes(sharedSecretKey);
        byte[] dataBytes = Encoding.UTF8.GetBytes(data);

        //use the SHA256Managed Class to compute the hash
        System.Security.Cryptography.HMACSHA256 hmac = new HMACSHA256(keyBytes);
        byte[] hmacBytes = hmac.ComputeHash(dataBytes);

        //retun as base64 string. Compared with the signature passed in the header of the post request from Shopify. If they match, the call is verified.
        return System.Convert.ToBase64String(hmacBytes);
    }

The Shopify docs for verifying their webhooks can be found HERE but only PHP and Ruby samples are included.

Can anyone see what I might be doing wrong? Should I be just passing the entire JSON request body as a string into this method?

4

4 Answers

2
votes

As you allude to in your question, you should be hashing the entire json request body in your method.

My .NET isn't too good, but Here's the part of the ruby example that shows you what to do:

post '/' do

  . . .

  data = request.body.read
  verified = verify_webhook(data, env["HTTP_X_SHOPIFY_HMAC_SHA256"])

  . . .

end

You can see that we're just grabbing the body of the request (as a string) and throwing it into the verify method verbatim. Give it a try and hopefully you'll have more luck.

7
votes
    private static bool Validate(string sharedSecretKey)
    {
        var data = GetStreamAsText(HttpContext.Current.Request.InputStream, HttpContext.Current.Request.ContentEncoding);
        var keyBytes = Encoding.UTF8.GetBytes(sharedSecretKey);
        var dataBytes = Encoding.UTF8.GetBytes(data);

        //use the SHA256Managed Class to compute the hash
        var hmac = new HMACSHA256(keyBytes);
        var hmacBytes = hmac.ComputeHash(dataBytes);

        //retun as base64 string. Compared with the signature passed in the header of the post request from Shopify. If they match, the call is verified.
        var hmacHeader = HttpContext.Current.Request.Headers["x-shopify-hmac-sha256"];
        var createSignature = Convert.ToBase64String(hmacBytes);
        return hmacHeader == createSignature;
    }

    private static string GetStreamAsText(Stream stream, Encoding encoding)
    {
        var bytesToGet = stream.Length;
        var input = new byte[bytesToGet];
        stream.Read(input, 0, (int)bytesToGet);
        stream.Seek(0, SeekOrigin.Begin); // reset stream so that normal ASP.NET processing can read data
        var text = encoding.GetString(input);
        return text;
    }
2
votes

As an improvement to the above code, you can convert it to an attribute with a few minor changes:

public class VerifyShopifyAttribute : ActionFilterAttribute
{
    private readonly string sharedSecret = "abc";

    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        if (!ValidateHash(actionContext))
        {
            // reject the request with a 400 error
            var response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, "Bad Request");
            actionContext.Response = response;
        }
    }

    private bool ValidateHash(HttpActionContext actionContext)
    {
        var context = (HttpContextBase)actionContext.Request.Properties["MS_HttpContext"];
        context.Request.InputStream.Seek(0, SeekOrigin.Begin);

        using (var stream = new MemoryStream())
        {
            context.Request.InputStream.CopyTo(stream);
            string requestBody = Encoding.UTF8.GetString(stream.ToArray());

            var keyBytes = Encoding.UTF8.GetBytes(sharedSecret);
            var dataBytes = Encoding.UTF8.GetBytes(requestBody);

            //use the SHA256Managed Class to compute the hash
            var hmac = new HMACSHA256(keyBytes);
            var hmacBytes = hmac.ComputeHash(dataBytes);

            //retun as base64 string. Compared with the signature passed in the header of the post request from Shopify. If they match, the call is verified.
            var hmacHeader = HttpContext.Current.Request.Headers["x-shopify-hmac-sha256"];
            var createSignature = Convert.ToBase64String(hmacBytes);
            return hmacHeader == createSignature;
        }
    }
}

And then you can use it like so for all of your webhooks:

[RoutePrefix("api")]
public class ShopifyWebHookController : ApiController
{
    [VerifyShopify]
    [HttpPost]
    public IHttpActionResult HandleWebhook(...)
    {
        ...
    }
}
0
votes

I was having this issue in an Azure Function using Azure functions 3.0 framework using C# on .NET Core 3.1. I was able to accomplish this using the following code, inspired by the above answers and my own tinkering.

// replace the appsettings variable with however you get your pre-shared signature
// req is passed in after being injected through the Azure Functions framework
private static async Task<bool> ValidateShopifySignature(HttpRequest req, AppSettings appSettings)
        {
            var hmacHeader = req.Headers["X-Shopify-Hmac-Sha256"];
           
            var sharedSignatureBytes = Encoding.UTF8.GetBytes(appSettings.ShopifyWebhookSignature);
            using var hmac = new HMACSHA256(sharedSignatureBytes);

            //copy the request body to a memory stream then convert it to a byte[]
            using MemoryStream dataStream = new();
            await req.Body.CopyToAsync(dataStream);
            var dataBytes = dataStream.ToArray();
            
            //compute a hash of the body based on the signature 
            var generatedHmacHashBytes = hmac.ComputeHash(dataBytes);
            var generatedSignature = Convert.ToBase64String(generatedHmacHashBytes);
          
            //compare that signature to the one that Shopify generated and sent over
            return hmacHeader == generatedSignature;
        }