如何创建匹配的HMAC值以验证.NET中的Shopify WebHook?

时间:2012-11-06 16:07:09

标签: c# .net shopify hmac sha256

我已经设置了一个端点来接收Shopify的webhook请求。

Shopify的请求包括从共享密钥和请求正文创建的HMAC标头。

我需要在服务器上计算HMAC并将其与请求标头中的值相匹配,以确保请求是真实的。

我似乎无法在.NET中创建适当的机制来创建匹配的HMAC值。

此时我的算法如下:

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);
    }

可以找到用于验证其webhook的Shopify文档HERE,但仅包含PHP和Ruby示例。

任何人都可以看到我可能做错了吗?我应该只是将整个JSON请求体作为字符串传递给此方法吗?

4 个答案:

答案 0 :(得分:7)

    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;
    }

答案 1 :(得分:2)

正如您在问题中提到的那样,您应该在方法中对整个json请求体进行哈希处理。

我的.NET不太好,但这是ruby示例的一部分,向您展示了该做什么:

post '/' do

  . . .

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

  . . .

end

你可以看到我们只是抓住请求的主体(作为一个字符串)并将其逐字地放入验证方法中。试一试,希望你能有更多的运气。

答案 2 :(得分:0)

作为上述代码的改进,您可以将其转换为具有一些小的更改的属性:

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;
        }
    }
}

然后您就可以将其像这样用于所有Webhooks:

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

答案 3 :(得分:0)

我在使用 Azure 函数 3.0 框架的 Azure 函数中遇到了这个问题,该框架在 .NET Core 3.1 上使用 C#。受上述答案和我自己的修改启发,我能够使用以下代码完成此操作。

// 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;
        }