Skip to content

Commit 8857369

Browse files
SP-1022 - C# Kiosk Demo: Add HMAC verification
1 parent 3e30150 commit 8857369

File tree

6 files changed

+143
-23
lines changed

6 files changed

+143
-23
lines changed

CsharpKioskDemoDotnet.IntegrationTests/AbstractUiIntegrationTest.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ protected Task<HttpResponseMessage> Get(string url)
6969

7070
protected Task<HttpResponseMessage> Post(
7171
string url,
72-
string jsonRequest
72+
string jsonRequest,
73+
Dictionary<string, string>? headers = null
7374
)
7475
{
7576
var httpContent = new StringContent(
@@ -78,6 +79,13 @@ string jsonRequest
7879
"application/json"
7980
);
8081

82+
if (headers != null) {
83+
foreach(var item in headers)
84+
{
85+
httpContent.Headers.Add(item.Key, item.Value);
86+
}
87+
}
88+
8189
return _client.PostAsync(url, httpContent);
8290
}
8391

@@ -106,4 +114,4 @@ public void Info(LogCode code, string message, Dictionary<string, object?> conte
106114
public void Error(LogCode code, string message, Dictionary<string, object?> context)
107115
{
108116
}
109-
}
117+
}

CsharpKioskDemoDotnet.IntegrationTests/Src/Invoice/Infrastructure/Ui/UpdateInvoice/UpdateInvoiceIntegrationTest.cs

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,14 @@ public async Task POST_InvoiceExistsForUuidAndUpdateDataAreValid_UpdateInvoice()
1919
var updateDataJson = UnitTest.GetDataFromFile("updateData.json");
2020

2121
// when
22-
var result = await Post("/invoices/" + invoice.Uuid, updateDataJson);
22+
var result = await Post(
23+
"/invoices/" + invoice.Uuid,
24+
updateDataJson,
25+
new Dictionary<string, string>
26+
{
27+
{ "x-signature", "bKGK0WgsFfMSEg4fpik9+OdjYrYNA1E99kI1QJmbfKw=" }
28+
}
29+
);
2330

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

3946
// when
40-
var result = await Post("/invoices/12312412", updateDataJson);
47+
var result = await Post(
48+
"/invoices/12312412",
49+
updateDataJson,
50+
new Dictionary<string, string>
51+
{
52+
{ "x-signature", "bKGK0WgsFfMSEg4fpik9+OdjYrYNA1E99kI1QJmbfKw=" }
53+
}
54+
);
4155

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

6074
// when
61-
var result = await Post("/invoices/" + invoice.Uuid, updateDataJson);
75+
var result = await Post(
76+
"/invoices/" + invoice.Uuid,
77+
updateDataJson,
78+
new Dictionary<string, string>
79+
{
80+
{ "x-signature", "16imUAXdJqur7yyQyDRRfcbPCeMPiuBFnNJVLlpi3hQ=" }
81+
}
82+
);
6283

6384
// then
6485
UnitTest.Equals(
@@ -75,6 +96,31 @@ public async Task POST_UpdateDataAreInvalid_DoNotUpdateInvoice()
7596
);
7697
}
7798

99+
[Fact]
100+
public async Task POST_WebhookSignatureInvalid_DoNotUpdateInvoice()
101+
{
102+
// given
103+
var invoice = CreateInvoice();
104+
var updateDataJson = UnitTest.GetDataFromFile("invalidUpdateData.json");
105+
106+
// when
107+
var result = await Post(
108+
"/invoices/" + invoice.Uuid,
109+
updateDataJson,
110+
new Dictionary<string, string>
111+
{
112+
{ "x-signature", "randomsignature" }
113+
}
114+
);
115+
116+
// then
117+
result.EnsureSuccessStatusCode();
118+
UnitTest.Equals(
119+
"new",
120+
GetInvoiceRepository().FindById(invoice.Id).Status
121+
);
122+
}
123+
78124
private CsharpKioskDemoDotnet.Invoice.Domain.Invoice CreateInvoice()
79125
{
80126
var invoiceJson = UnitTest.GetDataFromFile("invoice.json");
@@ -83,4 +129,4 @@ private CsharpKioskDemoDotnet.Invoice.Domain.Invoice CreateInvoice()
83129

84130
return invoice;
85131
}
86-
}
132+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright 2023 BitPay.
2+
// All rights reserved.
3+
4+
using System;
5+
using System.Security.Cryptography;
6+
using System.Text;
7+
8+
namespace CsharpKioskDemoDotnet.Invoice.Infrastructure.Features.Tasks.UpdateInvoice;
9+
10+
public class WebhookVerifier
11+
{
12+
public bool Verify(string signingKey, string sigHeader, string webhookBody)
13+
{
14+
ArgumentNullException.ThrowIfNull(signingKey);
15+
ArgumentNullException.ThrowIfNull(sigHeader);
16+
ArgumentNullException.ThrowIfNull(webhookBody);
17+
18+
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(signingKey));
19+
byte[] signatureBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(webhookBody));
20+
string calculated = Convert.ToBase64String(signatureBytes);
21+
bool match = sigHeader.Equals(calculated, StringComparison.Ordinal);
22+
23+
return match;
24+
}
25+
}

CsharpKioskDemoDotnet/Src/Invoice/Infrastructure/Ui/UpdateInvoice/HttpUpdateInvoice.cs

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,49 +3,88 @@
33

