Skip to content

Commit

Permalink
Merge pull request #22 from swlodarski-sumoheavy/feature/SP-1022
Browse files Browse the repository at this point in the history
SP-1022 - C# Kiosk Demo: Add HMAC verification
  • Loading branch information
bobbrodie authored Feb 6, 2025
2 parents 3e30150 + 8857369 commit 6785fe3
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ protected Task<HttpResponseMessage> Get(string url)

protected Task<HttpResponseMessage> Post(
string url,
string jsonRequest
string jsonRequest,
Dictionary<string, string>? headers = null
)
{
var httpContent = new StringContent(
Expand All @@ -78,6 +79,13 @@ string jsonRequest
"application/json"
);

if (headers != null) {
foreach(var item in headers)
{
httpContent.Headers.Add(item.Key, item.Value);
}
}

return _client.PostAsync(url, httpContent);
}

Expand Down Expand Up @@ -106,4 +114,4 @@ public void Info(LogCode code, string message, Dictionary<string, object?> conte
public void Error(LogCode code, string message, Dictionary<string, object?> context)
{
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,14 @@ public async Task POST_InvoiceExistsForUuidAndUpdateDataAreValid_UpdateInvoice()
var updateDataJson = UnitTest.GetDataFromFile("updateData.json");

// when
var result = await Post("/invoices/" + invoice.Uuid, updateDataJson);
var result = await Post(
"/invoices/" + invoice.Uuid,
updateDataJson,
new Dictionary<string, string>
{
{ "x-signature", "bKGK0WgsFfMSEg4fpik9+OdjYrYNA1E99kI1QJmbfKw=" }
}
);

// then
result.EnsureSuccessStatusCode();
Expand All @@ -37,7 +44,14 @@ public async Task POST_InvoiceDoesNotExistsForUuid_DoNotUpdateInvoice()
var updateDataJson = UnitTest.GetDataFromFile("updateData.json");

// when
var result = await Post("/invoices/12312412", updateDataJson);
var result = await Post(
"/invoices/12312412",
updateDataJson,
new Dictionary<string, string>
{
{ "x-signature", "bKGK0WgsFfMSEg4fpik9+OdjYrYNA1E99kI1QJmbfKw=" }
}
);

// then
UnitTest.Equals(
Expand All @@ -58,7 +72,14 @@ public async Task POST_UpdateDataAreInvalid_DoNotUpdateInvoice()
var updateDataJson = UnitTest.GetDataFromFile("invalidUpdateData.json");

// when
var result = await Post("/invoices/" + invoice.Uuid, updateDataJson);
var result = await Post(
"/invoices/" + invoice.Uuid,
updateDataJson,
new Dictionary<string, string>
{
{ "x-signature", "16imUAXdJqur7yyQyDRRfcbPCeMPiuBFnNJVLlpi3hQ=" }
}
);

// then
UnitTest.Equals(
Expand All @@ -75,6 +96,31 @@ public async Task POST_UpdateDataAreInvalid_DoNotUpdateInvoice()
);
}

[Fact]
public async Task POST_WebhookSignatureInvalid_DoNotUpdateInvoice()
{
// given
var invoice = CreateInvoice();
var updateDataJson = UnitTest.GetDataFromFile("invalidUpdateData.json");

// when
var result = await Post(
"/invoices/" + invoice.Uuid,
updateDataJson,
new Dictionary<string, string>
{
{ "x-signature", "randomsignature" }
}
);

// then
result.EnsureSuccessStatusCode();
UnitTest.Equals(
"new",
GetInvoiceRepository().FindById(invoice.Id).Status
);
}

private CsharpKioskDemoDotnet.Invoice.Domain.Invoice CreateInvoice()
{
var invoiceJson = UnitTest.GetDataFromFile("invoice.json");
Expand All @@ -83,4 +129,4 @@ private CsharpKioskDemoDotnet.Invoice.Domain.Invoice CreateInvoice()

return invoice;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright 2023 BitPay.
// All rights reserved.

using System;
using System.Security.Cryptography;
using System.Text;

namespace CsharpKioskDemoDotnet.Invoice.Infrastructure.Features.Tasks.UpdateInvoice;

public class WebhookVerifier
{
public bool Verify(string signingKey, string sigHeader, string webhookBody)
{
ArgumentNullException.ThrowIfNull(signingKey);
ArgumentNullException.ThrowIfNull(sigHeader);
ArgumentNullException.ThrowIfNull(webhookBody);

using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(signingKey));
byte[] signatureBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(webhookBody));
string calculated = Convert.ToBase64String(signatureBytes);
bool match = sigHeader.Equals(calculated, StringComparison.Ordinal);

return match;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,49 +3,88 @@

using CsharpKioskDemoDotnet.Invoice.Application.Features.Tasks.UpdateInvoice;
using CsharpKioskDemoDotnet.Invoice.Domain;
using CsharpKioskDemoDotnet.Invoice.Infrastructure.Features.Tasks.UpdateInvoice;
using CsharpKioskDemoDotnet.Shared;
using CsharpKioskDemoDotnet.Shared.Infrastructure;
using CsharpKioskDemoDotnet.Shared.Logger;

using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using System.Text;

using ILogger = CsharpKioskDemoDotnet.Shared.Logger.ILogger;

namespace CsharpKioskDemoDotnet.Invoice.Infrastructure.Ui.UpdateInvoice;

public class HttpUpdateInvoice : Controller
{
private readonly Application.Features.Tasks.UpdateInvoice.UpdateInvoice _updateInvoice;
private readonly IJsonToObjectConverter _jsonToObjectConverter;
private readonly WebhookVerifier _webhookVerifier;
private readonly IConfiguration _configuration;
private readonly ILogger _logger;

public HttpUpdateInvoice(
Application.Features.Tasks.UpdateInvoice.UpdateInvoice updateInvoice,
IJsonToObjectConverter jsonToObjectConverter
IJsonToObjectConverter jsonToObjectConverter,
WebhookVerifier webhookVerifier,
IConfiguration configuration,
ILogger logger
)
{
_updateInvoice = updateInvoice;
_jsonToObjectConverter = jsonToObjectConverter;
_webhookVerifier = webhookVerifier;
_configuration = configuration;
_logger = logger;
}

// POST: invoice/{uuid}
[HttpPost("invoices/{uuid}")]
public ActionResult Execute(
string uuid,
[FromBody] Dictionary<string, object> body
[FromHeader(Name = "x-signature")] string signature
)
{
var token = _configuration["BitPay:Token"];
using StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8);
var rawBody = reader.ReadToEndAsync().GetAwaiter().GetResult();

ArgumentNullException.ThrowIfNull(uuid);
ArgumentNullException.ThrowIfNull(rawBody);
ArgumentNullException.ThrowIfNull(token);

var body = JsonConvert.DeserializeObject<Dictionary<string, object>>(rawBody);

ArgumentNullException.ThrowIfNull(body);
try
{
_updateInvoice.Execute(uuid, GetData(body));

if (_webhookVerifier.Verify(token, signature, rawBody)) {
try
{
_updateInvoice.Execute(uuid, GetData(body));

return Ok();
}
catch (ValidationInvoiceUpdateDataFailedException exception)
{
return BadRequest(exception.Errors);
}
catch (InvoiceNotFoundException)
{
return NotFound();
}
} else {
_logger.Error(
LogCode.IpnSignatureVerificationFail,
"Webhook signature verification failed",
new Dictionary<string, object?>
{
{ "uuid", uuid }
}
);

return Ok();
}
catch (ValidationInvoiceUpdateDataFailedException exception)
{
return BadRequest(exception.Errors);
}
catch (InvoiceNotFoundException)
{
return NotFound();
}
}

private Dictionary<string, object?> GetData(Dictionary<string, object> body)
Expand All @@ -65,4 +104,4 @@ [FromBody] Dictionary<string, object> body

return data!;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public static void Execute(WebApplicationBuilder builder)
builder.Services.AddScoped<GetInvoiceDtoGrid>();
builder.Services.AddScoped<GetInvoiceDto>();
builder.Services.AddScoped<UpdateInvoice>();
builder.Services.AddScoped<WebhookVerifier>();

builder.Services
.AddServerSentEvents<INotificationsServerSentEventsService, NotificationsServerSentEventsService>(
Expand All @@ -70,4 +71,4 @@ public static void Execute(WebApplicationBuilder builder)
}
);
}
}
}
5 changes: 3 additions & 2 deletions CsharpKioskDemoDotnet/Src/Shared/Logger/LogCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ public enum LogCode
[Description("INVOICE_UPDATE_FAIL")] InvoiceUpdateFail,
[Description("IPN_RECEIVED")] IpnReceived,
[Description("IPN_VALIDATE_SUCCESS")] IpnValidateSuccess,
[Description("IPN_VALIDATE_FAIL")] IpnValidateFail
[Description("IPN_VALIDATE_FAIL")] IpnValidateFail,
[Description("IPN_SIGNATURE_VERIFICATION_FAIL")] IpnSignatureVerificationFail
}

public static class DescriptionAttributeExtensions
Expand All @@ -29,4 +30,4 @@ public static string GetEnumDescription(this Enum e)

return descriptionAttribute!.Description;
}
}
}

0 comments on commit 6785fe3

Please sign in to comment.