Creating webhooks with Django

(Comments)

What is a webhook

A webhook is a HTTP callback that is triggered on occurrence of an event. They are helpful in notifying an event to different web applications on the internet (or any network). Consider the scenario of a CI (continuous integration) environment. The requirement there is that as soon as a developer pushes his code changes to the repository, the process of testing and deployment has to start. This can be easily achieved with a webhook.
Let us create an event receiver endpoint with Django.

from django.views.decorators.http import require_http_methods

@require_http_methods(["GET", "POST"])
def hook_receiver_view(request):
    # Listens only for GET and POST requests
    # returns django.http.HttpResponseNotAllowed for other requests

    # Handle the event appropriately
    return HttpResponse('success')

This is just a simple view that accepts GET or POST request. The view must handle the event as required. Use this View's URL in configuring the webhook in the application that generates the event. That is all it takes to get triggered to an event.

Security concerns

Any such call, in general, will need pass some information and this will be done as part of the GET or POST request. Because this URL can be accessed by anyone on the internet and there is no authentication required (which need not be the case always), we need to be critical of the information passed as part of the request.
The saying goes as to never trust the values sent from the client. They could be coming from anywhere and could be a malicious attack. The same goes for webhooks. Anyone could be triggering the event. Always evaluate the request to be genuine.


For example, let us consider a scenario of a payment gateway calling back to our application when a user completes payment. The gateway has to provide some identification of the user and let it be 'id'. Assume that the callback URL is https://www.myservice.com/payment-confirm/. If the request is a GET request and the user 'id' is 'xyz', the callback URL would become https://www.myservice.com/payment-confirm/?id=xyz. And we might do something like this in our view.

@require_http_methods(["GET", "POST"])
def hook_receiver_view(request):
    user_id = request.GET.get('id', None)
    # Save the payment status
    payment = Payment.objects.get(user_id=user_id)
    payment.payment_successful = True
    payment.save()
    return HttpResponse('success')


But wait. Just because the user 'id' is passed as the URL parameter, we cannot assume the payment of the user with id 'xyz' is successful. Anyone can make a GET request with their id. So, the view must verify that the user 'xyz' has indeed successfully completed the payment using the API provided by the payment gateway service. We should be doing something like this.

@require_http_methods(["GET", "POST"])
def hook_receiver_view(request):
    user_id = request.GET.get('id', None)

    if payment_service.hasUserPaid(user_id): # This is where we are verifying the payment
        # Save the payment status
        payment = Payment.objects.get(user_id=user_id)
        payment.payment_successful = True
        payment.save()

    return HttpResponse('success')

We should be always returning the success HttpResponse to the webhook service because it would be expecting it. Otherwise the service might think that our callback could not handle the event correctly and may retry triggering the same event again.


You might also consider limiting the rate of requests that are accepted. If you are using the Django Rest Framework, it comes with in-built throttling. You can use Django Ratelimit for Django.

Creating webhooks with dj-webhooks

dj-webhooks allows us to create webhooks with options to create and manage event, callbacks, logs etc. Though the package is old let us try our hands on it. Install the package with

pip install dj-webhooks

Then create the events that will be triggered as follows in the django settings model.

WEBHOOK_EVENTS = (
    "payment.paid",
    "payment.cancelled",
    "payment.refunded",
    "payment.fulfilled"
)

When a user registers a callback URL for an event, it has to be saved to the webhooks model. Our view must do something like this.

from djwebhooks.models import WebhookTarget

def save_paymentsuccess_webhook(request):
    WebhookTarget.objects.create(
        owner=request.user,
        event='payment.succeeded',
        target_url= request.POST.get('callback_url'),
        header_content_type=WebhookTarget.CONTENT_TYPE_JSON,
    )
    # Some other operations

Now that the webhook is saved to the database, we need to call upon the URL listed for an event (by a user). To do this we must define a function that returns a JSON - serializable, generally a dictionary. Use the decorator djwebhooks.decorators.hook over the function to make it trigger the webhook. The function looks like this

from djwebhooks.decorators import hook

# The argument to the decorator specifies the event
@hook(event="payment.succeeded")
def send_purchase_confirmation(payment, owner):
    return {
        "order_num": payment.order_num,
        "date": payment.confirm_date,
        "email": payment.email
    }

Once we have this method ready, calling it will send a request to the callback URL specified by the user passed as the parameter owner the returned data as request payload in JSON format. You can also try out Django rest hooks which is in active development. Cheers until the next post.

Comments

Recent Posts

Archive

2022
2021
2020
2019
2018
2017
2016
2015
2014

Tags

Authors

Feeds

RSS / Atom