bchavez / Coinbase.Commerce

:moneybag: A .NET/C# implementation of the Coinbase Commerce API.
https://commerce.coinbase.com/docs/
Other
48 stars 9 forks source link

Is this out of sync with the latest api? #3

Closed bpatton00 closed 6 years ago

bpatton00 commented 6 years ago

The front end seems to work just fine but the back end hooks don't seem to be working.

Issue 1) the test posts from coinbase seem to now include pricing: nil which blows up. I managed to fix this by writing in some logic to remove it from the json if it shows up.

2) The create seems to be working on but the confirm seems to always come back with invalid shared secret.

3) possible side note, I'm working outside of MVC and despite it posting the test posts to my end point seem always seem to come back with a message that says "Failed to establish a connection to the remote server". I am returning 200 but it's not caring.

Appreciate any insight you can offer.



public HttpResponseMessage CallbackFunction ()
    {
        try
        {
            string SHARED_SECRET = "<Secret>";

            var requestSignature = Request.Headers[HeaderNames.WebhookSignature];
            //owner_functions.CreateGeneralLog("CoinBase", requestSignature);
            Request.InputStream.Seek(0, SeekOrigin.Begin);
            var json = new StreamReader(Request.InputStream).ReadToEnd();

            //clear up bad information if it's passed in
            if (json.Contains("\"pricing\":\"nil\",")) { json = json.Replace("\"pricing\":\"nil\",", ""); }

            owner_functions.CreateGeneralLog("CoinBase", json);

            if (!WebhookHelper.IsValid(SHARED_SECRET, requestSignature, json))
            {
                //fail
                owner_functions.CreateGeneralLog("CoinBase", "INVALID SHARED SECRET");
                return new HttpResponseMessage(HttpStatusCode.Unauthorized);

            }

            var webhook = JsonConvert.DeserializeObject<Webhook>(json);

            var chargeInfo = webhook.Event.DataAs<Charge>();
            try
            {
                var customerId = chargeInfo.Metadata["customerId"].ToObject<string>();
            }
            catch { owner_functions.CreateGeneralLog("CoinBase Error", "No Customer ID Found"); }

            var charge = webhook.Event.DataAs<Charge>();

            if (webhook.Event.IsChargeCreated)
            {
                    // The charge was created just now.
                    // Do something with the newly created
                    // event.
                    owner_functions.CreateGeneralLog("CoinBase", "Charge Created");
                    return new HttpResponseMessage(HttpStatusCode.OK);

            }

            else if (webhook.Event.IsChargeConfirmed)
            {
                    // The payment was confirmed
                    owner_functions.CreateGeneralLog("CoinBase", "Charge Confirmed");
                    return new HttpResponseMessage(HttpStatusCode.OK);

            }

            else if (webhook.Event.IsChargeFailed)
            {
                    // The payment failed. Log something.
                    owner_functions.CreateGeneralLog("CoinBase", "Charge Failed");
                    return new HttpResponseMessage(HttpStatusCode.OK);
            }            

            return new HttpResponseMessage(HttpStatusCode.OK);
        }
        catch (Exception ex) {
            owner_functions.CreateGeneralLog("CoinBase Error", ex.ToString());
            return new HttpResponseMessage(HttpStatusCode.BadRequest);
        }
    }
bchavez commented 6 years ago

Hi @bpatton00 ,

Thanks, Brian

bpatton00 commented 6 years ago

