Last active
November 22, 2022 15:56
-
-
Save host-anshu/3b2887131057c99e559d6d46d3ea84b9 to your computer and use it in GitHub Desktop.
Verify PayPal Webhook Signature using the REST APIs and the PayPal-Python-SDK
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
This snippet could be used to verify PayPal webhooks | |
PayPal docs isn't very clear about API verification mechanism. And am afraid neither | |
is the SDK. | |
BASIC CONCEPT: Your webhook_event in the body of verification request be in the same | |
order you have received from PayPal or else your verification fails. | |
EXTRA TIP IF USING DRF: Don't use request.DATA as it mutates the order of the request | |
and you can't retrieve it anymore using request.body (which is also request.stream.body) | |
Credit: https://github.com/paypal/PayPal-Python-SDK/issues/196 | |
The above link gave me idea about the SDK method, which I implemented as the fallback | |
method. Particularly because it's archived in favour of new SDK, that doesn't yet have | |
webhook support(another reason why I write this gist :-D ). This also helped me knowing | |
the basic concept I mentioned above. | |
""" | |
import json | |
import logging | |
import os | |
# paypalrestsdk==1.13.1 | |
import paypalrestsdk as paypal | |
from collections import OrderedDict | |
from functools import wraps | |
from paypalrestsdk import WebhookEvent | |
# I have used Django and DRF here | |
# Django==1.6.10 | |
# djangorestframework==2.4.3 | |
from django.conf import settings | |
from rest_framework.exceptions import ParseError | |
# Note: I've more setup in settings that make it work. It might not work directly. | |
logger = logging.getLogger(__name__) | |
paypal_api = paypal.configure({ | |
'client_id': os.getenv("PAYPAL_CLIENT_ID"), | |
'client_secret': os.getenv("PAYPAL_CLIENT_SECRET"), | |
}) | |
class PayPalAPIError(Exception): | |
"""Error when PayPal API fails""" | |
class BadRequest(ParseError): | |
"""Return HTTP 400 response when raised""" | |
class PaypalSignatureVerificationError(BadRequest): | |
"""Handles paypal signature verification errors.""" | |
def handle_api_error(func): | |
@wraps(func) | |
def wrapper(*args, **kwargs): | |
response = func(*args, **kwargs) | |
error = response.get("error", None) | |
if error: | |
raise PayPalAPIError(error) | |
return response | |
return wrapper | |
@handle_api_error | |
def verify_webhook_signature(data): | |
return paypal_api.post( | |
"/v1/notifications/verify-webhook-signature", | |
params=data, | |
headers={"Content-Type": "application/json"} | |
) | |
def verify_webhook_signature_using_sdk(*args): | |
return WebhookEvent.verify(*args) | |
def verify_event(request): | |
auth_algo = request.META.get("HTTP_PAYPAL_AUTH_ALGO") | |
cert_url = request.META.get("HTTP_PAYPAL_CERT_URL") | |
transmission_id = request.META.get("HTTP_PAYPAL_TRANSMISSION_ID") | |
transmission_sig = request.META.get("HTTP_PAYPAL_TRANSMISSION_SIG") | |
transmission_time = request.META.get("HTTP_PAYPAL_TRANSMISSION_TIME") | |
webhook_id = settings.PAYPAL_WEBHOOK_ID | |
# The link I gave credit above discusses about decoding it. I didn't use it. | |
# But am not sure when you need it. Please educate me if you come across a usage | |
# that needs it. | |
ordered_payload = json.loads(request.body, object_pairs_hook=OrderedDict) | |
data = dict( | |
auth_algo=auth_algo, cert_url=cert_url, transmission_id=transmission_id, | |
transmission_sig=transmission_sig, transmission_time=transmission_time, | |
webhook_id=webhook_id, webhook_event=ordered_payload | |
) | |
try: | |
signature_verification = verify_webhook_signature(data) | |
if signature_verification.get("verification_status") == "SUCCESS": | |
return True | |
except Exception as err: | |
logger.warning("Error verifying event using API: %s", err) | |
try: | |
# Fallback using SDK. Kept for those who are still using it. | |
signature_verification = verify_webhook_signature_using_sdk( | |
transmission_id, transmission_time, webhook_id, request.body, cert_url, | |
transmission_sig, auth_algo) | |
if signature_verification: | |
return signature_verification | |
except Exception as alter_err: | |
logger.warning("Error verifying event using SDK: %s", alter_err) | |
raise PaypalSignatureVerificationError("Paypal Webhook Signature Verification failed") |
Not work.
Error verifying event using API: 'Response' object has no attribute 'get'
Error verifying event using SDK: 'bytes' object has no attribute 'encode'
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I must also note that it has not been extensively tested.