Skip to content

Commit ba52121

Browse files
authored
Merge pull request #111 from swlodarski-sumoheavy/10.0.x
SP-966 - Validate incoming webhooks
2 parents 6e518c4 + f9e17bd commit ba52121

File tree

14 files changed

+363
-62
lines changed

14 files changed

+363
-62
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Bitpay\BPCheckout\Exception;
5+
6+
class HMACVerificationException extends \Exception
7+
{
8+
9+
}

Model/BPRedirect.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class BPRedirect
3737
protected OrderRepository $orderRepository;
3838
protected BitpayInvoiceRepository $bitpayInvoiceRepository;
3939
protected ReturnHash $returnHashHelper;
40+
protected EncryptorInterface $encryptor;
4041

4142
/**
4243
* @param Session $checkoutSession
@@ -53,6 +54,7 @@ class BPRedirect
5354
* @param OrderRepository $orderRepository
5455
* @param BitpayInvoiceRepository $bitpayInvoiceRepository
5556
* @param ReturnHash $returnHashHelper
57+
* @param EncryptorInterface $encryptor
5658
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
5759
*/
5860
public function __construct(
@@ -69,7 +71,8 @@ public function __construct(
6971
Client $client,
7072
OrderRepository $orderRepository,
7173
BitpayInvoiceRepository $bitpayInvoiceRepository,
72-
ReturnHash $returnHashHelper
74+
ReturnHash $returnHashHelper,
75+
EncryptorInterface $encryptor,
7376
) {
7477
$this->checkoutSession = $checkoutSession;
7578
$this->orderInterface = $orderInterface;
@@ -85,6 +88,7 @@ public function __construct(
8588
$this->orderRepository = $orderRepository;
8689
$this->bitpayInvoiceRepository = $bitpayInvoiceRepository;
8790
$this->returnHashHelper = $returnHashHelper;
91+
$this->encryptor = $encryptor;
8892
}
8993

9094
/**
@@ -150,7 +154,8 @@ public function execute(ResultInterface $defaultResult, string $returnId = null)
150154
$order->getId(),
151155
$invoiceID,
152156
$invoice->getExpirationTime(),
153-
$invoice->getAcceptanceWindow()
157+
$invoice->getAcceptanceWindow(),
158+
$this->encryptor->encrypt($this->config->getToken())
154159
);
155160
$this->transactionRepository->add($incrementId, $invoiceID, 'new');
156161

Model/BitpayInvoiceRepository.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,17 @@ public function __construct(BitpayInvoice $bitpayInvoice)
2121
* @param string $invoiceID
2222
* @param int $expirationTime
2323
* @param int|null $acceptanceWindow
24+
* @param string|null $bitpayToken
2425
* @return void
2526
*/
26-
public function add(string $orderId, string $invoiceID, int $expirationTime, ?int $acceptanceWindow): void
27-
{
28-
$this->bitpayInvoice->add($orderId, $invoiceID, $expirationTime, $acceptanceWindow);
27+
public function add(
28+
string $orderId,
29+
string $invoiceID,
30+
int $expirationTime,
31+
?int $acceptanceWindow,
32+
?string $bitpayToken
33+
): void {
34+
$this->bitpayInvoice->add($orderId, $invoiceID, $expirationTime, $acceptanceWindow, $bitpayToken);
2935
}
3036

3137
/**

Model/Ipn/WebhookVerifier.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Bitpay\BPCheckout\Model\Ipn;
5+
6+
class WebhookVerifier
7+
{
8+
/**
9+
* Verify the validity of webhooks (HMAC)
10+
*
11+
* @see https://developer.bitpay.com/reference/hmac-verification
12+
*
13+
* @param string $signingKey
14+
* @param string $sigHeader
15+
* @param string $webhookBody
16+
*
17+
* @return bool
18+
*/
19+
public function isValidHmac(string $signingKey, string $sigHeader, string $webhookBody): bool
20+
{
21+
$hmac = base64_encode(
22+
hash_hmac(
23+
'sha256',
24+
$webhookBody,
25+
$signingKey,
26+
true
27+
)
28+
);
29+
30+
return $sigHeader === $hmac;
31+
}
32+
}

Model/IpnManagement.php

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,23 @@
55

66
use Bitpay\BPCheckout\Api\IpnManagementInterface;
77
use Bitpay\BPCheckout\Exception\IPNValidationException;
8+
use Bitpay\BPCheckout\Exception\HMACVerificationException;
89
use Bitpay\BPCheckout\Helper\ReturnHash;
910
use Bitpay\BPCheckout\Logger\Logger;
1011
use Bitpay\BPCheckout\Model\Ipn\BPCItem;
1112
use Bitpay\BPCheckout\Model\Ipn\Validator;
13+
use Bitpay\BPCheckout\Model\Ipn\WebhookVerifier;
1214
use Magento\Checkout\Model\Session;
1315
use Magento\Framework\App\ResponseFactory;
1416
use Magento\Framework\DataObject;
17+
use Magento\Framework\Encryption\EncryptorInterface;
1518
use Magento\Framework\Registry;
1619
use Magento\Framework\Serialize\Serializer\Json;
1720
use Magento\Framework\UrlInterface;
1821
use Magento\Framework\Webapi\Rest\Request;
1922
use Magento\Framework\Webapi\Rest\Response;
2023
use Magento\Quote\Model\QuoteFactory;
21-
use Magento\Sales\Api\Data\OrderInterface;
24+
use Magento\Sales\Model\OrderFactory;
2225

2326
/**
2427
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
@@ -32,7 +35,7 @@ class IpnManagement implements IpnManagementInterface
3235
protected UrlInterface $url;
3336
protected Session $checkoutSession;
3437
protected QuoteFactory $quoteFactory;
35-
protected OrderInterface $orderInterface;
38+
protected OrderFactory $orderFactory;
3639
protected Registry $coreRegistry;
3740
protected Logger $logger;
3841
protected Config $config;
@@ -42,14 +45,33 @@ class IpnManagement implements IpnManagementInterface
4245
protected Request $request;
4346
protected Client $client;
4447
protected Response $response;
48+
49+
/**
50+
* @var BitpayInvoiceRepository
51+
*/
52+
protected BitpayInvoiceRepository $bitpayInvoiceRepository;
53+
54+
/**
55+
* @var EncryptorInterface
56+
*/
57+
protected EncryptorInterface $encryptor;
58+
59+
/**
60+
* @var WebhookVerifier
61+
*/
62+
protected WebhookVerifier $webhookVerifier;
63+
64+
/**
65+
* @var ReturnHash
66+
*/
4567
protected ReturnHash $returnHashHelper;
4668

4769
/**
4870
* @param ResponseFactory $responseFactory
4971
* @param UrlInterface $url
5072
* @param Registry $registry
5173
* @param Session $checkoutSession
52-
* @param OrderInterface $orderInterface
74+
* @param OrderFactory $orderFactory
5375
* @param QuoteFactory $quoteFactory
5476
* @param Logger $logger
5577
* @param Config $config
@@ -59,6 +81,9 @@ class IpnManagement implements IpnManagementInterface
5981
* @param Request $request
6082
* @param Client $client
6183
* @param Response $response
84+
* @param BitpayInvoiceRepository $bitpayInvoiceRepository
85+
* @param EncryptorInterface $encryptor
86+
* @param WebhookVerifier $webhookVerifier
6287
* @param ReturnHash $returnHashHelper
6388
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
6489
*/
@@ -67,7 +92,7 @@ public function __construct(
6792
UrlInterface $url,
6893
Registry $registry,
6994
Session $checkoutSession,
70-
OrderInterface $orderInterface,
95+
OrderFactory $orderFactory,
7196
QuoteFactory $quoteFactory,
7297
Logger $logger,
7398
Config $config,
@@ -77,13 +102,16 @@ public function __construct(
77102
Request $request,
78103
Client $client,
79104
Response $response,
105+
BitpayInvoiceRepository $bitpayInvoiceRepository,
106+
EncryptorInterface $encryptor,
107+
WebhookVerifier $webhookVerifier,
80108
ReturnHash $returnHashHelper
81109
) {
82110
$this->coreRegistry = $registry;
83111
$this->responseFactory = $responseFactory;
84112
$this->url = $url;
85113
$this->quoteFactory = $quoteFactory;
86-
$this->orderInterface = $orderInterface;
114+
$this->orderFactory = $orderFactory;
87115
$this->checkoutSession = $checkoutSession;
88116
$this->logger = $logger;
89117
$this->config = $config;
@@ -93,6 +121,9 @@ public function __construct(
93121
$this->request = $request;
94122
$this->client = $client;
95123
$this->response = $response;
124+
$this->bitpayInvoiceRepository = $bitpayInvoiceRepository;
125+
$this->encryptor = $encryptor;
126+
$this->webhookVerifier = $webhookVerifier;
96127
$this->returnHashHelper = $returnHashHelper;
97128
}
98129

@@ -108,7 +139,7 @@ public function postClose()
108139
$response = $this->responseFactory->create();
109140
try {
110141
$orderID = $this->request->getParam('orderID', null);
111-
$order = $this->orderInterface->loadByIncrementId($orderID);
142+
$order = $this->orderFactory->create()->loadByIncrementId($orderID);
112143
$invoiceCloseHandling = $this->config->getBitpayInvoiceCloseHandling();
113144
if ($this->config->getBitpayCheckoutSuccess() === 'standard' && $invoiceCloseHandling === 'keep_order') {
114145
$this->checkoutSession->setLastSuccessQuoteId($order->getQuoteId())
@@ -151,10 +182,22 @@ public function postClose()
151182
public function postIpn()
152183
{
153184
try {
154-
$allData = $this->serializer->unserialize($this->request->getContent());
185+
$requestBody = $this->request->getContent();
186+
$allData = $this->serializer->unserialize($requestBody);
155187
$data = $allData['data'];
156188
$event = $allData['event'];
157189
$orderId = $data['orderId'];
190+
191+
$bitPayInvoiceData = $this->bitpayInvoiceRepository->getByOrderId($orderId);
192+
if (!empty($bitPayInvoiceData['bitpay_token'])) {
193+
$signingKey = $this->encryptor->decrypt($bitPayInvoiceData['bitpay_token']);
194+
$xSignature = $this->request->getHeader('x-signature');
195+
196+
if (!$this->webhookVerifier->isValidHmac($signingKey, $xSignature, $requestBody)) {
197+
throw new HMACVerificationException('HMAC Verification Failed!');
198+
}
199+
}
200+
158201
$orderInvoiceId = $data['id'];
159202
$row = $this->transactionRepository->findBy($orderId, $orderInvoiceId);
160203
$client = $this->client->initialize();
@@ -179,7 +222,7 @@ public function postIpn()
179222
$invoiceStatus = $this->invoice->getBPCCheckInvoiceStatus($client, $orderInvoiceId);
180223
$updateWhere = ['order_id = ?' => $orderId, 'transaction_id = ?' => $orderInvoiceId];
181224
$this->transactionRepository->update('transaction_status', $invoiceStatus, $updateWhere);
182-
$order = $this->orderInterface->loadByIncrementId($orderId);
225+
$order = $this->orderFactory->create()->loadByIncrementId($orderId);
183226
switch ($event['name']) {
184227
case Invoice::COMPLETED:
185228
if ($invoiceStatus == 'complete') {

Model/ResourceModel/BitpayInvoice.php

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,16 @@ public function _construct()
2626
* @param string $invoiceID
2727
* @param int $expirationTime
2828
* @param int|null $acceptanceWindow
29+
* @param string|null $bitpayToken
2930
* @return void
3031
*/
31-
public function add(string $orderId, string $invoiceID, int $expirationTime, ?int $acceptanceWindow)
32-
{
32+
public function add(
33+
string $orderId,
34+
string $invoiceID,
35+
int $expirationTime,
36+
?int $acceptanceWindow,
37+
?string $bitpayToken
38+
) {
3339
$connection = $this->getConnection();
3440
$table_name = $connection->getTableName(self::TABLE_NAME);
3541
$connection->insert(
@@ -38,7 +44,8 @@ public function add(string $orderId, string $invoiceID, int $expirationTime, ?in
3844
'order_id' => $orderId,
3945
'invoice_id' => $invoiceID,
4046
'expiration_time' => $expirationTime,
41-
'acceptance_window'=> $acceptanceWindow
47+
'acceptance_window'=> $acceptanceWindow,
48+
'bitpay_token' => $bitpayToken
4249
]
4350
);
4451
}

Test/Integration/Model/BPRedirectTest.php

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use Magento\Sales\Model\OrderRepository;
2525
use Magento\TestFramework\Helper\Bootstrap;
2626
use PHPUnit\Framework\TestCase;
27+
use PHPUnit\Framework\MockObject\MockObject;
2728
use Magento\Framework\Encryption\EncryptorInterface;
2829

2930
/**
@@ -111,27 +112,45 @@ class BPRedirectTest extends TestCase
111112
* @var EncryptorInterface|MockObject $encryptor
112113
*/
113114
private $encryptor;
115+
116+
/**
117+
* @var ReturnHash $returnHash
118+
*/
119+
private $returnHash;
120+
121+
114122
public function setUp(): void
115123
{
116124
$this->objectManager = Bootstrap::getObjectManager();
117125
$this->checkoutSession = $this->objectManager->get(Session::class);
118126
$this->orderInterface = $this->objectManager->get(OrderInterface::class);
119127
$this->config = $this->objectManager->get(Config::class);
120128
$this->transactionRepository = $this->objectManager->get(TransactionRepository::class);
129+
/**
130+
* @var Invoice|MockObject
131+
*/
121132
$this->invoice = $this->getMockBuilder(Invoice::class)->disableOriginalConstructor()->getMock();
122133
$this->messageManager = $this->objectManager->get(Manager::class);
123134
$this->registry = $this->objectManager->get(Registry::class);
124135
$this->url = $this->objectManager->get(UrlInterface::class);
125136
$this->logger = $this->objectManager->get(Logger::class);
126137
$this->resultFactory = $this->objectManager->get(ResultFactory::class);
138+
/**
139+
* @var Client|MockObject
140+
*/
127141
$this->client = $this->getMockBuilder(Client::class)->disableOriginalConstructor()->getMock();
128142
$this->orderRepository = $this->objectManager->get(OrderRepository::class);
129143
$this->bitpayInvoiceRepository = $this->objectManager->get(BitpayInvoiceRepository::class);
130144
$this->bitpayInvoiceRepository = $this->objectManager->get(BitpayInvoiceRepository::class);
145+
/**
146+
* @var EncryptorInterface|MockObject
147+
*/
131148
$this->encryptor = $this->getMockBuilder(EncryptorInterface::class)
132149
->disableOriginalConstructor()
133150
->getMock();
134151

152+
$this->returnHash = $this->objectManager->get(ReturnHash::class);
153+
135154
$this->bpRedirect = new BPRedirect(
136155
$this->checkoutSession,
137156
$this->orderInterface,
@@ -146,6 +165,7 @@ public function setUp(): void
146165
$this->client,
147166
$this->orderRepository,
148167
$this->bitpayInvoiceRepository,
168+
$this->returnHash,
149169
$this->encryptor
150170
);
151171
}
@@ -197,7 +217,6 @@ public function testExecute(): void
197217
$this->assertEquals('100000001', $result[0]['order_id']);
198218
$this->assertEquals('new', $result[0]['transaction_status']);
199219
$this->assertEquals('test', $this->config->getBitpayEnv());
200-
$this->assertEquals('redirect', $this->config->getBitpayUx());
201220
$this->assertEquals($bitpayMethodCode, $methodCode);
202221
}
203222

0 commit comments

Comments
 (0)