Securing Indexer Webhooks

In order to verify that the webhooks that you're receiving from your event indexers are coming from Graffle, and that the contents are valid, you can specify an optional HMAC token (https://en.wikipedia.org/wiki/HMAC) at the project level:

NOTE: The HMAC token must be a base64 encoded string.

To get started, you'll need two pieces of information:

  1. Your Graffle company Id - available in the top right corner of the portal.

  2. A secret HMAC token that you set on the project settings page. NOTE: This token must be a base-64 encoded string.

Once you've set an HMAC token, your webhooks will now include an authorization header in the form of:

hmacauth companyId:base64RequestSignature:nonce:requestTimestamp"

companyID: - Your Graffle company Id

base64RequestSignature: - A HMACSHA256 hash of the request and its properties using your HMAC token.

nonce: - A nonce (https://en.wikipedia.org/wiki/Cryptographic_nonce) that can be used to prevent replay attacks.

requestTimestamp: - The UNIX timestamp of when the webhook was sent.

C# HMAC Decoding Example

In your authorization pipeline:
```
public Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
    var req = context.HttpContext.Request;
    var authHeaders = context.HttpContext.Request.Headers["Authorization"].FirstOrDefault();
    if (authHeaders != null && authHeaders.StartsWith("hmacauth"))
    {
        //remove "hmacauth " from the start of the header
        var rawAuthHeader = authHeaders.Substring(9);
        var authorizationHeaderArray = GetAuthorizationHeaderValues(rawAuthHeader);
        if (authorizationHeaderArray != null)
        {
            var companyId = authorizationHeaderArray[0];
            var incomingBase64Signature = authorizationHeaderArray[1];
            var nonce = authorizationHeaderArray[2];
            var requestTimeStamp = authorizationHeaderArray[3];
            var isValid = IsValidRequest(req, companyId, incomingBase64Signature, nonce, requestTimeStamp);
            if (isValid.Result)
            {
                //handle valid request
            }
            else
            {
                //handle invalid request - hash did not match
            }
        }
        else
        {
            //handle invalid request - missing/invalid header value
        }
    }
    else
    {
        //handle invalid request - missing/invalid header value
    }
    return Task.FromResult(0);
}
```
Helper methods:
```
private string[] GetAuthorizationHeaderValues(string rawAuthHeader)
{
    var credArray = rawAuthHeader.Split(':');
    if (credArray.Length == 4)
    {
        return credArray;
    }
    else
    {
        return null;
    }
}
private async Task<bool> IsValidRequest(HttpRequest req, string companyId, string incomingBase64Signature, string nonce, string requestTimeStamp)
{
    string requestContentBase64String = "";
    string requestUri = HttpUtility.UrlEncode(req.GetDisplayUrl().ToLower());
    string requestHttpMethod = req.Method;
    if (companyId != "YOUR GRAFFLE COMPANY ID")
    {
        return false;
    }
    var sharedKey = "YOUR BASE-64 ENCODED HMAC TOKEN";
    if (IsReplayRequest(nonce, requestTimeStamp))
    {
        return false;
    }
    var contentStream = new StreamReader(req.Body);
    byte[] hash = ComputeHash(await contentStream.ReadToEndAsync());
    if (hash != null)
    {
        requestContentBase64String = Convert.ToBase64String(hash);
    }
    string data = String.Format("{0}{1}{2}{3}{4}{5}", companyId, requestHttpMethod, requestUri, requestTimeStamp, nonce, requestContentBase64String);
    var secretKeyBytes = Convert.FromBase64String(sharedKey);
    byte[] signature = Encoding.UTF8.GetBytes(data);
    using (HMACSHA256 hmac = new HMACSHA256(secretKeyBytes))
    {
        byte[] signatureBytes = hmac.ComputeHash(signature);
        var base64Signature = Convert.ToBase64String(signatureBytes);
        return (incomingBase64Signature.Equals(base64Signature, StringComparison.Ordinal));
    }
}
private bool IsReplayRequest(string nonce, string requestTimeStamp)
{
    if (System.Runtime.Caching.MemoryCache.Default.Contains(nonce))
    {
        return true;
    }
    DateTime epochStart = new DateTime(1970, 01, 01, 0, 0, 0, 0, DateTimeKind.Utc);
    TimeSpan currentTs = DateTime.UtcNow - epochStart;
    var serverTotalSeconds = Convert.ToUInt64(currentTs.TotalSeconds);
    var requestTotalSeconds = Convert.ToUInt64(requestTimeStamp);
    if ((serverTotalSeconds - requestTotalSeconds) > requestMaxAgeInSeconds)
    {
        return true;
    }
    System.Runtime.Caching.MemoryCache.Default.Add(nonce, requestTimeStamp, DateTimeOffset.UtcNow.AddSeconds(requestMaxAgeInSeconds));
    return false;
}
private static byte[] ComputeHash(string requestBody)
{
    using (MD5 md5 = MD5.Create())
    {
        byte[] hash = null;
        var content = Encoding.UTF8.GetBytes(requestBody);
        if (content.Length != 0)
        {
            hash = md5.ComputeHash(content);
        }
        return hash;
    }
}
```

Node.js HMAC Decoding Example

The GUIDs in the example are dummy values provided for the example only.

var crypto = require('crypto');

var body = '{"id":"8e16ee18d644974a00e8bb6aba16b23162dc81db17edd1108ced04191ec058e5","graffleProjectId":"6d2ff96e-715f-42fc-87f7-136da141bdde","graffleCompanyId":"29df57b8-a4ff-4ae9-bc9b-1fb50c49ac54","flowEventId":"A.c1e4f4f4c4257510.TopShotMarketV3.MomentListed","graffleEventToken":"59f6debd-128b-4f05-9e0f-d60e33eea21e","blockHeight":24824433,"eventDate":"2022-02-26T02:56:20.4927984+00:00","createdAt":"2022-02-26T02:56:22.6460387+00:00","metaData":null,"blockEventData":{"id":12204291,"price":4.00000000,"seller":"0xab8034319996ec49"},"webHook":"https://webhook.site/b2996651-a887-44ea-97e4-d2c1871e8a89","flowTransactionId":"d7f08a91f59e0aa6b411e9cc140180d1f2b2296c35b886b6414013acf43167e2"}';
console.log('body');
console.log(body);

var hash = crypto.createHash('md5').update(body).digest('base64');
console.log(hash);

var csharpHash = 'dxFH3n75au/c1cLSmB53AA==';

console.log('request body hash matches:');
console.log(hash === csharpHash);

var secret = 'dGVzdA==';
console.log('base64 secret:');
console.log(secret);
var secretbuff = new Buffer.from(secret, 'base64');
var secrettext = secretbuff.toString('utf8');
console.log('utf8 secret:');
console.log(secrettext);
var secretKeyBytes = [];
for (var i = 0; i < secrettext.length; i++) {
    secretKeyBytes.push(secrettext.charCodeAt(i));
}
console.log('secret bytes:');
console.log(Buffer.from(secretKeyBytes));

var data = '29df57b8-a4ff-4ae9-bc9b-1fb50c49ac54POSThttps%3a%2f%2fwebhook.site%2fb2996651-a887-44ea-97e4-d2c1871e8a89164584420609ed04a357254562bd969530a2b295aedxFH3n75au/c1cLSmB53AA==';
var utf8Data = unescape(encodeURIComponent(data, 'utf-8'));
var signature = [];
for (var i = 0; i < utf8Data.length; i++) {
    signature.push(utf8Data.charCodeAt(i));
}
console.log('signaturebytes:');
console.log(signature);

var base64Signature = crypto.createHmac('sha256', Buffer.from(secretKeyBytes)).update(Buffer.from(signature, 'utf8')).digest('base64');
console.log('base64Signature:');
console.log(base64Signature);

var incomingBase64Signature = 'zGa8YdMC2LE1Jo+8+fcIkrsNasM36OJ10eFkBhAGEdA='
console.log("IsValidRequest:")
console.log(incomingBase64Signature == base64Signature);

Last updated