From c6f8971c7e90f19948385ef3e1e4c4e51f6937c8 Mon Sep 17 00:00:00 2001 From: Jay Patel Date: Mon, 8 Jun 2015 13:18:13 -0500 Subject: [PATCH] Webhook Validate Event Support - Added validateWebhookEvent method - Updated Tests - Updated Samples --- lib/PayPal/Api/WebhookEvent.php | 42 +++++++- sample/notifications/ValidateWebhookEvent.php | 36 +++++++ tests/PayPal/Test/Api/WebhookEventTest.php | 96 +++++++++++++++++++ 3 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 sample/notifications/ValidateWebhookEvent.php diff --git a/lib/PayPal/Api/WebhookEvent.php b/lib/PayPal/Api/WebhookEvent.php index 353f4e2..5c786f9 100644 --- a/lib/PayPal/Api/WebhookEvent.php +++ b/lib/PayPal/Api/WebhookEvent.php @@ -2,12 +2,12 @@ namespace PayPal\Api; -use PayPal\Common\PayPalModel; use PayPal\Common\PayPalResourceModel; +use PayPal\Exception\PayPalConnectionException; use PayPal\Validation\ArgumentValidator; -use PayPal\Api\WebhookEventList; use PayPal\Rest\ApiContext; use PayPal\Transport\PayPalRestCall; +use PayPal\Validation\JsonValidator; /** * Class WebhookEvent @@ -163,6 +163,44 @@ class WebhookEvent extends PayPalResourceModel return $this->resource; } + /** + * Validates Received Event from Webhook, and returns the webhook event object. Because security verifications by verifying certificate chain is not enabled in PHP yet, + * we need to fallback to default behavior of retrieving the ID attribute of the data, and make a separate GET call to PayPal APIs, to retrieve the data. + * This is important to do again, as hacker could have faked the data, and the retrieved data cannot be trusted without either doing client side security validation, or making a separate call + * to PayPal APIs to retrieve the actual data. This limits the hacker to mimick a fake data, as hacker wont be able to predict the Id correctly. + * + * NOTE: PLEASE DO NOT USE THE DATA PROVIDED IN WEBHOOK DIRECTLY, AS HACKER COULD PASS IN FAKE DATA. IT IS VERY IMPORTANT THAT YOU RETRIEVE THE ID AND MAKE A SEPARATE CALL TO PAYPAL API. + * + * @param string $body + * @param ApiContext $apiContext + * @param PayPalRestCall $restCall is the Rest Call Service that is used to make rest calls + * @return WebhookEvent + * @throws \InvalidArgumentException if input arguments are incorrect, or Id is not found. + * @throws PayPalConnectionException if any exception from PayPal APIs other than not found is sent. + */ + public static function validateAndGetReceivedEvent($body, $apiContext = null, $restCall = null) + { + if ($body == null | empty($body)){ + throw new \InvalidArgumentException("Body cannot be null or empty"); + } + if (!JsonValidator::validate($body, true)) { + throw new \InvalidArgumentException("Request Body is not a valid JSON."); + } + $object = new WebhookEvent($body); + if ($object->getId() == null) { + throw new \InvalidArgumentException("Id attribute not found in JSON. Possible reason could be invalid JSON Object"); + } + try { + return self::get($object->getId(), $apiContext, $restCall); + } catch(PayPalConnectionException $ex) { + if ($ex->getCode() == 404) { + // It means that the given webhook event Id is not found for this merchant. + throw new \InvalidArgumentException("Webhook Event Id provided in the data is incorrect. This could happen if anyone other than PayPal is faking the incoming webhook data."); + } + throw $ex; + } + } + /** * Retrieves the Webhooks event resource identified by event_id. Can be used to retrieve the payload for an event. * diff --git a/sample/notifications/ValidateWebhookEvent.php b/sample/notifications/ValidateWebhookEvent.php new file mode 100644 index 0000000..e8c5372 --- /dev/null +++ b/sample/notifications/ValidateWebhookEvent.php @@ -0,0 +1,36 @@ +getId(), $bodyReceived, $output); + + diff --git a/tests/PayPal/Test/Api/WebhookEventTest.php b/tests/PayPal/Test/Api/WebhookEventTest.php index 99acb63..8f172b1 100644 --- a/tests/PayPal/Test/Api/WebhookEventTest.php +++ b/tests/PayPal/Test/Api/WebhookEventTest.php @@ -3,6 +3,7 @@ namespace PayPal\Test\Api; use PayPal\Common\PayPalResourceModel; +use PayPal\Exception\PayPalConnectionException; use PayPal\Validation\ArgumentValidator; use PayPal\Api\WebhookEventList; use PayPal\Rest\ApiContext; @@ -128,6 +129,48 @@ class WebhookEventTest extends \PHPUnit_Framework_TestCase $this->assertNotNull($result); } + /** + * @dataProvider mockProvider + * @param WebhookEvent $obj + */ + public function testValidateWebhook($obj, $mockApiContext) + { + $mockPayPalRestCall = $this->getMockBuilder('\PayPal\Transport\PayPalRestCall') + ->disableOriginalConstructor() + ->getMock(); + + $mockPayPalRestCall->expects($this->any()) + ->method('execute') + ->will($this->returnValue( + WebhookEventTest::getJson() + )); + + $result = WebhookEvent::validateAndGetReceivedEvent('{"id":"123"}', $mockApiContext, $mockPayPalRestCall); + //$result = $obj->get("eventId", $mockApiContext, $mockPayPalRestCall); + $this->assertNotNull($result); + } + + /** + * @dataProvider mockProvider + * @param WebhookEvent $obj + * @param ApiContext $mockApiContext + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Webhook Event Id provided in the data is incorrect. This could happen if anyone other than PayPal is faking the incoming webhook data. + */ + public function testValidateWebhook404($obj, $mockApiContext) + { + $mockPayPalRestCall = $this->getMockBuilder('\PayPal\Transport\PayPalRestCall') + ->disableOriginalConstructor() + ->getMock(); + + $mockPayPalRestCall->expects($this->any()) + ->method('execute') + ->will($this->throwException(new PayPalConnectionException(null, "404 not found", 404))); + + $result = WebhookEvent::validateAndGetReceivedEvent('{"id":"123"}', $mockApiContext, $mockPayPalRestCall); + $this->assertNotNull($result); + } + public function mockProvider() { $obj = self::getObject(); @@ -139,4 +182,57 @@ class WebhookEventTest extends \PHPUnit_Framework_TestCase array($obj, null) ); } + + /** + * @dataProvider mockProvider + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Body cannot be null or empty + */ + public function testValidateWebhookNull($mockApiContext) + { + WebhookEvent::validateAndGetReceivedEvent(null, $mockApiContext); + } + + /** + * @dataProvider mockProvider + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Body cannot be null or empty + */ + public function testValidateWebhookEmpty($mockApiContext) + { + WebhookEvent::validateAndGetReceivedEvent('', $mockApiContext); + } + + /** + * @dataProvider mockProvider + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Request Body is not a valid JSON. + */ + public function testValidateWebhookInvalid($mockApiContext) + { + WebhookEvent::validateAndGetReceivedEvent('something-invalid', $mockApiContext); + } + + /** + * @dataProvider mockProvider + * @param $mockApiContext ApiContext + * @expectedException \PHPUnit_Framework_Error_Notice + * @expectedExceptionMessage Missing Accessor: PayPal\Api\WebhookEvent:setValid. You might be using older version of SDK. If not, create an issue at https://github.com/paypal/PayPal-PHP-SDK/issues + */ + public function testValidateWebhookValidJSONWithMissingObject($obj, $mockApiContext) + { + WebhookEvent::validateAndGetReceivedEvent('{"valid":"json"}', $mockApiContext); + } + + /** + * @dataProvider mockProvider + * @param $mockApiContext ApiContext + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Id attribute not found in JSON. Possible reason could be invalid JSON Object + */ + public function testValidateWebhookValidJSONWithoutId($obj, $mockApiContext) + { + WebhookEvent::validateAndGetReceivedEvent('{"summary":"json"}', $mockApiContext); + } + }