Webhook Validate Event Support

- Added validateWebhookEvent method
- Updated Tests
- Updated Samples
This commit is contained in:
Jay Patel
2015-06-08 13:18:13 -05:00
parent ddefd1d633
commit c6f8971c7e
3 changed files with 172 additions and 2 deletions

View File

@@ -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.
*

View File

@@ -0,0 +1,36 @@
<?php
$apiContext = require __DIR__ . '/../bootstrap.php';
// # Validate Webhook
// PHP Currently does not support certificate chain validation, that is necessary to validate webhook directly, from received data
// To resolve that, we need to use alternative, which includes making a GET call to obtain the data directly from PayPal.
//
// ## Received Body from Webhook
// Body received from webhook. This would be the data that you receive in the post request that comes from PayPal, to your webhook set URL.
// This is a sample data, that represents the webhook event data.
$bodyReceived = '{"id":"WH-36G56432PK518391U-9HW18392D95289106","create_time":"2015-06-01T20:21:13Z","resource_type":"sale","event_type":"PAYMENT.SALE.COMPLETED","summary":"Payment completed for $ 20.0 USD","resource":{"id":"2FY57107YS3937627","create_time":"2015-06-01T20:20:28Z","update_time":"2015-06-01T20:20:46Z","amount":{"total":"20.00","currency":"USD"},"payment_mode":"INSTANT_TRANSFER","state":"completed","protection_eligibility":"ELIGIBLE","protection_eligibility_type":"ITEM_NOT_RECEIVED_ELIGIBLE,UNAUTHORIZED_PAYMENT_ELIGIBLE","parent_payment":"PAY-2SV945219E505370PKVWL5DA","transaction_fee":{"value":"0.88","currency":"USD"},"links":[{"href":"https://api.sandbox.paypal.com/v1/payments/sale/2FY57107YS3937627","rel":"self","method":"GET"},{"href":"https://api.sandbox.paypal.com/v1/payments/sale/2FY57107YS3937627/refund","rel":"refund","method":"POST"},{"href":"https://api.sandbox.paypal.com/v1/payments/payment/PAY-2SV945219E505370PKVWL5DA","rel":"parent_payment","method":"GET"}]},"links":[{"href":"https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-36G56432PK518391U-9HW18392D95289106","rel":"self","method":"GET"},{"href":"https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-36G56432PK518391U-9HW18392D95289106/resend","rel":"resend","method":"POST"}]}';
/**
* This is one way to receive the entire body that you received from PayPal webhook. This is one of the way to retrieve that information.
* Just uncomment the below line to read the data from actual request.
*/
/** @var String $bodyReceived */
// $bodyReceived = file_get_contents('php://input');
// ### Validate Received Event Method
// Call the validateReceivedEvent() method with provided body, and apiContext object to validate
try {
/** @var \PayPal\Api\WebhookEvent $output */
$output = \PayPal\Api\WebhookEvent::validateAndGetReceivedEvent($bodyReceived, $apiContext);
} catch (Exception $ex) {
// NOTE: PLEASE DO NOT USE RESULTPRINTER CLASS IN YOUR ORIGINAL CODE. FOR SAMPLE ONLY
ResultPrinter::printError("Validate Received Webhook Event", "WebhookEvent", null, $bodyReceived, $ex);
exit(1);
}
// NOTE: PLEASE DO NOT USE RESULTPRINTER CLASS IN YOUR ORIGINAL CODE. FOR SAMPLE ONLY
ResultPrinter::printResult("Validate Received Webhook Event", "WebhookEvent", $output->getId(), $bodyReceived, $output);

View File

@@ -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);
}
}