Skip to content

Commit

Permalink
Merge pull request #111 from swlodarski-sumoheavy/10.0.x
Browse files Browse the repository at this point in the history
SP-966 - Validate incoming webhooks
  • Loading branch information
p-maguire authored Dec 19, 2024
2 parents 6e518c4 + f9e17bd commit ba52121
Show file tree
Hide file tree
Showing 14 changed files with 363 additions and 62 deletions.
9 changes: 9 additions & 0 deletions Exception/HMACVerificationException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);

namespace Bitpay\BPCheckout\Exception;

class HMACVerificationException extends \Exception
{

}
9 changes: 7 additions & 2 deletions Model/BPRedirect.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class BPRedirect
protected OrderRepository $orderRepository;
protected BitpayInvoiceRepository $bitpayInvoiceRepository;
protected ReturnHash $returnHashHelper;
protected EncryptorInterface $encryptor;

/**
* @param Session $checkoutSession
Expand All @@ -53,6 +54,7 @@ class BPRedirect
* @param OrderRepository $orderRepository
* @param BitpayInvoiceRepository $bitpayInvoiceRepository
* @param ReturnHash $returnHashHelper
* @param EncryptorInterface $encryptor
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
Expand All @@ -69,7 +71,8 @@ public function __construct(
Client $client,
OrderRepository $orderRepository,
BitpayInvoiceRepository $bitpayInvoiceRepository,
ReturnHash $returnHashHelper
ReturnHash $returnHashHelper,
EncryptorInterface $encryptor,
) {
$this->checkoutSession = $checkoutSession;
$this->orderInterface = $orderInterface;
Expand All @@ -85,6 +88,7 @@ public function __construct(
$this->orderRepository = $orderRepository;
$this->bitpayInvoiceRepository = $bitpayInvoiceRepository;
$this->returnHashHelper = $returnHashHelper;
$this->encryptor = $encryptor;
}

/**
Expand Down Expand Up @@ -150,7 +154,8 @@ public function execute(ResultInterface $defaultResult, string $returnId = null)
$order->getId(),
$invoiceID,
$invoice->getExpirationTime(),
$invoice->getAcceptanceWindow()
$invoice->getAcceptanceWindow(),
$this->encryptor->encrypt($this->config->getToken())
);
$this->transactionRepository->add($incrementId, $invoiceID, 'new');

Expand Down
12 changes: 9 additions & 3 deletions Model/BitpayInvoiceRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,17 @@ public function __construct(BitpayInvoice $bitpayInvoice)
* @param string $invoiceID
* @param int $expirationTime
* @param int|null $acceptanceWindow
* @param string|null $bitpayToken
* @return void
*/
public function add(string $orderId, string $invoiceID, int $expirationTime, ?int $acceptanceWindow): void
{
$this->bitpayInvoice->add($orderId, $invoiceID, $expirationTime, $acceptanceWindow);
public function add(
string $orderId,
string $invoiceID,
int $expirationTime,
?int $acceptanceWindow,
?string $bitpayToken
): void {
$this->bitpayInvoice->add($orderId, $invoiceID, $expirationTime, $acceptanceWindow, $bitpayToken);
}

/**
Expand Down
32 changes: 32 additions & 0 deletions Model/Ipn/WebhookVerifier.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);

namespace Bitpay\BPCheckout\Model\Ipn;

class WebhookVerifier
{
/**
* Verify the validity of webhooks (HMAC)
*
* @see https://developer.bitpay.com/reference/hmac-verification
*
* @param string $signingKey
* @param string $sigHeader
* @param string $webhookBody
*
* @return bool
*/
public function isValidHmac(string $signingKey, string $sigHeader, string $webhookBody): bool
{
$hmac = base64_encode(
hash_hmac(
'sha256',
$webhookBody,
$signingKey,
true
)
);

return $sigHeader === $hmac;
}
}
59 changes: 51 additions & 8 deletions Model/IpnManagement.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,23 @@

use Bitpay\BPCheckout\Api\IpnManagementInterface;
use Bitpay\BPCheckout\Exception\IPNValidationException;
use Bitpay\BPCheckout\Exception\HMACVerificationException;
use Bitpay\BPCheckout\Helper\ReturnHash;
use Bitpay\BPCheckout\Logger\Logger;
use Bitpay\BPCheckout\Model\Ipn\BPCItem;
use Bitpay\BPCheckout\Model\Ipn\Validator;
use Bitpay\BPCheckout\Model\Ipn\WebhookVerifier;
use Magento\Checkout\Model\Session;
use Magento\Framework\App\ResponseFactory;
use Magento\Framework\DataObject;
use Magento\Framework\Encryption\EncryptorInterface;
use Magento\Framework\Registry;
use Magento\Framework\Serialize\Serializer\Json;
use Magento\Framework\UrlInterface;
use Magento\Framework\Webapi\Rest\Request;
use Magento\Framework\Webapi\Rest\Response;
use Magento\Quote\Model\QuoteFactory;
use Magento\Sales\Api\Data\OrderInterface;
use Magento\Sales\Model\OrderFactory;

