7

Im trying to implement a stripe webhook using the the c# library Stripe.net by Jayme Davis. i have set up the test endpoint in the stripe dashboard and generated the secret. The endpoint is being hit fine, and will generate the StripeEvent using the StripeEventUtility.ParseEvent. The problem is with using the ConstructEvent function i cant get the signatures to match. Any help or suggestions would be much appreciated.

isSignaturePresent is returning false

//call to create event
stripeEvent = ConstructEvent(json, Request.Headers["Stripe-Signature"], 
SecretFromStripeDashBoard);


private StripeEvent ConstructEvent(string json, string 
stripeSignatureHeader, string secret, int tolerance = 300)
    {
        var signatureItems = parseStripeSignature(stripeSignatureHeader);

        var signature = computeSignature(secret, signatureItems["t"].FirstOrDefault(), json);

        if (!isSignaturePresent(signature, signatureItems["v1"]))
            throw new Exception("The signature for the webhook is not present in the Stripe-Signature header.");

        //var utcNow = EpochUtcNowOverride ?? DateTime.UtcNow.ConvertDateTimeToEpoch();
        //var webhookUtc = Convert.ToInt32(signatureItems["t"].FirstOrDefault());

        //if (utcNow - webhookUtc > tolerance)
        //    throw new Exception("The webhook cannot be processed because the current timestamp is above the allowed tolerance.");

        return Mapper<StripeEvent>.MapFromJson(json);
    }

    private ILookup<string, string> parseStripeSignature(string stripeSignatureHeader)
    {
        return stripeSignatureHeader.Trim()
            .Split(',')
            .Select(item => item.Trim().Split('='))
            .ToLookup(item => item[0], item => item[1]);
    }

    private bool isSignaturePresent(string signature, IEnumerable<string> signatures)
    {
        return signatures.Any(key => secureCompare(key, signature));
    }

    private string computeSignature(string secret, string timestamp, string payload)
    {
        var secretBytes = Encoding.UTF8.GetBytes(secret);
        var payloadBytes = Encoding.UTF8.GetBytes($"{timestamp}.{payload}");

        var cryptographer = new HMACSHA256(secretBytes);
        var hash = cryptographer.ComputeHash(payloadBytes);

        return BitConverter.ToString(hash).Replace("-", "").ToLower();
    }

    private bool secureCompare(string a, string b)
    {
        if (a.Length != b.Length) return false;

        var result = 0;

        for (var i = 0; i < a.Length; i++)
        {
            result |= a[i] ^ b[i];
        }

        return result == 0;
    }
}
stephen
  • 107
  • 1
  • 7
  • How are you initializing the `json` variable? – Ywain May 10 '17 at 09:52
  • i using var json = JsonSerializer.SerializeToString(request); – stephen May 10 '17 at 12:29
  • 1
    So you're reserializing the deserialized request body. However, it's very unlikely that this would return the exact same string as the original request body. You need to use the raw request body as passed by your web server/framework. – Ywain May 10 '17 at 12:32
  • will give that a try thanks – stephen May 10 '17 at 12:54
  • Yeah that worked a treat, thanks. – stephen May 10 '17 at 13:18
  • This also happens in Scala, with the Play framework. The request body is parsed to json by default, and if you try to access the string, its the deserialized json which is different from the raw body, so the sig verification fails. I detailed how to get the raw body in this question: https://stackoverflow.com/questions/59332440/how-to-get-the-raw-request-body-in-play/59332549#59332549 – Ali Dec 14 '19 at 05:36

3 Answers3

7

I answered in comments above, but to recap, the issue was that the json string provided to the ConstructEvent method did not contain the exact payload sent by Stripe.

Rather, you initialized the payload with:

var json = JsonSerializer.SerializeToString(request);

i.e. you reserialized the deserialized request's body. However, it's very unlikely that you would get the same string as the original payload sent by Stripe. E.g. Stripe could send this payload:

{
  "a_key": "a_value",
  "another_key": "another_value"
}

but after deserializing+reserializing, your JSON string could contain this value:

{"another_key": "another_value","a_key": "a_value"}

as the key order is not guaranteed to be maintained, and other formatting options (newlines, indents) come into play.

Webhook signatures are generated using the exact payload (as a raw string), so when verifying signatures, you must also provide that exact payload, as passed by your web server or framework.

Ywain
  • 16,854
  • 4
  • 51
  • 67
  • Hi @Ywain , How to get the exact payload from the request body. I've tried several methods (getting the body as [FromBody], getting the body from StreamReader etc) but none of the did not resolve the issue. – Isuru Siriwardana Mar 03 '20 at 07:15
  • Hi @IsuruSiriwardana. If you use ASP.NET, you should be able to get the raw body with something like this: `string rawBody; using (StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8)) { rawBody = await reader.ReadToEndAsync(); }` – Ywain Mar 04 '20 at 21:30
1

As pointed in the previous answer, the json string should be identical to the playload sent by stripe. In my situation I had to remove the \r to make it work like follow var json = (await stripStream.ReadToEndAsync()).Replace("\r", ""); Here is the code:

 [HttpPost("webhook")]
    public async Task<IActionResult> Index()
    {
        try
        {
            using (var stripStream = new StreamReader(HttpContext.Request.Body))
            {
                var json = (await stripStream.ReadToEndAsync()).Replace("\r", "");
                var secretKey = _settings.Value.StripeSettings.WebhookSecretKey;
                // validate webhook called by stripe only
                Logger.Information($"[WEBHOOK]: Validating webhook signature {secretKey.Substring(0, 4)}**********");
                var stripeEvent = EventUtility.ConstructEvent(json, Request.Headers["Stripe-Signature"], secretKey);
                Logger.Information($"[WEBHOOK]: Webhook signature validated");

                // handle stripe event
                await this._stripeWebhookService.HandleAsync(stripeEvent);

                return Ok();
            }
        }catch(Exception){ // handle exception}
Benzara Tahar
  • 2,058
  • 1
  • 17
  • 21
0

My application kept throwing an exception at this point as well.

 var stripeEvent = EventUtility.ConstructEvent(
              json,
              Request.Headers["Stripe-Signature"],
              secret
            );

Reading the exception logs it said I was using a new version of NuGet package stripe.net and an older version in the stripe dashboard. I update my account to the latest API version and I no longer got the error.

Be careful updating the versions in the dashboard if you are using older versions in other applications. You have 72 hours to undo the update in the dashboard if it breaks your other applications. I was starting off from scratch so updating was not a big deal for me.

user2325149
  • 44
  • 1
  • 4