json payload example {"id":1,"scheduled_for":"2017-01-31T20:50:02Z","attempt_number":1,"event":{"id":"24934862-d980-46cb-9402-43c81b0cdba6","resource":"event","type":"charge:confirmed","api_version":"2018-03-22","created_at":"2017-01-31T20:49:02Z","data":{"code":"66BEOV2A","id":"24911111-aeae-46cb-9402-43c81b0cdba6","resource":"charge","name":"The Sovereign Individual","description":"Mastering the Transition to the Information Age","hosted_url":"https://commerce.coinbase.com/charges/66BEOV2A","created_at":"2017-01-31T20:49:02Z","confirmed_at":"2017-01-31T20:51:02Z","expires_at":"2017-01-31T21:04:02Z","timeline":[{"time":"2017-01-31T20:49:02Z","status":"NEW"},{"status":"PENDING","payment":{"network":"ethereum","transaction_id":"0xe02fead885c3e4019945428ed54d094247bada2d0ac41b08fce7ce137bf29587"},"time":"2017-01-31T20:50:02Z"},{"status":"COMPLETED","payment":{"network":"ethereum","transaction_id":"0xe02fead885c3e4019945428ed54d094247bada2d0ac41b08fce7ce137bf29587"},"time":"2017-01-31T20:51:02Z"}],"metadata":{},"pricing":"nil","pricing_type":"no_price","payments":[{"network":"ethereum","transaction_id":"0xe02fead885c3e4019945428ed54d094247bada2d0ac41b08fce7ce137bf29587","status":"CONFIRMED","value":{"local":{"amount":"100.0","currency":"USD"},"crypto":{"amount":"10.00","currency":"ETH"}},"block":{"height":100,"hash":"0xe02fead885c3e4019945428ed54d094247bada2d0ac41b08fce7ce137bf29587","confirmations_accumulated":8,"confirmations_required":2}}],"addresses":{"bitcoin":"0000000000000000000000000000000000","ethereum":"0x0000000000000000000000000000000000000000"}}}}

Item 2: As I looked a bit deeper into the way the validation works for the payload I think modifying the json is going to cause that to fail everytime, so that makes sense.

Item 3: image

What I'm trying to accomplish: simply create a functional end point for the webhooks so I can update the database as events occur. Should work but no luck so far.

bchavez commented 6 years ago

Ah, okay, thank you for updating the original post.

So, on No.2, you're modifying the JSON webhook payload with:

if (json.Contains("\"pricing\":\"nil\",")) { json = json.Replace("\"pricing\":\"nil\",", ""); }

The essence of Webhook signatures and HMAC message authentication is to ensure the payload hasn't been tampered with or that the payload hasn't been forged. So, as soon as you modify the JSON (before checking), the Webhook payload has been tampered with and the Webhook signature provided by Coinbase is no longer valid. The short story is you can't modify the JSON and have WebhookHelper.IsValid pass.

What you can do... is do the validation before modifying the JSON. IE:

owner_functions.CreateGeneralLog("CoinBase", json);

if (!WebhookHelper.IsValid(SHARED_SECRET, requestSignature, json))
{
    //fail
    owner_functions.CreateGeneralLog("CoinBase", "INVALID SHARED SECRET");
    return new HttpResponseMessage(HttpStatusCode.Unauthorized);
}

//clear up bad information if it's passed in
if (json.Contains("\"pricing\":\"nil\",")) { json = json.Replace("\"pricing\":\"nil\",", ""); }

If there's an extra field causing JSON deserialization to panic, then we'll need to fix that here and issue a new release.

Let me know if that helps.

bchavez commented 6 years ago

Re: No.3 - Failed to establish connection issue

Could you try using ngrok to debug & test your callbacks? I have a feeling there's a firewall or some kind of HTTPS restriction causing some connection failure somewhere.

Hard to tell if it's the library or some kind of network blockage somewhere.

bpatton00 commented 6 years ago

Ok, this can be closed, no issue with the code base here (outside the nil issue). Thanks for getting me down the right path. Here are my notes if anyone else runs into issues: 1) as expected re-ordering the cleanup did let me get around the nil pricing issue. 2) the re-ordering also solved the secret being invalidated 3) I transitioned to an older model handler via ihttphandler which allowed me to properly return an error code after I changed context.response.end() to HttpContext.Current.ApplicationInstance.CompleteRequest(); . This brought the webhook tests in line to what I was expecting.

My Handler looks like this if anyone needs an example:

public class CoinBaseHandler : IHttpHandler
{
    public CoinBaseHandler()
    {

    }

