Skip to content

Commit 42491f1

Browse files
authored
Merge pull request #8 from SUMO-Kwiatkowski/SP-1024
SP-1024 Add HMAC verification
2 parents 73875bb + 3c644e3 commit 42491f1

File tree

13 files changed

+433
-97
lines changed

13 files changed

+433
-97
lines changed

app/Features/Invoice/UpdateInvoice/BaseUpdateInvoiceValidator.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
final class BaseUpdateInvoiceValidator implements UpdateInvoiceValidator
1515
{
16-
public function execute(?array $data, ?BitPayInvoice $bitPayInvoice): void
16+
public function execute(?array $data, ?BitPayInvoice $bitPayInvoice, array $headers): void
1717
{
1818
if (!$data) {
1919
throw new ValidationFailed('Missing data');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Features\Invoice\UpdateInvoice;
6+
7+
use App\Shared\Exceptions\SignatureVerificationFailed;
8+
use App\Features\Shared\Configuration\BitPayConfigurationInterface;
9+
use Psr\Log\LoggerInterface;
10+
use RuntimeException;
11+
12+
final class BitPaySignatureValidator
13+
{
14+
public const MISSING_SIGNATURE_MESSAGE = 'Missing signature header';
15+
public const INVALID_SIGNATURE_MESSAGE = 'Invalid signature';
16+
public const MISSING_TOKEN_MESSAGE = 'Invalid BitPay configuration - missing token';
17+
18+
private BitPayConfigurationInterface $bitpayConfiguration;
19+
public function __construct(
20+
BitPayConfigurationInterface $bitpayConfiguration,
21+
) {
22+
$this->bitpayConfiguration = $bitpayConfiguration;
23+
}
24+
25+
public function execute(array $data, array $headers): void
26+
{
27+
$token = $this->bitpayConfiguration->getToken();
28+
29+
if (!$token) {
30+
throw new RuntimeException(self::MISSING_TOKEN_MESSAGE);
31+
}
32+
33+
$sigHeader = $headers['x-signature'][0] ?? null;
34+
35+
if (!$sigHeader) {
36+
throw new SignatureVerificationFailed(self::MISSING_SIGNATURE_MESSAGE);
37+
}
38+
39+
$hmac = base64_encode(hash_hmac(
40+
'sha256',
41+
json_encode($data),
42+
$token,
43+
true
44+
));
45+
46+
if ($sigHeader !== $hmac) {
47+
throw new SignatureVerificationFailed(self::INVALID_SIGNATURE_MESSAGE);
48+
}
49+
}
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Features\Invoice\UpdateInvoice;
6+
7+
interface BitPaySignatureValidatorInterface
8+
{
9+
/**
10+
* Validates the BitPay signature against the provided data and headers
11+
*
12+
* @param array $data The payload data to validate
13+
* @param array $headers The headers containing the signature - x-signature
14+
*
15+
* @throws \App\Shared\Exceptions\SignatureVerificationFailed When signature is missing or invalid
16+
* @throws \RuntimeException When BitPay token is missing
17+
*/
18+
public function execute(array $data, array $headers): void;
19+
}

app/Features/Invoice/UpdateInvoice/UpdateInvoiceIpnValidator.php

+10-4
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,23 @@ final class UpdateInvoiceIpnValidator implements UpdateInvoiceValidator
2020

2121
private UpdateInvoiceValidator $updateInvoiceValidator;
2222
private Logger $logger;
23+
private BitPaySignatureValidator $bitPaySignatureValidator;
2324

24-
public function __construct(UpdateInvoiceValidator $updateInvoiceValidator, Logger $logger)
25-
{
25+
public function __construct(
26+
UpdateInvoiceValidator $updateInvoiceValidator,
27+
Logger $logger,
28+
BitPaySignatureValidator $bitPaySignatureValidator
29+
) {
2630
$this->updateInvoiceValidator = $updateInvoiceValidator;
2731
$this->logger = $logger;
32+
$this->bitPaySignatureValidator = $bitPaySignatureValidator;
2833
}
2934

30-
public function execute(?array $data, ?BitPayInvoice $bitPayInvoice): void
35+
public function execute(?array $data, ?BitPayInvoice $bitPayInvoice, ?array $headers): void
3136
{
3237
try {
33-
$this->updateInvoiceValidator->execute($data, $bitPayInvoice);
38+
$this->bitPaySignatureValidator->execute($data, $headers);
39+
$this->updateInvoiceValidator->execute($data, $bitPayInvoice, $headers);
3440

3541
if (!$bitPayInvoice) {
3642
throw new ValidationFailed(self::MISSING_BITPAY_MESSAGE);

app/Features/Invoice/UpdateInvoice/UpdateInvoiceUsingBitPayIpn.php

+9-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use App\Models\Invoice\InvoicePayment;
1717
use App\Models\Invoice\InvoicePaymentCurrency;
1818
use App\Models\Invoice\InvoiceRepositoryInterface;
19+
use App\Shared\Exceptions\SignatureVerificationFailed;
1920

2021
class UpdateInvoiceUsingBitPayIpn
2122
{
@@ -45,7 +46,7 @@ public function __construct(
4546
$this->bitPayConfiguration = $bitPayConfiguration;
4647
}
4748

48-
public function execute(string $uuid, array $data): void
49+
public function execute(string $uuid, array $data, array $headers): void
4950
{
5051
$invoice = $this->invoiceRepository->findOneByUuid($uuid);
5152
if (!$invoice) {
@@ -54,18 +55,24 @@ public function execute(string $uuid, array $data): void
5455

5556
try {
5657
$client = $this->bitPayClientFactory->create();
58+
5759
$bitPayInvoice = $client->getInvoice(
5860
$invoice->getBitpayId(),
5961
$this->bitPayConfiguration->getFacade(),
6062
$this->bitPayConfiguration->isSignRequest()
6163
);
6264

6365
$updateInvoiceData = $this->bitPayUpdateMapper->execute($data)->toArray();
64-
$this->updateInvoiceValidator->execute($data, $bitPayInvoice);
66+
$this->updateInvoiceValidator->execute($data, $bitPayInvoice, $headers);
6567

6668
$this->updateInvoice($invoice, $updateInvoiceData);
6769

6870
$this->sendUpdateInvoiceNotification->execute($invoice, $data['event'] ?? null);
71+
} catch (SignatureVerificationFailed $e) {
72+
$this->logger->error('SIGNATURE_VERIFICATION_FAIL', $e->getMessage(), [
73+
'id' => $invoice->id
74+
]);
75+
throw $e;
6976
} catch (\Exception | \TypeError $e) {
7077
$this->logger->error('INVOICE_UPDATE_FAIL', 'Failed to update invoice', [
7178
'id' => $invoice->id

app/Features/Invoice/UpdateInvoice/UpdateInvoiceValidator.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@ interface UpdateInvoiceValidator
1616
/**
1717
* @throws ValidationFailed
1818
*/
19-
public function execute(?array $data, ?BitPayInvoice $bitPayInvoice): void;
19+
public function execute(?array $data, ?BitPayInvoice $bitPayInvoice, array $headers): void;
2020
}

app/Http/Controllers/Invoice/UpdateInvoiceController.php

+7-4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use App\Http\Controllers\Controller;
1515
use Illuminate\Http\Request;
1616
use Symfony\Component\HttpFoundation\Response;
17+
use App\Shared\Exceptions\SignatureVerificationFailed;
1718

1819
class UpdateInvoiceController extends Controller
1920
{
@@ -30,17 +31,19 @@ public function execute(Request $request, string $uuid): Response
3031
{
3132
$this->logger->info('IPN_RECEIVED', 'Received IPN', $request->request->all());
3233

33-
/** @var array $data */
34-
$data = $request->request->get('data');
35-
$event = $request->request->get('event');
34+
$payload = json_decode($request->getContent(), true);
35+
$data = $payload['data'];
36+
$event = $payload['event'];
3637

3738
$data['uuid'] = $uuid;
3839
$data['event'] = $event['name'] ?? null;
3940

4041
try {
41-
$this->updateInvoice->execute($uuid, $data);
42+
$this->updateInvoice->execute($uuid, $data, $request->headers->all());
4243
} catch (MissingInvoice $e) {
4344
return response(null, Response::HTTP_NOT_FOUND);
45+
} catch (SignatureVerificationFailed $e) {
46+
return response($e->getMessage(), Response::HTTP_UNAUTHORIZED);
4447
} catch (\Exception $e) {
4548
return response('Unable to process update', Response::HTTP_BAD_REQUEST);
4649
}

app/Infrastructure/Laravel/AppServiceProvider.php

+9-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
use App\Features\Invoice\UpdateInvoice\SendUpdateInvoiceEventStream;
1818
use App\Features\Invoice\UpdateInvoice\UpdateInvoiceIpnValidator;
1919
use App\Features\Invoice\UpdateInvoice\UpdateInvoiceValidator;
20+
use App\Features\Invoice\UpdateInvoice\BitPaySignatureValidator;
21+
use App\Features\Invoice\UpdateInvoice\BitPaySignatureValidatorInterface;
2022
use App\Features\Shared\Logger;
2123
use App\Features\Shared\SseConfiguration;
2224
use App\Features\Shared\UrlProvider;
@@ -87,12 +89,18 @@ function () {
8789
SseMercureConfiguration::class
8890
);
8991

92+
$this->app->bind(
93+
BitPaySignatureValidatorInterface::class,
94+
BitPaySignatureValidator::class
95+
);
96+
9097
$this->app->bind(
9198
UpdateInvoiceIpnValidator::class,
9299
function () {
93100
return new UpdateInvoiceIpnValidator(
94101
$this->app->make(BaseUpdateInvoiceValidator::class),
95-
$this->app->make(Logger::class)
102+
$this->app->make(Logger::class),
103+
$this->app->make(BitPaySignatureValidator::class)
96104
);
97105
}
98106
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
/**
4+
* Copyright (c) 2019 BitPay
5+
**/
6+
7+
declare(strict_types=1);
8+
9+
namespace App\Shared\Exceptions;
10+
11+
class SignatureVerificationFailed extends \RuntimeException
12+
{
13+
}

routes/web.php

-1
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,3 @@
2323
Route::post('/invoices', [CreateInvoiceController::class, 'execute'])->name('createInvoice');
2424
Route::get('/invoices/{id}', [GetInvoiceViewController::class, 'execute'])->name('invoiceView');
2525
Route::post('/invoices/{uuid}', [UpdateInvoiceController::class, 'execute'])->name('updateInvoice');
26-

0 commit comments

Comments
 (0)