/**
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
Expand All @@ -32,7 +35,7 @@ class IpnManagement implements IpnManagementInterface
protected UrlInterface $url;
protected Session $checkoutSession;
protected QuoteFactory $quoteFactory;
protected OrderInterface $orderInterface;
protected OrderFactory $orderFactory;
protected Registry $coreRegistry;
protected Logger $logger;
protected Config $config;
Expand All @@ -42,14 +45,33 @@ class IpnManagement implements IpnManagementInterface
protected Request $request;
protected Client $client;
protected Response $response;

/**
* @var BitpayInvoiceRepository
*/
protected BitpayInvoiceRepository $bitpayInvoiceRepository;

/**
* @var EncryptorInterface
*/
protected EncryptorInterface $encryptor;

/**
* @var WebhookVerifier
*/
protected WebhookVerifier $webhookVerifier;

/**
* @var ReturnHash
*/
protected ReturnHash $returnHashHelper;

/**
* @param ResponseFactory $responseFactory
* @param UrlInterface $url
* @param Registry $registry
* @param Session $checkoutSession
* @param OrderInterface $orderInterface
* @param OrderFactory $orderFactory
* @param QuoteFactory $quoteFactory
* @param Logger $logger
* @param Config $config
Expand All @@ -59,6 +81,9 @@ class IpnManagement implements IpnManagementInterface
* @param Request $request
* @param Client $client
* @param Response $response
* @param BitpayInvoiceRepository $bitpayInvoiceRepository
* @param EncryptorInterface $encryptor
* @param WebhookVerifier $webhookVerifier
* @param ReturnHash $returnHashHelper
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
Expand All @@ -67,7 +92,7 @@ public function __construct(
UrlInterface $url,
Registry $registry,
Session $checkoutSession,
OrderInterface $orderInterface,
OrderFactory $orderFactory,
QuoteFactory $quoteFactory,
Logger $logger,
Config $config,
Expand All @@ -77,13 +102,16 @@ public function __construct(
Request $request,
Client $client,
Response $response,
BitpayInvoiceRepository $bitpayInvoiceRepository,
EncryptorInterface $encryptor,
WebhookVerifier $webhookVerifier,
ReturnHash $returnHashHelper
) {
$this->coreRegistry = $registry;
$this->responseFactory = $responseFactory;
$this->url = $url;
$this->quoteFactory = $quoteFactory;
$this->orderInterface = $orderInterface;
$this->orderFactory = $orderFactory;
$this->checkoutSession = $checkoutSession;
$this->logger = $logger;
$this->config = $config;
Expand All @@ -93,6 +121,9 @@ public function __construct(
$this->request = $request;
$this->client = $client;
$this->response = $response;
$this->bitpayInvoiceRepository = $bitpayInvoiceRepository;
$this->encryptor = $encryptor;
$this->webhookVerifier = $webhookVerifier;
$this->returnHashHelper = $returnHashHelper;
}

Expand All @@ -108,7 +139,7 @@ public function postClose()
$response = $this->responseFactory->create();
try {
$orderID = $this->request->getParam('orderID', null);
$order = $this->orderInterface->loadByIncrementId($orderID);
$order = $this->orderFactory->create()->loadByIncrementId($orderID);
$invoiceCloseHandling = $this->config->getBitpayInvoiceCloseHandling();
if ($this->config->getBitpayCheckoutSuccess() === 'standard' && $invoiceCloseHandling === 'keep_order') {
$this->checkoutSession->setLastSuccessQuoteId($order->getQuoteId())
Expand Down Expand Up @@ -151,10 +182,22 @@ public function postClose()
public function postIpn()
{
try {
$allData = $this->serializer->unserialize($this->request->getContent());
$requestBody = $this->request->getContent();
$allData = $this->serializer->unserialize($requestBody);
$data = $allData['data'];
$event = $allData['event'];
$orderId = $data['orderId'];

$bitPayInvoiceData = $this->bitpayInvoiceRepository->getByOrderId($orderId);
if (!empty($bitPayInvoiceData['bitpay_token'])) {
$signingKey = $this->encryptor->decrypt($bitPayInvoiceData['bitpay_token']);
$xSignature = $this->request->getHeader('x-signature');

if (!$this->webhookVerifier->isValidHmac($signingKey, $xSignature, $requestBody)) {
throw new HMACVerificationException('HMAC Verification Failed!');
}
}

$orderInvoiceId = $data['id'];
$row = $this->transactionRepository->findBy($orderId, $orderInvoiceId);
$client = $this->client->initialize();
Expand All @@ -179,7 +222,7 @@ public function postIpn()
$invoiceStatus = $this->invoice->getBPCCheckInvoiceStatus($client, $orderInvoiceId);
$updateWhere = ['order_id = ?' => $orderId, 'transaction_id = ?' => $orderInvoiceId];
$this->transactionRepository->update('transaction_status', $invoiceStatus, $updateWhere);
$order = $this->orderInterface->loadByIncrementId($orderId);
$order = $this->orderFactory->create()->loadByIncrementId($orderId);
switch ($event['name']) {
case Invoice::COMPLETED:
if ($invoiceStatus == 'complete') {
Expand Down
13 changes: 10 additions & 3 deletions Model/ResourceModel/BitpayInvoice.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,16 @@ public function _construct()
* @param string $invoiceID
* @param int $expirationTime
* @param int|null $acceptanceWindow
* @param string|null $bitpayToken
* @return void
*/
public function add(string $orderId, string $invoiceID, int $expirationTime, ?int $acceptanceWindow)
{
public function add(
string $orderId,
string $invoiceID,
int $expirationTime,
?int $acceptanceWindow,
?string $bitpayToken
) {
$connection = $this->getConnection();
$table_name = $connection->getTableName(self::TABLE_NAME);
$connection->insert(
Expand All @@ -38,7 +44,8 @@ public function add(string $orderId, string $invoiceID, int $expirationTime, ?in
'order_id' => $orderId,
'invoice_id' => $invoiceID,
'expiration_time' => $expirationTime,
'acceptance_window'=> $acceptanceWindow
'acceptance_window'=> $acceptanceWindow,
'bitpay_token' => $bitpayToken
]
);
}
Expand Down
21 changes: 20 additions & 1 deletion Test/Integration/Model/BPRedirectTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use Magento\Sales\Model\OrderRepository;
use Magento\TestFramework\Helper\Bootstrap;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\MockObject\MockObject;
use Magento\Framework\Encryption\EncryptorInterface;

/**
Expand Down Expand Up @@ -111,27 +112,45 @@ class BPRedirectTest extends TestCase
* @var EncryptorInterface|MockObject $encryptor
*/
private $encryptor;

/**
* @var ReturnHash $returnHash
*/
private $returnHash;


public function setUp(): void
{
$this->objectManager = Bootstrap::getObjectManager();
$this->checkoutSession = $this->objectManager->get(Session::class);
$this->orderInterface = $this->objectManager->get(OrderInterface::class);
$this->config = $this->objectManager->get(Config::class);
$this->transactionRepository = $this->objectManager->get(TransactionRepository::class);
/**
* @var Invoice|MockObject
*/
$this->invoice = $this->getMockBuilder(Invoice::class)->disableOriginalConstructor()->getMock();
$this->messageManager = $this->objectManager->get(Manager::class);
$this->registry = $this->objectManager->get(Registry::class);
$this->url = $this->objectManager->get(UrlInterface::class);
$this->logger = $this->objectManager->get(Logger::class);
$this->resultFactory = $this->objectManager->get(ResultFactory::class);
/**
* @var Client|MockObject
*/
$this->client = $this->getMockBuilder(Client::class)->disableOriginalConstructor()->getMock();
$this->orderRepository = $this->objectManager->get(OrderRepository::class);
$this->bitpayInvoiceRepository = $this->objectManager->get(BitpayInvoiceRepository::class);
$this->bitpayInvoiceRepository = $this->objectManager->get(BitpayInvoiceRepository::class);
/**
* @var EncryptorInterface|MockObject
*/
$this->encryptor = $this->getMockBuilder(EncryptorInterface::class)
->disableOriginalConstructor()
->getMock();

$this->returnHash = $this->objectManager->get(ReturnHash::class);

$this->bpRedirect = new BPRedirect(
$this->checkoutSession,
$this->orderInterface,
Expand All @@ -146,6 +165,7 @@ public function setUp(): void
$this->client,
$this->orderRepository,
$this->bitpayInvoiceRepository,
$this->returnHash,
$this->encryptor
);
}
Expand Down Expand Up @@ -197,7 +217,6 @@ public function testExecute(): void
$this->assertEquals('100000001', $result[0]['order_id']);
$this->assertEquals('new', $result[0]['transaction_status']);
$this->assertEquals('test', $this->config->getBitpayEnv());
$this->assertEquals('redirect', $this->config->getBitpayUx());
$this->assertEquals($bitpayMethodCode, $methodCode);
}

Expand Down
Loading

0 comments on commit ba52121

Please sign in to comment.