From 18233f22bca16de1b5ec2d1adb1ff03608447d2c Mon Sep 17 00:00:00 2001 From: Maciej Kwiatkowski Date: Tue, 10 Sep 2024 15:48:29 +0200 Subject: [PATCH 1/3] Validating incoming webhooks --- BitPayLib/class-bitpaycreateorder.php | 26 +++ BitPayLib/class-bitpayipnprocess.php | 32 ++- BitPayLib/class-bitpaypluginsetup.php | 13 +- BitPayLib/class-bitpaywebhookverifier.php | 21 ++ composer.json | 3 + tests/.gitignore | 3 +- .../Unit/BitPayLib/test-bitpayipnprocess.php | 195 ++++++++++++++++-- .../BitPayLib/test-bitpaywebhookverifier.php | 95 +++++++++ 8 files changed, 365 insertions(+), 23 deletions(-) create mode 100644 BitPayLib/class-bitpaycreateorder.php create mode 100644 BitPayLib/class-bitpaywebhookverifier.php create mode 100644 tests/Unit/BitPayLib/test-bitpaywebhookverifier.php diff --git a/BitPayLib/class-bitpaycreateorder.php b/BitPayLib/class-bitpaycreateorder.php new file mode 100644 index 0000000..1d73fbd --- /dev/null +++ b/BitPayLib/class-bitpaycreateorder.php @@ -0,0 +1,26 @@ +bitpay_payment_settings = $bitpay_payment_settings; + } + + public function execute( int $order_id ): void { + $token = $this->bitpay_payment_settings->get_bitpay_token(); + $order = new \WC_Order( $order_id ); + + $order->update_meta_data( self::BITPAY_TOKEN_ORDER_METADATA_KEY, $token ); + $order->save_meta_data(); + } +} diff --git a/BitPayLib/class-bitpayipnprocess.php b/BitPayLib/class-bitpayipnprocess.php index ae4dc2a..4e8de53 100644 --- a/BitPayLib/class-bitpayipnprocess.php +++ b/BitPayLib/class-bitpayipnprocess.php @@ -24,17 +24,23 @@ class BitPayIpnProcess { private BitPayLogger $logger; private BitPayClientFactory $factory; private BitPayWordpressHelper $bitpay_wordpress_helper; + private BitPayWebhookVerifier $bitpay_webhook_verifier; + private BitPayPaymentSettings $bitpay_payment_settings; public function __construct( BitPayCheckoutTransactions $bitpay_checkout_transactions, BitPayClientFactory $factory, BitPayWordpressHelper $bitpay_wordpress_helper, - BitPayLogger $logger + BitPayLogger $logger, + BitPayWebhookVerifier $bitpay_webhook_verifier, + BitPayPaymentSettings $bitpay_payment_settings, ) { $this->bitpay_checkout_transactions = $bitpay_checkout_transactions; $this->logger = $logger; $this->factory = $factory; $this->bitpay_wordpress_helper = $bitpay_wordpress_helper; + $this->bitpay_webhook_verifier = $bitpay_webhook_verifier; + $this->bitpay_payment_settings = $bitpay_payment_settings; } public function execute( WP_REST_Request $request ): void { @@ -45,9 +51,11 @@ public function execute( WP_REST_Request $request ): void { $data['event'] = $event; $data['requestDate'] = date( 'Y-m-d H:i:s' ); $invoice_id = $data['id'] ?? null; + $x_signature = $request->get_header( 'x-signature' ); $this->logger->execute( $data, 'INCOMING IPN', true ); - if ( ! $event || ! $data || ! $invoice_id ) { + + if ( ! $event || ! $data || ! $invoice_id || ! $x_signature ) { $this->logger->execute( 'Wrong IPN request', 'INCOMING IPN ERROR', false, true ); return; } @@ -57,6 +65,8 @@ public function execute( WP_REST_Request $request ): void { do_action( 'bitpay_checkout_woocoomerce_after_get_invoice', $bitpay_invoice ); $order = $this->bitpay_wordpress_helper->get_order( $bitpay_invoice->getOrderId() ); $this->validate_order( $order, $invoice_id ); + $this->validate_webhook( $x_signature, $request->get_body(), $order ); + $this->process( $bitpay_invoice, $order, $event['name'] ); } catch ( BitPayInvalidOrder $e ) { // phpcs:ignore // do nothing. @@ -295,10 +305,6 @@ private function process_processing( Invoice $bitpay_invoice, WC_Order $order ): $order->update_status( $new_status, __( 'BitPay payment processing', 'woocommerce' ) ); } - private function has_final_status( WC_Order $order ): bool { - return \in_array( $order->get_status(), self::FINAL_WC_ORDER_STATUSES, true ); - } - /** * We don't want to change complete order to process. * @@ -322,4 +328,18 @@ private function should_process_refund(): bool { $should_process_refund_status = $this->get_wc_order_statuses()['bitpay_checkout_order_process_refund'] ?? '1'; return '1' === $should_process_refund_status; } + + private function validate_webhook( string $x_signature, string $webhook_body, WC_Order $order ): void { + $order_bitpay_token = $order->get_meta( BitPayCreateOrder::BITPAY_TOKEN_ORDER_METADATA_KEY ); + + if ( $order_bitpay_token && + ! $this->bitpay_webhook_verifier->verify( + $this->bitpay_payment_settings->get_bitpay_token(), + $x_signature, + $webhook_body + ) + ) { + throw new \Exception( 'IPN Request failed HMAC validation' ); + } + } } diff --git a/BitPayLib/class-bitpaypluginsetup.php b/BitPayLib/class-bitpaypluginsetup.php index 7a610ab..9215e4c 100644 --- a/BitPayLib/class-bitpaypluginsetup.php +++ b/BitPayLib/class-bitpaypluginsetup.php @@ -25,6 +25,7 @@ class BitPayPluginSetup { private BitPayPaymentSettings $bitpay_payment_settings; private BitPayInvoiceCreate $bitpay_invoice_create; private BitPayCheckoutTransactions $bitpay_checkout_transactions; + private BitPayCreateOrder $bitpay_create_order; public function __construct() { $this->bitpay_payment_settings = new BitPayPaymentSettings(); @@ -32,8 +33,9 @@ public function __construct() { $cart = new BitPayCart(); $logger = new BitPayLogger(); $wordpress_helper = new BitPayWordpressHelper(); + $webhook_verifier = new BitPayWebhookVerifier(); $this->bitpay_checkout_transactions = new BitPayCheckoutTransactions( $wordpress_helper ); - $this->bitpay_ipn_process = new BitPayIpnProcess( $this->bitpay_checkout_transactions, $factory, $wordpress_helper, $logger ); + $this->bitpay_ipn_process = new BitPayIpnProcess( $this->bitpay_checkout_transactions, $factory, $wordpress_helper, $logger, $webhook_verifier, $this->bitpay_payment_settings ); $this->bitpay_cancel_order = new BitPayCancelOrder( $cart, $this->bitpay_checkout_transactions, $logger ); $this->bitpay_invoice_create = new BitPayInvoiceCreate( $factory, @@ -42,6 +44,9 @@ public function __construct() { $wordpress_helper, $logger ); + $this->bitpay_create_order = new BitPayCreateOrder( + $this->bitpay_payment_settings + ); } public function execute(): void { @@ -58,6 +63,8 @@ public function execute(): void { add_filter( 'woocommerce_payment_gateways', array( $this, 'wc_bitpay_checkout_add_to_gateways' ) ); add_filter( 'woocommerce_order_button_html', array( $this, 'bitpay_checkout_replace_order_button_html' ), 10, 2 ); add_action( 'woocommerce_blocks_loaded', array( $this, 'register_payment_block' ) ); + add_action( 'woocommerce_new_order', array( $this, 'bitpay_create_order' ) ); + add_action( 'woocommerce_update_order', array( $this, 'bitpay_create_order' ) ); // http:///wp-json/bitpay/ipn/status url. // http:///wp-json/bitpay/cartfix/restore url. @@ -221,4 +228,8 @@ function () { 5 ); } + + public function bitpay_create_order( int $order_id ): void { + $this->bitpay_create_order->execute( $order_id ); + } } diff --git a/BitPayLib/class-bitpaywebhookverifier.php b/BitPayLib/class-bitpaywebhookverifier.php new file mode 100644 index 0000000..f99849f --- /dev/null +++ b/BitPayLib/class-bitpaywebhookverifier.php @@ -0,0 +1,21 @@ +willReturn( file_get_contents(__DIR__ . '/json/bitpay_paid_ipn_webhook.json') ); + $request->expects(self::once())->method('get_header')->with('x-signature')->willReturn('x-signature-header-value'); $bitpay_invoice->method('getOrderId')->willReturn( self::BITPAY_INVOICE_ID ); $bitpay_client_factory->method('create')->willReturn( $bitpay_client ); $bitpay_client->method('getInvoice')->with( self::BITPAY_INVOICE_ID, \BitPaySDK\Model\Facade::POS, false ) @@ -52,7 +56,9 @@ public function it_should_do_not_allow_to_process_non_bitpay_orders() { $wordpress_helper, $bitpay_client_factory, $bitpay_checkout_transactions, - $logger + $logger, + $this->get_bitpay_webhook_verifier(), + $this->get_bitpay_payment_settings() ); // then @@ -95,6 +101,7 @@ public function it_should_do_not_allow_to_process_with_wrong_transaction() { ->willReturn( file_get_contents(__DIR__ . '/json/bitpay_paid_ipn_webhook.json') ); + $request->expects(self::once())->method('get_header')->with('x-signature')->willReturn('x-signature-header-value'); $bitpay_invoice->method('getOrderId')->willReturn( self::BITPAY_INVOICE_ID ); $bitpay_client_factory->method('create')->willReturn( $bitpay_client ); $bitpay_client->method('getInvoice')->with( self::BITPAY_INVOICE_ID, \BitPaySDK\Model\Facade::POS, false ) @@ -107,7 +114,9 @@ public function it_should_do_not_allow_to_process_with_wrong_transaction() { $wordpress_helper, $bitpay_client_factory, $transactions, - $logger + $logger, + $this->get_bitpay_webhook_verifier(), + $this->get_bitpay_payment_settings() ); $logger->expects(self::exactly(2))->method('execute')->with( @@ -129,8 +138,10 @@ public function it_should_do_not_allow_to_process_with_wrong_transaction() { /** * @test */ - public function it_should_process_ipn_request() { + public function it_should_process_ipn_request_without_verification_when_order_has_no_saved_token() { // given + $webhook_verifier = $this->get_bitpay_webhook_verifier(); + $webhook_verifier->expects(self::never())->method('verify'); $wordpress_helper = $this->get_wordpress_helper(); $request = $this->getMockBuilder(\WP_REST_Request::class)->getMock(); $transactions = $this->get_checkout_transactions(); @@ -150,6 +161,8 @@ public function it_should_process_ipn_request() { ->willReturn( file_get_contents(__DIR__ . '/json/bitpay_paid_ipn_webhook.json') ); + $request->expects(self::once())->method('get_header')->with('x-signature')->willReturn('x-signature-header-value'); + $bitpay_invoice->method('getOrderId')->willReturn( self::BITPAY_INVOICE_ID ); $bitpay_client_factory->method('create')->willReturn( $bitpay_client ); $bitpay_client->method('getInvoice')->with( self::BITPAY_INVOICE_ID, \BitPaySDK\Model\Facade::POS, false ) @@ -162,7 +175,119 @@ public function it_should_process_ipn_request() { $wordpress_helper, $bitpay_client_factory, $transactions, - $logger + $logger, + $webhook_verifier, + $this->get_bitpay_payment_settings() + ); + + // then + $wc_order + ->expects(self::once()) + ->method('add_order_note') + ->with('BitPay Invoice ID: someId is paid and awaiting confirmation.'); + $transactions->expects(self::once())->method('update_transaction_status'); + + // when + $testedClass->execute($request); + } + + /** + * @test + */ + public function it_should_not_process_failed_verification_ipn_request() { + // given + $webhook_verifier = $this->get_bitpay_webhook_verifier(); + $webhook_verifier->expects(self::once())->method('verify')->willReturn(false); + $wordpress_helper = $this->get_wordpress_helper(); + $request = $this->getMockBuilder(\WP_REST_Request::class)->getMock(); + $transactions = $this->get_checkout_transactions(); + $bitpay_invoice = $this->getMockBuilder(\BitPaySDK\Model\Invoice\Invoice::class)->getMock(); + $bitpay_client = $this->getMockBuilder(\BitPaySDK\Client::class) + ->disableOriginalConstructor() + ->getMock(); + $logger = $this->get_bitpay_logger(); + $bitpay_client_factory = $this->getMockBuilder(BitPayClientFactory::class) + ->disableOriginalConstructor()->getMock(); + $wc_order = $this->get_wc_order(); + $bitpay_payment_settings = $this->get_bitpay_payment_settings(); + + $transactions->method('count_transaction_id')->willReturn(1); + $bitpay_invoice->method('getStatus')->willReturn('paid'); + $bitpay_invoice->method('getId')->willReturn(self::BITPAY_INVOICE_ID); + $request->method('get_body') + ->willReturn( + file_get_contents(__DIR__ . '/json/bitpay_paid_ipn_webhook.json') + ); + $request->expects(self::once())->method('get_header')->with('x-signature')->willReturn('x-signature-header-value'); + $bitpay_invoice->method('getOrderId')->willReturn( self::BITPAY_INVOICE_ID ); + $bitpay_client_factory->method('create')->willReturn( $bitpay_client ); + $bitpay_client->method('getInvoice')->with( self::BITPAY_INVOICE_ID, \BitPaySDK\Model\Facade::POS, false ) + ->willReturn($bitpay_invoice); + $wordpress_helper->method('get_order') + ->with(self::BITPAY_INVOICE_ID) + ->willReturn($wc_order); + $wc_order->method('get_meta')->with(BitPayCreateOrder::BITPAY_TOKEN_ORDER_METADATA_KEY)->willReturn('secret_token'); + $bitpay_payment_settings->expects(self::once())->method('get_bitpay_token')->willReturn('different_token'); + + $testedClass = $this->getTestedClass( + $wordpress_helper, + $bitpay_client_factory, + $transactions, + $logger, + $webhook_verifier, + $bitpay_payment_settings + ); + + // then + $wc_order + ->expects(self::never()) + ->method('add_order_note'); + $transactions->expects(self::never())->method('update_transaction_status'); + + // when + $testedClass->execute($request); + } + + public function it_should_verify_order_with_saved_token_ipn_request_and_process_correctly_verified_ipn() { + // given + $wordpress_helper = $this->get_wordpress_helper(); + $request = $this->getMockBuilder(\WP_REST_Request::class)->getMock(); + $transactions = $this->get_checkout_transactions(); + $webhook_verifier = $this->get_bitpay_webhook_verifier(); + $webhook_verifier->method('verify')->willReturn(true); + $bitpay_invoice = $this->getMockBuilder(\BitPaySDK\Model\Invoice\Invoice::class)->getMock(); + $bitpay_client = $this->getMockBuilder(\BitPaySDK\Client::class) + ->disableOriginalConstructor() + ->getMock(); + $logger = $this->get_bitpay_logger(); + $bitpay_client_factory = $this->getMockBuilder(BitPayClientFactory::class) + ->disableOriginalConstructor()->getMock(); + $wc_order = $this->get_wc_order(); + $wc_order->method('get_meta')->with(BitPayCreateOrder::BITPAY_TOKEN_ORDER_METADATA_KEY)->willReturn('secret_token'); + + $transactions->method('count_transaction_id')->willReturn(1); + $bitpay_invoice->method('getStatus')->willReturn('paid'); + $bitpay_invoice->method('getId')->willReturn(self::BITPAY_INVOICE_ID); + $request->method('get_body') + ->willReturn( + file_get_contents(__DIR__ . '/json/bitpay_paid_ipn_webhook.json') + ); + $request->expects(self::once())->method('get_header')->with('x-signature')->willReturn('x-signature-header-value'); + $bitpay_invoice->method('getOrderId')->willReturn( self::BITPAY_INVOICE_ID ); + $bitpay_client_factory->method('create')->willReturn( $bitpay_client ); + $bitpay_client->method('getInvoice')->with( self::BITPAY_INVOICE_ID, \BitPaySDK\Model\Facade::POS, false ) + ->willReturn($bitpay_invoice); + $wordpress_helper->method('get_order') + ->with(self::BITPAY_INVOICE_ID) + ->willReturn($wc_order); + + $testedClass = $this->getTestedClass( + $wordpress_helper, + $bitpay_client_factory, + $transactions, + $logger, + $webhook_verifier, + $this->get_bitpay_payment_settings() ); // then @@ -200,6 +325,7 @@ public function it_should_confirm_order(): void { ->willReturn( file_get_contents(__DIR__ . '/json/bitpay_confirmed_ipn_webhook.json') ); + $request->expects(self::once())->method('get_header')->with('x-signature')->willReturn('x-signature-header-value'); $bitpay_invoice->method('getOrderId')->willReturn( self::BITPAY_INVOICE_ID ); $bitpay_client_factory->method('create')->willReturn( $bitpay_client ); $bitpay_client->method('getInvoice')->with( self::BITPAY_INVOICE_ID, \BitPaySDK\Model\Facade::POS, false ) @@ -212,7 +338,9 @@ public function it_should_confirm_order(): void { $wordpress_helper, $bitpay_client_factory, $transactions, - $logger + $logger, + $this->get_bitpay_webhook_verifier(), + $this->get_bitpay_payment_settings() ); $wc_order @@ -254,6 +382,7 @@ public function it_should_complete_order(): void { ->willReturn( file_get_contents(__DIR__ . '/json/bitpay_completed_ipn_webhook.json') ); + $request->expects(self::once())->method('get_header')->with('x-signature')->willReturn('x-signature-header-value'); $bitpay_invoice->method('getOrderId')->willReturn( self::BITPAY_INVOICE_ID ); $bitpay_client_factory->method('create')->willReturn( $bitpay_client ); $bitpay_client->method('getInvoice')->with( self::BITPAY_INVOICE_ID, \BitPaySDK\Model\Facade::POS, false ) @@ -266,7 +395,9 @@ public function it_should_complete_order(): void { $wordpress_helper, $bitpay_client_factory, $transactions, - $logger + $logger, + $this->get_bitpay_webhook_verifier(), + $this->get_bitpay_payment_settings() ); $wc_order @@ -307,6 +438,7 @@ public function it_should_decline_order(): void { ->willReturn( file_get_contents(__DIR__ . '/json/bitpay_declined_ipn_webhook.json') ); + $request->expects(self::once())->method('get_header')->with('x-signature')->willReturn('x-signature-header-value'); $bitpay_invoice->method('getOrderId')->willReturn( self::BITPAY_INVOICE_ID ); $bitpay_client_factory->method('create')->willReturn( $bitpay_client ); $bitpay_client->method('getInvoice')->with( self::BITPAY_INVOICE_ID, \BitPaySDK\Model\Facade::POS, false ) @@ -319,7 +451,9 @@ public function it_should_decline_order(): void { $wordpress_helper, $bitpay_client_factory, $transactions, - $logger + $logger, + $this->get_bitpay_webhook_verifier(), + $this->get_bitpay_payment_settings() ); $wc_order @@ -357,6 +491,7 @@ public function it_should_fail_order(): void { ->willReturn( file_get_contents(__DIR__ . '/json/bitpay_invalid_ipn_webhook.json') ); + $request->expects(self::once())->method('get_header')->with('x-signature')->willReturn('x-signature-header-value'); $bitpay_invoice->method('getOrderId')->willReturn( self::BITPAY_INVOICE_ID ); $bitpay_client_factory->method('create')->willReturn( $bitpay_client ); $bitpay_client->method('getInvoice')->with( self::BITPAY_INVOICE_ID, \BitPaySDK\Model\Facade::POS, false ) @@ -369,7 +504,9 @@ public function it_should_fail_order(): void { $wordpress_helper, $bitpay_client_factory, $transactions, - $logger + $logger, + $this->get_bitpay_webhook_verifier(), + $this->get_bitpay_payment_settings() ); $wc_order @@ -407,6 +544,7 @@ public function it_should_expire_order(): void { ->willReturn( file_get_contents(__DIR__ . '/json/bitpay_expired_ipn_webhook.json') ); + $request->expects(self::once())->method('get_header')->with('x-signature')->willReturn('x-signature-header-value'); $bitpay_invoice->method('getOrderId')->willReturn( self::BITPAY_INVOICE_ID ); $bitpay_client_factory->method('create')->willReturn( $bitpay_client ); $bitpay_client->method('getInvoice')->with( self::BITPAY_INVOICE_ID, \BitPaySDK\Model\Facade::POS, false ) @@ -419,7 +557,9 @@ public function it_should_expire_order(): void { $wordpress_helper, $bitpay_client_factory, $transactions, - $logger + $logger, + $this->get_bitpay_webhook_verifier(), + $this->get_bitpay_payment_settings() ); $wc_order @@ -456,6 +596,7 @@ public function it_should_refund_order(): void { ->willReturn( file_get_contents(__DIR__ . '/json/bitpay_refunded_ipn_webhook.json') ); + $request->expects(self::once())->method('get_header')->with('x-signature')->willReturn('x-signature-header-value'); $bitpay_invoice->method('getOrderId')->willReturn( self::BITPAY_INVOICE_ID ); $bitpay_client_factory->method('create')->willReturn( $bitpay_client ); $bitpay_client->method('getInvoice')->with( self::BITPAY_INVOICE_ID, \BitPaySDK\Model\Facade::POS, false ) @@ -468,7 +609,9 @@ public function it_should_refund_order(): void { $wordpress_helper, $bitpay_client_factory, $transactions, - $logger + $logger, + $this->get_bitpay_webhook_verifier(), + $this->get_bitpay_payment_settings() ); $wc_order @@ -507,6 +650,7 @@ public function it_should_complete_for_wcorder_wccomplete_status_and_wccompleted ->willReturn( file_get_contents(__DIR__ . '/json/bitpay_completed_ipn_webhook.json') ); + $request->expects(self::once())->method('get_header')->with('x-signature')->willReturn('x-signature-header-value'); $bitpay_invoice->method('getOrderId')->willReturn( self::BITPAY_INVOICE_ID ); $bitpay_client_factory->method('create')->willReturn( $bitpay_client ); $bitpay_client->method('getInvoice')->with( self::BITPAY_INVOICE_ID, \BitPaySDK\Model\Facade::POS, false ) @@ -519,7 +663,9 @@ public function it_should_complete_for_wcorder_wccomplete_status_and_wccompleted $wordpress_helper, $bitpay_client_factory, $transactions, - $logger + $logger, + $this->get_bitpay_webhook_verifier(), + $this->get_bitpay_payment_settings() ); $wc_order @@ -561,6 +707,7 @@ public function it_should_complete_for_wcorder_wcprocessing_status_and_wccomplet ->willReturn( file_get_contents(__DIR__ . '/json/bitpay_completed_ipn_webhook.json') ); + $request->expects(self::once())->method('get_header')->with('x-signature')->willReturn('x-signature-header-value'); $bitpay_invoice->method('getOrderId')->willReturn( self::BITPAY_INVOICE_ID ); $bitpay_client_factory->method('create')->willReturn( $bitpay_client ); $bitpay_client->method('getInvoice')->with( self::BITPAY_INVOICE_ID, \BitPaySDK\Model\Facade::POS, false ) @@ -573,7 +720,9 @@ public function it_should_complete_for_wcorder_wcprocessing_status_and_wccomplet $wordpress_helper, $bitpay_client_factory, $transactions, - $logger + $logger, + $this->get_bitpay_webhook_verifier(), + $this->get_bitpay_payment_settings() ); $wc_order @@ -597,6 +746,20 @@ private function get_bitpay_logger() { return $this->getMockBuilder(BitPayLogger::class)->disableOriginalConstructor()->getMock(); } + /** + * @return (BitPayWebhookVerifier|\PHPUnit\Framework\MockObject\MockObject) + */ + private function get_bitpay_webhook_verifier() { + return $this->getMockBuilder(BitPayWebhookVerifier::class)->disableOriginalConstructor()->getMock(); + } + + /** + * @return (BitPayPaymentSettings|\PHPUnit\Framework\MockObject\MockObject) + */ + private function get_bitpay_payment_settings() { + return $this->getMockBuilder(BitPayPaymentSettings::class)->disableOriginalConstructor()->getMock(); + } + /** * @return (BitPayCheckoutTransactions&\PHPUnit\Framework\MockObject\MockObject) */ @@ -644,8 +807,10 @@ private function getTestedClass( BitPayWordpressHelper $wordpress_helper, BitPayClientFactory $bitpay_client_factory, BitPayCheckoutTransactions $bitpay_checkout_transactions, - BitPayLogger $logger + BitPayLogger $logger, + BitPayWebhookVerifier $webhook_verifier, + BitPayPaymentSettings $payment_settings ): BitPayIpnProcess { - return new BitPayIpnProcess($bitpay_checkout_transactions, $bitpay_client_factory, $wordpress_helper, $logger); + return new BitPayIpnProcess($bitpay_checkout_transactions, $bitpay_client_factory, $wordpress_helper, $logger, $webhook_verifier, $payment_settings); } } diff --git a/tests/Unit/BitPayLib/test-bitpaywebhookverifier.php b/tests/Unit/BitPayLib/test-bitpaywebhookverifier.php new file mode 100644 index 0000000..c57ad37 --- /dev/null +++ b/tests/Unit/BitPayLib/test-bitpaywebhookverifier.php @@ -0,0 +1,95 @@ +fail("Test webhook body file does not exist."); + } + return file_get_contents($file); + } + + /** + * @test + */ + public function it_should_return_true_if_the_signature_matches() + { + $verifier = $this->getTestedClass(); + $signingKey = 'my_secret_key'; + + // bitpay_paid_ipn_webhook request body signed with the signing key: my_secret_key + $expected_signature_header = 'G3AN1yIRVFPahcmKXK0qg9UksH9WlK3Llvvs7APZOzc='; + + $this->assertTrue( + $verifier->verify($signingKey, $expected_signature_header, $this->get_test_webhook_body()) + ); + } + + /** + * @test + */ + public function it_should_return_false_if_the_signature_doesnt_match() + { + $verifier = $this->getTestedClass(); + + // key different from the one used to test sign + $signingKey = 'different_key'; + + // bitpay_paid_ipn_webhook request body signed with the signing key: my_secret_key + $expected_signature_header = 'G3AN1yIRVFPahcmKXK0qg9UksH9WlK3Llvvs7APZOzc='; + + $this->assertFalse( + $verifier->verify($signingKey, $expected_signature_header, $this->get_test_webhook_body()) + ); + } + + /** + * @test + */ + public function it_should_return_false_for_empty_signature() + { + $verifier = $this->getTestedClass(); + $signingKey = 'my_secret_key'; + + $this->assertFalse( + $verifier->verify($signingKey, '', $this->get_test_webhook_body()) + ); + } + + /** + * @test + */ + public function it_should_return_false_if_the_request_body_is_tampered_with() + { + $verifier = $this->getTestedClass(); + $signingKey = 'my_secret_key'; + + $originalWebhookBody = $this->get_test_webhook_body(); +// $expected_signature_header = base64_encode( +// hash_hmac('sha256', $originalWebhookBody, $signingKey, true) +// ); + + // bitpay_paid_ipn_webhook request body signed with the signing key: my_secret_key + $expected_signature_header = 'G3AN1yIRVFPahcmKXK0qg9UksH9WlK3Llvvs7APZOzc='; + + // Tamper with the request body (e.g., change a character) + $tamperedWebhookBody = str_replace('payment', 'tampered', $originalWebhookBody); + + $this->assertFalse( + $verifier->verify($signingKey, $expected_signature_header, $tamperedWebhookBody), + "Tampered webhook body should result in a failed verification" + ); + } + + private function getTestedClass(): BitPayWebhookVerifier { + return new BitPayWebhookVerifier(); + } +} From 5b798d8aed056d94b19e2f2a8b28b03b7c477a3e Mon Sep 17 00:00:00 2001 From: Patrick Date: Thu, 19 Sep 2024 15:46:44 -0400 Subject: [PATCH 2/3] SP-965: Fix indent --- BitPayLib/class-bitpaypluginsetup.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BitPayLib/class-bitpaypluginsetup.php b/BitPayLib/class-bitpaypluginsetup.php index b335e99..777445d 100644 --- a/BitPayLib/class-bitpaypluginsetup.php +++ b/BitPayLib/class-bitpaypluginsetup.php @@ -47,7 +47,7 @@ public function __construct() { ); $this->bitpay_create_order = new BitPayCreateOrder( $this->bitpay_payment_settings - ); + ); $this->bitpay_support_package = new BitPaySupportPackage( $wordpress_helper, $logger From a0a2c339ca7d446fa9426e35b16d0c3e8629bf00 Mon Sep 17 00:00:00 2001 From: Patrick Date: Thu, 19 Sep 2024 15:49:31 -0400 Subject: [PATCH 3/3] SP-965: Add additional tab --- BitPayLib/class-bitpaypluginsetup.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BitPayLib/class-bitpaypluginsetup.php b/BitPayLib/class-bitpaypluginsetup.php index 777445d..6a8978f 100644 --- a/BitPayLib/class-bitpaypluginsetup.php +++ b/BitPayLib/class-bitpaypluginsetup.php @@ -47,7 +47,7 @@ public function __construct() { ); $this->bitpay_create_order = new BitPayCreateOrder( $this->bitpay_payment_settings - ); + ); $this->bitpay_support_package = new BitPaySupportPackage( $wordpress_helper, $logger