无法使用 C# 使 Typeform Webhook 签名正常工作
Unable to get Typeform Webhook Signature with C# to work
首先,这个问题已经被问到并得到回答 但它是特定于 Ruby/PHP 的,虽然我试图遵循它和 Typeform 自己的指导,但我无法实施C# 中的 Typeform 签名检查。
我编写了一个扩展方法来根据通过 webhook 发送的有效负载验证 Typeform 签名。如果签名有效,它 returns 字符串 (json) 有效载荷但如果不是 returns 错误。
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}`");
}
}
}
根据在 SO 上发现的其他问题,我将方法修改为 运行 没有编码(见下文)但仍然得到相同的结果,哈希值不匹配。
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);
}
}
我希望这会有所帮助...我也是 TypeForm API 的新手,但我有以下代码正在运行...非常混乱,我还没有抽出时间重构它...它在我的一个表单上工作正常,但出于某种原因,当我使用不同的 TypeForm 帐户时它不起作用(即使我设置了相同的秘密)......这就是为什么我还没有重构它......
但只是分享,因为它适用于我的第一个帐户并且可能有用(我的第二个帐户有同样的问题,结果不匹配......如果我找到原因,我会在这里告诉你和更新此答案
private bool IsValid(string jsonRequest)
{
string typeFormSig = Request.Headers["Typeform-Signature"];
string generatedSig = $"sha256={CreateToken(jsonRequest)}";
_logger.LogInformation($"SIGNATURE STUFF: sec: {SECRET} typeform: {typeFormSig} MyGen: {generatedSig}");
return (typeFormSig == generatedSig);
}
private static string CreateToken(string message)
{
var encoding = new System.Text.ASCIIEncoding();
byte[] keyByte = encoding.GetBytes(SECRET);
byte[] messageBytes = encoding.GetBytes(message);
using (var hmacsha256 = new HMACSHA256(keyByte))
{
byte[] hashmessage = hmacsha256.ComputeHash(messageBytes);
return Convert.ToBase64String(hashmessage);
}
}
这就是我得到 json 字符串的方式:
[HttpPost("")]
public async Task<IActionResult> Receive()
{
using (var reader = new StreamReader(Request.Body))
{
string jsonRequest = await reader.ReadToEndAsync();
尝试使用 UTF 编码器而不是建议的原始 ASCII 编码器:
private static string CreateToken(string message)
{
var encoding = new System.Text.UTF8Encoding();
byte[] keyByte = encoding.GetBytes(SECRET);
byte[] messageBytes = encoding.GetBytes(message);
using (var hmacsha256 = new HMACSHA256(keyByte))
{
byte[] hashmessage = hmacsha256.ComputeHash(messageBytes);
return Convert.ToBase64String(hashmessage);
}
}
private bool IsValid(string jsonRequest)
{
string typeFormSig = Request.Headers["Typeform-Signature"];
string generatedSig = $"sha256={CreateToken(jsonRequest)}";
_logger.LogInformation($"SIGNATURE STUFF: sec: {SECRET} typeform: {typeFormSig} MyGen: {generatedSig}");
return (typeFormSig == generatedSig);
}
这是我最终使用的解决方案。这个问题的大多数答案的某些方面最终提供了解决问题的线索。
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;
}
如果您在测试 webhook 服务时遇到此问题,请记住 Typeform 发送的 JSON 正文已缩小 和 以单个 Unix 换行符 (0x0a
) 字符.
结尾
这在将请求正文直接发送到 HMACSHA256
实例时关系不大,但如果您尝试使用内联 JSON 字符串验证代码,则这一点很重要。特别令人困惑的是,Typeform 在 webhooks 管理 UI.
中显示格式化的 JSON 输出
C# 中字符串的签名验证可能如下所示:
using System;
class Program {
static void Main(string[] args) {
var key = "secret-key";
// Note:
// - No spaces after `:`, `,`, etc.
// - No indentation.
// - No newlines except for a single line feed character on the end.
var body = "{\"event_id\":\"01FCR2NZ5NNGBPTWEJXV0FR5V3\",\"event_type\":\"...\"}\u000a";
// Convert strings to UTF-8 byte arrays.
var keyBytes = Text.Encoding.UTF8.GetBytes(key);
var bodyBytes = Text.Encoding.UTF8.GetBytes(body);
string signature;
using (var hmac = new Security.Cryptography.HMACSHA256(keyBytes))
{
// Calculate a hash and convert it to a base-64 string.
var computedHashBytes = hmac.ComputeHash(bodyBytes);
var computedHashBase64 = Convert.ToBase64String(computedHashBytes);
signature = $"sha256={computedHashBase64}";
}
Console.WriteLine(signature);
}
}
首先,这个问题已经被问到并得到回答
我编写了一个扩展方法来根据通过 webhook 发送的有效负载验证 Typeform 签名。如果签名有效,它 returns 字符串 (json) 有效载荷但如果不是 returns 错误。
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}`");
}
}
}
根据在 SO 上发现的其他问题,我将方法修改为 运行 没有编码(见下文)但仍然得到相同的结果,哈希值不匹配。
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);
}
}
我希望这会有所帮助...我也是 TypeForm API 的新手,但我有以下代码正在运行...非常混乱,我还没有抽出时间重构它...它在我的一个表单上工作正常,但出于某种原因,当我使用不同的 TypeForm 帐户时它不起作用(即使我设置了相同的秘密)......这就是为什么我还没有重构它......
但只是分享,因为它适用于我的第一个帐户并且可能有用(我的第二个帐户有同样的问题,结果不匹配......如果我找到原因,我会在这里告诉你和更新此答案
private bool IsValid(string jsonRequest)
{
string typeFormSig = Request.Headers["Typeform-Signature"];
string generatedSig = $"sha256={CreateToken(jsonRequest)}";
_logger.LogInformation($"SIGNATURE STUFF: sec: {SECRET} typeform: {typeFormSig} MyGen: {generatedSig}");
return (typeFormSig == generatedSig);
}
private static string CreateToken(string message)
{
var encoding = new System.Text.ASCIIEncoding();
byte[] keyByte = encoding.GetBytes(SECRET);
byte[] messageBytes = encoding.GetBytes(message);
using (var hmacsha256 = new HMACSHA256(keyByte))
{
byte[] hashmessage = hmacsha256.ComputeHash(messageBytes);
return Convert.ToBase64String(hashmessage);
}
}
这就是我得到 json 字符串的方式:
[HttpPost("")]
public async Task<IActionResult> Receive()
{
using (var reader = new StreamReader(Request.Body))
{
string jsonRequest = await reader.ReadToEndAsync();
尝试使用 UTF 编码器而不是建议的原始 ASCII 编码器:
private static string CreateToken(string message)
{
var encoding = new System.Text.UTF8Encoding();
byte[] keyByte = encoding.GetBytes(SECRET);
byte[] messageBytes = encoding.GetBytes(message);
using (var hmacsha256 = new HMACSHA256(keyByte))
{
byte[] hashmessage = hmacsha256.ComputeHash(messageBytes);
return Convert.ToBase64String(hashmessage);
}
}
private bool IsValid(string jsonRequest)
{
string typeFormSig = Request.Headers["Typeform-Signature"];
string generatedSig = $"sha256={CreateToken(jsonRequest)}";
_logger.LogInformation($"SIGNATURE STUFF: sec: {SECRET} typeform: {typeFormSig} MyGen: {generatedSig}");
return (typeFormSig == generatedSig);
}
这是我最终使用的解决方案。这个问题的大多数答案的某些方面最终提供了解决问题的线索。
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;
}
如果您在测试 webhook 服务时遇到此问题,请记住 Typeform 发送的 JSON 正文已缩小 和 以单个 Unix 换行符 (0x0a
) 字符.
这在将请求正文直接发送到 HMACSHA256
实例时关系不大,但如果您尝试使用内联 JSON 字符串验证代码,则这一点很重要。特别令人困惑的是,Typeform 在 webhooks 管理 UI.
C# 中字符串的签名验证可能如下所示:
using System;
class Program {
static void Main(string[] args) {
var key = "secret-key";
// Note:
// - No spaces after `:`, `,`, etc.
// - No indentation.
// - No newlines except for a single line feed character on the end.
var body = "{\"event_id\":\"01FCR2NZ5NNGBPTWEJXV0FR5V3\",\"event_type\":\"...\"}\u000a";
// Convert strings to UTF-8 byte arrays.
var keyBytes = Text.Encoding.UTF8.GetBytes(key);
var bodyBytes = Text.Encoding.UTF8.GetBytes(body);
string signature;
using (var hmac = new Security.Cryptography.HMACSHA256(keyBytes))
{
// Calculate a hash and convert it to a base-64 string.
var computedHashBytes = hmac.ComputeHash(bodyBytes);
var computedHashBase64 = Convert.ToBase64String(computedHashBytes);
signature = $"sha256={computedHashBase64}";
}
Console.WriteLine(signature);
}
}