Closed bpatton00 closed 6 years ago
Hi @bpatton00 ,
Thanks, Brian
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:
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.
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.
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.
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"/>
Hello, @bpatton00 @bchavez , Can I use Asp.net Webhook extension to handle the coinbase webhook response?
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..."
Also, almost forgot, don't forget to use:
https://smee.io/ or https://ngrok.com/
to help you debug webhooks.
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);
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?
Hi, @bchavez
I'm currently using Asp.Net Core 2.1
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
Thanks so much @bchavez It works perfectly now.
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!"); } });`
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?
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.