    public void ProcessRequest(HttpContext context)
    {
        //context.Response.ContentType = "text/plain";
        //context.Response.Write("Hello World");

        SqlConnection con = new SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings["DBConnString"].ConnectionString);

        //var json = new StreamReader(context.Request.InputStream).ReadToEnd();
        try
        {
            string SHARED_SECRET = "<secret-here>";

            var requestSignature = context.Request.Headers[HeaderNames.WebhookSignature];
            //owner_functions.CreateGeneralLog("CoinBase", requestSignature);
            context.Request.InputStream.Seek(0, SeekOrigin.Begin);
            var json = new StreamReader(context.Request.InputStream).ReadToEnd();

            owner_functions.CreateGeneralLog("CoinBase", json);

            if (!WebhookHelper.IsValid(SHARED_SECRET, requestSignature, json))
            {
                //fail
                owner_functions.CreateGeneralLog("CoinBase", "INVALID SHARED SECRET");
                context.Response.Write("INVALID SHARED SECRET");
                context.Response.StatusCode = 500;
                context.Response.End();
            }

            //clear up bad information if it's passed in
            if (json.Contains("\"pricing\":\"nil\",")) { json = json.Replace("\"pricing\":\"nil\",", ""); }

            var webhook = JsonConvert.DeserializeObject<Webhook>(json);

            var chargeInfo = webhook.Event.DataAs<Charge>();
            try
            {
                var customerId = chargeInfo.Metadata["customerId"].ToObject<string>();
            }
            catch { owner_functions.CreateGeneralLog("CoinBase Error", "No Customer ID Found"); }

            if (webhook.Event.IsChargeCreated)
            {
                // The charge was created just now.
                // Do something with the newly created
                // event.
                owner_functions.CreateGeneralLog("CoinBase", "Charge Created");
                //context.Response.Write("Charge Created");
                context.Response.StatusCode = 200;
                HttpContext.Current.ApplicationInstance.CompleteRequest();

            }

            else if (webhook.Event.IsChargeConfirmed)
            {
                // The payment was confirmed
                owner_functions.CreateGeneralLog("CoinBase", "Charge Confirmed");
                //context.Response.Write("Charge Confirmed");
                context.Response.StatusCode = 200;
                //context.Response.End();                
                HttpContext.Current.ApplicationInstance.CompleteRequest();

            }

            else if (webhook.Event.IsChargeFailed)
            {
                // The payment failed. Log something.
                owner_functions.CreateGeneralLog("CoinBase", "Charge Failed");
                //context.Response.Write("Charge Failed");
                context.Response.StatusCode = 200;
                HttpContext.Current.ApplicationInstance.CompleteRequest();
            }

            context.Response.StatusCode = 200;
            HttpContext.Current.ApplicationInstance.CompleteRequest();
        }
        catch (Exception ex)
        {
            owner_functions.CreateGeneralLog("CoinBase Error", ex.ToString());
            context.Response.Write("Error");
            context.Response.StatusCode = 500;
            context.Response.End();
        }
    }

    public bool IsReusable
    {
        get
        {
            return false;
        }
    }
}

in the web config , you need to add an httphandler (exact structure depends on your IIS version, config, and code architecture)


<add path="merchant/coinbase.callback" verb="*" type="CoinBaseHandler"/>
Odigietony commented 5 years ago

Hello, @bpatton00 @bchavez , Can I use Asp.net Webhook extension to handle the coinbase webhook response?

bchavez commented 5 years ago

Hi @Odigietony,

Could you clarify what asp.net webhook extensions you're referring to? Are you talking about these?

In order to use any webhooks with Coinbase Commerce and this C# library, you want to use WebhookHelper static class as outlined in the readme and any information you can glean from this issue that may help.

I don't provide any "automatic built-in support" for any particular framework like ASPNET Core or ASP.NET MVC 5 because doing so would require a specific reference dependency on the underlying framework's HttpContext requiring us to release multiple DLLs for every web framework imaginable on .NET and maintain version upkeep. So, in order to keep mintiance issues low, you'll have to pull out JSON payloads from whatever web framework you're using and use WebhookHelper to verify callbacks you receive.

