paypal/PayPal-PHP-SDK

Failed to validate Webhook event BILLING.PLAN.CREATED

knifesk opened this issue · 7 comments

General information

  • SDK/Library version: 1.11.0
  • Environment: Sandbox
  • PayPal-Debug-ID values:
  • Language, language version, and OS: PHP 7.1, CentOS 6.5

Issue description

I am unable to validate a webhook request BILLING.PLAN.CREATED. The same implementation validates another event type (PAYMENT.SALE.COMPLETED) just perfect.. I've tracked the problem down to the bit that I think is causing the problem:

BILLING.PLAN.CREATED event contains a property named "plans" that contains a property named "links", this propery is an array of objects... as it comes empty, when I set the request body (as string) to the $webhookEvent->fromJson() method, it parses it as an object.. I found this out because I diffed both strings and came up with this:

martin@devbox:~/Desktop$ diff a.json b.json 
61c61
<             "links": [],
---
>             "links": {},

This code is where the error is coming

$webhookEvent = new WebhookEvent();
$webhookEvent->fromJson($body);
$setWebhookEvent($webhookEvent);

Full received body

{"id":"WH-98369035J8790723J-86X201489X416192T","event_version":"1.0","create_time":"2017-03-09T22:53:49Z","resource_type":"Agreement","event_type":"BILLING.SUBSCRIPTION.CREATED","summary":"A billing subscription was created","resource":{"agreement_details":{"outstanding_balance":{"value":"0.00"},"num_cycles_remaining":"0","num_cycles_completed":"0","next_billing_date":"2017-03-09T10:00:00Z","last_payment_date":"2017-03-09T22:53:48Z","last_payment_amount":{"value":"10.00"},"final_payment_due_date":"1970-01-01T00:00:00Z","failed_payment_count":"0"},"description":"Obfuscated Prueba 1","links":[{"href":"api.sandbox.paypal.com/v1/payments/billing-agreements/I-KTC2A0BLXVGD","rel":"self","method":"GET"}],"shipping_address":{"recipient_name":"test buyer","line1":"1 Main St","city":"San Jose","state":"CA","postal_code":"95131","country_code":"US"},"id":"I-KTC2A0BLXVGD","state":"Active","payer":{"payment_method":"paypal","status":"verified","payer_info":{"email":"obfuscated-buyer@gmail.com","first_name":"","last_name":"","payer_id":"U6GSXHMT5SBG8","shipping_address":{"recipient_name":"test buyer","line1":"1 Main St","city":"San Jose","state":"CA","postal_code":"95131","country_code":"US"}}},"plan":{"curr_code":"EUR",

"links":[],

"payment_definitions":[{"type":"REGULAR","frequency":"Month","frequency_interval":"1","amount":{"value":"100.00"},"cycles":"0","charge_models":[{"type":"TAX","amount":{"value":"0.00"}},{"type":"SHIPPING","amount":{"value":"0.00"}}]}],"merchant_preferences":{"setup_fee":{"value":"10.00"},"auto_bill_amount":"YES","max_fail_attempts":"2"}},"start_date":"2017-03-09T08:00:00Z"},"links":[{"href":"https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-98369035J8790723J-86X201489X416192T","rel":"self","method":"GET"},{"href":"https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-98369035J8790723J-86X201489X416192T/resend","rel":"resend","method":"POST"}]}

Full posted body

{"webhook_id":"40291045TG185602Y","auth_algo":"SHA256withRSA","transmission_id":"37e451f0-051d-11e7-9432-6b62a8a99ac4","cert_url":"https://api.sandbox.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-a5cafa77","transmission_sig":"Rgc6Vbg/zP78FItMowi5rPCxMJ8SltXzCPtJRw0ZPDQytafQAWBpRR8iovtbuhp92TDEl8AhVTqt/Y1uNJ+Bjhoxe/+eMppQ7KT3VAUkNidPL2tdglknqWU5xZM2BBoISl1UgV667Spmq0KXenoIP4fYVuHcVyCC2dpWM1k8qNB/hkTk6N7nzML+bm0E+/j7LbanQNDMA2mtbMOE55264Ovmt80Rnv4iTbmhTTiR7M8amS+y7xvpEo5jYkyG0Ip4Y59UP51mYMB+E5mv0Ga6MHOE31LjeLNj86VhFXhzvDcoxIdjCDVnBc4z0Y52uDCN4nWi9Rv5P+h5PcctXgwmwQ==","transmission_time":"2017-03-09T23:07:50Z","webhook_event":{"id":"WH-98369035J8790723J-86X201489X416192T","event_version":"1.0","create_time":"2017-03-09T22:53:49Z","resource_type":"Agreement","event_type":"BILLING.SUBSCRIPTION.CREATED","summary":"A billing subscription was created","resource":{"agreement_details":{"outstanding_balance":{"value":"0.00"},"num_cycles_remaining":"0","num_cycles_completed":"0","next_billing_date":"2017-03-09T10:00:00Z","last_payment_date":"2017-03-09T22:53:48Z","last_payment_amount":{"value":"10.00"},"final_payment_due_date":"1970-01-01T00:00:00Z","failed_payment_count":"0"},"description":"Obfuscated Prueba 1","links":[{"href":"api.sandbox.paypal.com/v1/payments/billing-agreements/I-KTC2A0BLXVGD","rel":"self","method":"GET"}],"shipping_address":{"recipient_name":"test buyer","line1":"1 Main St","city":"San Jose","state":"CA","postal_code":"95131","country_code":"US"},"id":"I-KTC2A0BLXVGD","state":"Active","payer":{"payment_method":"paypal","status":"verified","payer_info":{"email":"obfuscated-buyer@gmail.com","first_name":"","last_name":"","payer_id":"U6GSXHMT5SBG8","shipping_address":{"recipient_name":"test buyer","line1":"1 Main St","city":"San Jose","state":"CA","postal_code":"95131","country_code":"US"}}},"plan":{"curr_code":"EUR",

"links":{},

"payment_definitions":[{"type":"REGULAR","frequency":"Month","frequency_interval":"1","amount":{"value":"100.00"},"cycles":"0","charge_models":[{"type":"TAX","amount":{"value":"0.00"}},{"type":"SHIPPING","amount":{"value":"0.00"}}]}],"merchant_preferences":{"setup_fee":{"value":"10.00"},"auto_bill_amount":"YES","max_fail_attempts":"2"}},"start_date":"2017-03-09T08:00:00Z"},"links":[{"href":"https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-98369035J8790723J-86X201489X416192T","rel":"self","method":"GET"},{"href":"https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-98369035J8790723J-86X201489X416192T/resend","rel":"resend","method":"POST"}]}}

