Search code examples
c#webhookstypeform

Unable to get Typeform Webhook Signature with C# to work


First, this question has been asked and answered here but it was specific to Ruby/PHP and whilst I have attempted to follow it and the guidance from Typeform themselves, I am unable to implement the Typeform-Signature Check in C#.

I have written an extension method to validate the Typeform Signature against the payload sent via the webhook. If the signature is valid, it returns the string (json) payload but if not it returns an error.

public static class HttpRequestExtensions {
    private const string SignatureHeader = "Typeform-Signature";
    private static readonly Encoding encoding = new UTF8Encoding ();

    public static async Task<Result<string>> ValidateAndRetrievePayload (this HttpRequestMessage request, string key) {
        var headerValue = request.GetHeaderValue (SignatureHeader);
        if (string.IsNullOrWhiteSpace (headerValue)) return Result.Failure<string> ($"'{SignatureHeader}' Header not found or empty.");

        var json = await request.Content.ReadAsStringAsync ();
        var payload = encoding.GetBytes (json);
        using (var hmac256 = new HMACSHA256 (encoding.GetBytes (key))) {
            var hashPayload = hmac256.ComputeHash (payload);
            var base64String = Convert.ToBase64String (hashPayload);
            var hashResult = $"sha256={base64String}";
            if (hashResult.Equals (headerValue)) return Result.Success (json);
            return Result.Failure<string> ($"'{SignatureHeader}' does not match. Header: `{headerValue}` | Hash: `{hashResult}`");
        }
    }
}

Based on other questions found on SO, I modified the method to run without encoding (see below) but still ended up in with the same result, the hashes are not matching.

public static class HttpRequestExtensions
{
    private const string SignatureHeader = "Typeform-Signature";

    public static async Task<Result<string>> ValidateAndRetrievePayload(this HttpRequestMessage request, string key)
    {
        var headerValue = request.GetHeaderValue(SignatureHeader);
        if (string.IsNullOrWhiteSpace(headerValue))
            return Result.Failure<string>($"'{SignatureHeader}' Header not found or empty.");

        var payload = await request.Content.ReadAsByteArrayAsync();
        var byteKey = GetBytes(key);
        using (var hmac256 = new HMACSHA256(byteKey))
        {
            var hashPayload = hmac256.ComputeHash(payload);
            var base64String = Convert.ToBase64String(hashPayload);
            var hashResult = $"sha256={base64String}";
            if (hashResult.Equals(headerValue))
                return Result.Success(await request.Content.ReadAsStringAsync());
            return Result.Failure<string>(
                $"'{SignatureHeader}' does not match. Header: `{headerValue}` | Hash: `{hashResult}`");
        }
    }

    private static byte[] GetBytes(string value)
    {
        var bytes = new byte[value.Length * sizeof(char)];
        Buffer.BlockCopy(value.ToCharArray(), 0, bytes, 0, bytes.Length);
        return bytes;
    }

    private static string GetString(byte[] bytes)
    {
        var chars = new char[bytes.Length / sizeof(char)];
        Buffer.BlockCopy(bytes, 0, chars, 0, bytes.Length);
        return new string(chars);
    }
}

Solution

  • Here is the solution I ended up using. There are aspects from most answers on this question that ended up providing leads to solving the issue.

    public async Task<bool> ValidateSignature(HttpRequest request, Signature signatureData)
    {
        var headerValue = request.Headers[signatureData.HeaderKeyName];
        var keyBytes = Encoding.UTF8.GetBytes(signatureData.Secret);
        var messageBytes = Encoding.UTF8.GetBytes(await request.ReadAsStringAsync());
        byte[] hashMessage;
    
        switch (signatureData.HashType)
        {
            case HashType.HMAC_Sha1:
                hashMessage = new HMACSHA1(keyBytes).ComputeHash(messageBytes);
                break;
    
            case HashType.HMAC_Sha256:
                hashMessage = new HMACSHA256(keyBytes).ComputeHash(messageBytes);
                break;
    
            case HashType.HMAC_Sha384:
                hashMessage = new HMACSHA384(keyBytes).ComputeHash(messageBytes);
                break;
    
            case HashType.HMAC_Sha512:
                hashMessage = new HMACSHA512(keyBytes).ComputeHash(messageBytes);
                break;
    
            case HashType.HMAC_MD5:
                hashMessage = new HMACMD5(keyBytes).ComputeHash(messageBytes);
                break;
    
            default:
                throw new ArgumentOutOfRangeException(nameof(signatureData), "Hash type not currently supported.");
        }
    
        var builder = new StringBuilder();
        foreach (var t in hashMessage) builder.Append(t.ToString("x2"));
    
        var finalValue = builder.ToString();
        if (signatureData.HasPrefix) finalValue = $"{signatureData.PrefixValue}{builder}";
    
        return finalValue == headerValue;
    }