44
using CsharpKioskDemoDotnet.Invoice.Application.Features.Tasks.UpdateInvoice;
55
using CsharpKioskDemoDotnet.Invoice.Domain;
6+
using CsharpKioskDemoDotnet.Invoice.Infrastructure.Features.Tasks.UpdateInvoice;
67
using CsharpKioskDemoDotnet.Shared;
8+
using CsharpKioskDemoDotnet.Shared.Infrastructure;
9+
using CsharpKioskDemoDotnet.Shared.Logger;
710

811
using Microsoft.AspNetCore.Mvc;
12+
using Newtonsoft.Json;
13+
using System.Text;
14+
15+
using ILogger = CsharpKioskDemoDotnet.Shared.Logger.ILogger;
916

1017
namespace CsharpKioskDemoDotnet.Invoice.Infrastructure.Ui.UpdateInvoice;
1118

1219
public class HttpUpdateInvoice : Controller
1320
{
1421
private readonly Application.Features.Tasks.UpdateInvoice.UpdateInvoice _updateInvoice;
1522
private readonly IJsonToObjectConverter _jsonToObjectConverter;
23+
private readonly WebhookVerifier _webhookVerifier;
24+
private readonly IConfiguration _configuration;
25+
private readonly ILogger _logger;
1626

1727
public HttpUpdateInvoice(
1828
Application.Features.Tasks.UpdateInvoice.UpdateInvoice updateInvoice,
19-
IJsonToObjectConverter jsonToObjectConverter
29+
IJsonToObjectConverter jsonToObjectConverter,
30+
WebhookVerifier webhookVerifier,
31+
IConfiguration configuration,
32+
ILogger logger
2033
)
2134
{
2235
_updateInvoice = updateInvoice;
2336
_jsonToObjectConverter = jsonToObjectConverter;
37+
_webhookVerifier = webhookVerifier;
38+
_configuration = configuration;
39+
_logger = logger;
2440
}
2541

2642
// POST: invoice/{uuid}
2743
[HttpPost("invoices/{uuid}")]
2844
public ActionResult Execute(
2945
string uuid,
30-
[FromBody] Dictionary<string, object> body
46+
[FromHeader(Name = "x-signature")] string signature
3147
)
3248
{
49+
var token = _configuration["BitPay:Token"];
50+
using StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8);
51+
var rawBody = reader.ReadToEndAsync().GetAwaiter().GetResult();
52+
3353
ArgumentNullException.ThrowIfNull(uuid);
54+
ArgumentNullException.ThrowIfNull(rawBody);
55+
ArgumentNullException.ThrowIfNull(token);
56+
57+
var body = JsonConvert.DeserializeObject<Dictionary<string, object>>(rawBody);
58+
3459
ArgumentNullException.ThrowIfNull(body);
35-
try
36-
{
37-
_updateInvoice.Execute(uuid, GetData(body));
60+
61+
if (_webhookVerifier.Verify(token, signature, rawBody)) {
62+
try
63+
{
64+
_updateInvoice.Execute(uuid, GetData(body));
65+
66+
return Ok();
67+
}
68+
catch (ValidationInvoiceUpdateDataFailedException exception)
69+
{
70+
return BadRequest(exception.Errors);
71+
}
72+
catch (InvoiceNotFoundException)
73+
{
74+
return NotFound();
75+
}
76+
} else {
77+
_logger.Error(
78+
LogCode.IpnSignatureVerificationFail,
79+
"Webhook signature verification failed",
80+
new Dictionary<string, object?>
81+
{
82+
{ "uuid", uuid }
83+
}
84+
);
3885

3986
return Ok();
4087
}
41-
catch (ValidationInvoiceUpdateDataFailedException exception)
42-
{
43-
return BadRequest(exception.Errors);
44-
}
45-
catch (InvoiceNotFoundException)
46-
{
47-
return NotFound();
48-
}
4988
}
5089

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

66105
return data!;
67106
}
68-
}
107+
}

CsharpKioskDemoDotnet/Src/Shared/Infrastructure/DependencyInjectionConfiguration.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ public static void Execute(WebApplicationBuilder builder)
6060
builder.Services.AddScoped<GetInvoiceDtoGrid>();
6161
builder.Services.AddScoped<GetInvoiceDto>();
6262
builder.Services.AddScoped<UpdateInvoice>();
63+
builder.Services.AddScoped<WebhookVerifier>();
6364

6465
builder.Services
6566
.AddServerSentEvents<INotificationsServerSentEventsService, NotificationsServerSentEventsService>(
@@ -70,4 +71,4 @@ public static void Execute(WebApplicationBuilder builder)
7071
}
7172
);
7273
}
73-
}
74+
}

CsharpKioskDemoDotnet/Src/Shared/Logger/LogCode.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ public enum LogCode
1515
[Description("INVOICE_UPDATE_FAIL")] InvoiceUpdateFail,
1616
[Description("IPN_RECEIVED")] IpnReceived,
1717
[Description("IPN_VALIDATE_SUCCESS")] IpnValidateSuccess,
18-
[Description("IPN_VALIDATE_FAIL")] IpnValidateFail
18+
[Description("IPN_VALIDATE_FAIL")] IpnValidateFail,
19+
[Description("IPN_SIGNATURE_VERIFICATION_FAIL")] IpnSignatureVerificationFail
1920
}
2021

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

3031
return descriptionAttribute!.Description;
3132
}
32-
}
33+
}

0 commit comments

Comments
 (0)