This is my verification code

    public function validateWebhook($webhookid)
    {
        $headers = array_change_key_case(getallheaders(), CASE_UPPER);
        $body = file_get_contents('php://input');

        $sigVer = new VerifyWebhookSignature();

        $sigVer->setWebhookId($webhookid);

        $sigVer->setAuthAlgo($headers['PAYPAL-AUTH-ALGO']);
        $sigVer->setTransmissionId($headers['PAYPAL-TRANSMISSION-ID']);
        $sigVer->setCertUrl($headers['PAYPAL-CERT-URL']);
        $sigVer->setTransmissionSig($headers['PAYPAL-TRANSMISSION-SIG']);
        $sigVer->setTransmissionTime($headers['PAYPAL-TRANSMISSION-TIME']);

        $webhookEvent = new WebhookEvent();
        $webhookEvent->fromJson($body);
        $sigVer->setWebhookEvent($webhookEvent);

        $request = clone $sigVer;

        try {
            /** @var \PayPal\Api\VerifyWebhookSignatureResponse $output */
            $this->log->debug('Posting VerifyWebhookSignature');
            $this->log->debug($request->toJSON());
            $output = $sigVer->post($this->apiContext);
            $this->log->debug('Output from VerifyWebhookSignature', [$output]);
        } catch (Exception $ex) {
            $this->log->error($ex->getMessage());
            $this->log->error('Exception Data: ' . $ex->getData());
            throw $ex;
        }

        return $output->getVerificationStatus() === 'SUCCESS';
    }

and this is the log:

[2017-03-10 12:44:14] PAYPAL.DEBUG: Posting VerifyWebhookSignature [] {"uid":"845c366"}
[2017-03-10 12:44:17] PAYPAL.DEBUG: Output from VerifyWebhookSignature ["[object] (PayPal\\Api\\VerifyWebhookSignatureResponse: {\n    \"verification_status\": \"FAILURE\"\n})"] {"uid":"845c366"}

+1 - I can verify that I am seeing this same behaviour as well, caused by the same diff in links, and I'm on PHP 5.3 ( don't laugh, just pity me )

I have escalated this for further review, the validation won't work if the request body doesn't match. Thanks for the helpful difference showing the links being changed from empty array to empty object.

Clearly there's a bug with the (un)serialization, but for this case only we could avoid the error just by avoiding Deserializing and serializing the body.. Of course, if the error its the deserialization there could be other undetected issues related to this

I have seen similar issues with parsing the JSON request body then serializing back to JSON. The validation checksum is calculated from the JSON body as a string, not by validating the values in the fields. I learned this the hard way with my webhook listener changing 100.0 to 100.

In PayPalModel::_convertToArray() method in the PHP SDK, there is this code:

        // we need to convert array to StdClass object to properly
        // represent JSON String
        if (sizeof($ret) <= 0) {
            $ret = new PayPalModel();
        }

That's the problematic piece, IMHO. If you comment out this line, the event verifies properly. Of course, removing that might well break other things.

Hey @TrevorAtITS !

This can cause issue for us in future. If any object in the APIs are introduced, it would come out as a StdClass, which is fine, but if the developer updates their dependencies once we add those fields to our SDKs, it would become PayPalModel or its child class. This would be a breaking change, and won't let us add new model objects in this SDK in future.

We are overriding and making webhookVerification toJSON to handle this special case for requestBody.