如何实现Zoho订阅webhook哈希验证?

问题描述 投票:0回答:1

我正在尝试将服务与Zoho订阅集成在一起,并且我想确保呼叫实际上来自Zoho。为此,在Webhook中,勾选“我想保护此Webhook”,并遵循其linked page上的文档-但我很难生成匹配的哈希值。正确验证哈希的技巧是什么?

.net hash webhooks zoho
1个回答
0
投票

我设法破解了,这是我在此过程中发现的一些陷阱,以及有效的.Net实现。

Gotchas

配置Webhook时

  • 在webhook URL中,未指定端口
  • 单击保存时,该服务必须已启动并且正在运行,并且可用于来自Zoho的呼叫。这是因为Zoho在单击“保存”后立即进行了一次测试调用,并且在未通过http调用的情况下拒绝存储您的更改。这意味着您的开发或测试箱需要公开可用。对我来说,在Webhook中指定IP地址无效,我需要一个域名。无法从Internet访问开发人员计算机,最后我不得不使用动态DNS条目和路由器端口转发从家庭环境中测试集成。
  • 就我而言,使用DELETE http动词不起作用。 Zoho始终将完整的JSON正文或仅一对大括号附加到调用URL,从而使API调用失败。但是,Zoho支持人员声称此方法应该可行,并且在他们的测试环境中不会发生此问题。
  • [计算散列之前,您需要构建一个字符串以计算散列。从他们的文档中尚不清楚如何为JSON负载执行此操作(影响其“原始”和“默认有效负载”设置)。网络上一些未回答的问题询问是否需要反序列化,拼合,订购。答案是否定的,仅使用从http请求中检索到的有效负载即可。因此,组装字符串的正确方法是:获取URL查询参数(如果有的话)和表单字段(如果有的话),并按字母顺序将它们按字母升序排序,将其键和值字符串附加到字符串中,且不带引号,空格,等号迹象。在不对该字符串进行任何处理的情况下,将http呼叫内容附加到该字符串中。
  • 在散列生成中,使用UTF8Encoding。
  • 在Zoho默认有效负载和原始数据的情况下使用的Http标头:Content-Type = application / json; charset = UTF-8和X-Zoho-Webhook-Signature = $ {Generated Hash Value}。使用x-www-form-urlencoded Webhooks时:Content-Type = application / x-www-form-urlencoded和X-Zoho-Webhook-Signature = $ {Generate Hash Value}

代码

我们的验证被实现为ASP-Net筛选器,我删除了该位,以专注于哈希计算位。

    public async Task ValidateZohoCall(HttpRequest request)
    {
        var zohoCalculatedHashValue = request.Headers.GetHeaderValue("X-Zoho-Webhook-Signature");
        if (string.IsNullOrEmpty(zohoCalculatedHashValue))
        {
            throw new Exception("Webhook signature is missing.");
        }
        else
        {
            var toHash = BuildZohoStringToHash(request);
            string locallyCalculatedHashValue = GetHash(toHash);

            // Compare our value against what is in the request headers
            if (locallyCalculatedHashValue != zohoCalculatedHashValue)
                throw new Exception("Webhook signature is invalid.");
        }
    }

    public string GetRequestBody(HttpRequest request)
    {
        string requestBody = "";
        request.EnableRewind();
        using (var stream = new StreamReader(request.Body))
        {
            stream.BaseStream.Position = 0;
            requestBody = stream.ReadToEnd();
        }
        return requestBody;
    }

    /// <summary>
    /// Concatenates parts of the http request into a single string according to
    /// Zoho specifications.
    /// </summary>
    public string BuildZohoStringToHash(HttpRequest request)
    {
        StringBuilder sb = new StringBuilder();

        // Get request fields from query string and form content.
        var mergedRequestFields = new Dictionary<string, object>();
        mergedRequestFields.Add(GetItemsFromQuery(request));
        mergedRequestFields.Add(GetItemsFromForm(request));

        // Sort those fields alphabetically by key name and append to output string.
        foreach (var kv in mergedRequestFields.OrderBy(x =>
            x.Key).ToDictionary(x => x.Key, y => y.Value))
                sb.Append($"{kv.Key}{kv.Value}");

        // Default-payload and raw type messages should not be processed,
        // just appended to the end of the string.
        sb.Append(GetRequestBody(request));

        return sb.ToString();
    }

    public Dictionary<string, object> GetItemsFromQuery(HttpRequest request)
    {
        return request.Query.ToDictionary(x => x.Key, y => (object)y.Value);
    }

    public Dictionary<string, object> GetItemsFromForm(HttpRequest request)
    {
        if (!request.HasFormContentType || (request.Form == null) || !request.Form.Any())
            return new Dictionary<string, object>();

        return request.Form.ToDictionary(x => x.Key, y => (object)y.Value);
    }

    public string GetHash(string text)
    {
        var encoding = new UTF8Encoding();
        byte[] textBytes = encoding.GetBytes(text);
        byte[] keyBytes = encoding.GetBytes(_zohoWebhookSecret);
        byte[] hashBytes;

        using (HMACSHA256 hash = new HMACSHA256(keyBytes))
            hashBytes = hash.ComputeHash(textBytes);

        return BitConverter.ToString(hashBytes).Replace("-", "").ToLower();
    }
© www.soinside.com 2019 - 2024. All rights reserved.