Reputation: 1443
I'm trying to integrate a service with Zoho subscriptions, and I want to make sure that the call actually comes from Zoho. To do that, in the webhook, I tick "I want to secure this webhook" and follow the documentation on their linked page - but I struggle to generate matching hash values. What are the tricks of correctly verifying the hash?
Upvotes: 0
Views: 484
Reputation: 1443
I managed to crack it, here are some gotcha's I found in the process, and a working .Net implementation.
Gotchas
When configuring the webhook
Code
Our validation was implemented as an ASP-Net filter, I removed that bit to concentrate on the hash calculation bit.
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();
}
Upvotes: 1