If you have any issues like deserialization errors with nil contained the JSON payload (as described here in the original issue), then please let me know so we can get that fixed.

Thanks, Brian

:crescent_moon: :stars: "Nothing good happens past 2am..."

bchavez commented 5 years ago

Also, almost forgot, don't forget to use:

https://smee.io/ or https://ngrok.com/

to help you debug webhooks.

Odigietony commented 5 years ago

Hello @bchavez, read the outline and made reference to your Sample Callback But I keep getting this error

HttpResponse does not contain a definition for InputStream and no accessible extension method inputStream accepting a first argument of type HttpRequest could not be found(are you missing a using directive or an assembly reference?)

on this line: Request.InputStream.Seek(0, SeekOrigin.Begin);

bchavez commented 5 years ago

Hi @Odigietony,

Request.InputStream.Seek(0, SeekOrigin.Begin);

This line above depends on the underlying web framework you're using. Accessing the request body will be different for each web framework. In this particular case, when using legacy .NET framework, you need to rewind the InputStream because the request body was already read by framework internals.

What are you using? ASP.NET Core? ASP.NET MVC? Also, what version?

Odigietony commented 5 years ago

Hi, @bchavez

I'm currently using Asp.Net Core 2.1

bchavez commented 5 years ago

Hi @Odigietony ,

ASP.NET Core 2 looks something like this:

[HttpPost]
public ActionResult Post()
{
   string postBody = null;
   using (var sr = new StreamReader(this.Request.Body))
   {
      postBody = sr.ReadToEnd();
   }

   if( string.IsNullOrWhiteSpace(postBody) )
      return BadRequest("Invalid HTTP POST body.");

   if( !this.Request.Headers.TryGetValue(HeaderNames.WebhookSignature, out var headerValues) )
      return BadRequest("No signature header value to authenticate.");

   var webhookSignature = headerValues.FirstOrDefault();

   if( string.IsNullOrWhiteSpace(webhookSignature) )
      return BadRequest("No signature header value to authenticate.");

   //Validate Webhook Callback Here
   if (WebhookHelper.IsValid(sharedSecret: "SECRET", headerValue: webhookSignature, jsonBody: postBody))
   {
      return Ok("Thank you for your purchase.");
   }
   else
   {
      return Unauthorized("Hacker!");
   }
}

Be sure to use the latest version of C#, that is 7.3.

Hope that helps! Brian

:chocolate_bar: :cookie: :lollipop: Ronald Jenkees - Stay Crunchy

Odigietony commented 5 years ago

Thanks so much @bchavez It works perfectly now.

ccurves commented 3 years ago

Hi @bpatton00 I'm stuck can someone please put me through; Any time I try testing the webhook to my application . I get this error "Failed to establish a connection to the remote server at *mysite.com" contacted coinbase support but they still haven't gotten back to me for over a week now. The application is hosted on heroku. Any idea what am doing wrong?

`router.post("/webhook", (req, res) => { const rawBody = req.rawBody; const signature = req.headers["x-cc-webhook-signature"]; const webhookSecret = "your-webhook-secret";

try { const event = Webhook.verifyEventBody(rawBody, signature, webhookSecret);

if (event.type === "charge:pending") {
  // TODO
  // user paid, but transaction not confirm on blockchain yet
}

if (event.type === "charge:confirmed") {
  // TODO
  // all good, charge confirmed
}

if (event.type === "charge:failed") {
  // TODO
  // charge failed or expired
}

res.send(`success ${event.id}`);

} catch (error) { functions.logger.error(error); res.status(400).send("failure!"); } });`

BoboYaya commented 3 years ago

Also, almost forgot, don't forget to use:

https://smee.io/ or https://ngrok.com/

to help you debug webhooks.

i use ngrok debug, it works well. Put on my host, it case "Failed to establish a connection to the remote server"! what is wrong?