Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Stripe webhook signature failed - Stripe.net

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;
    }
}
like image 990
stephen Avatar asked May 10 '17 09:05

stephen


People also ask

How do I verify my webhook signature?

To verify the signature, create the same SHA256 HMAC signature and then compare it to the webhook payload to ensure that they match. If they match, then you can be sure that the webhook came from Zendesk. If they don't, it may be a request from another source.

Does Stripe retry webhooks?

In test mode, Stripe retries three times over a few hours. Webhooks can be manually retried after this time in the Dashboard, and you can also query for missed events to reconcile the data over any time period.

How do you use webhooks with Stripe?

Steps to receive webhooks Create a webhook endpoint as an HTTP endpoint (URL) on your local server. Handle requests from Stripe by parsing each event object and returning 2xx response status codes. Test that your webhook endpoint is working properly using the Stripe CLI.


2 Answers

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.

like image 163
Ywain Avatar answered Oct 16 '22 01:10

Ywain


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}
like image 38
Benzara Tahar Avatar answered Oct 16 '22 02:10

Benzara Tahar