A cute cartoon image of a Python with a shield.

Django REST Framework Security: Permissions and Authentication

Written by

All of the code for this tutorial is here.

If you’re unfamiliar, Django REST Framework (or DRF) is a Python package developed by the software foundation Encode that adds a lot of useful functionality to Django. It makes it incredibly easy to create HTTP endpoints for your database models and cuts down on the amount of repetitive code that you would otherwise need to write yourself.

An Overview of How Django REST Framework Works

Let’s start with an example. An incredibly common thing to have to do for an application is connect to an external payment provider like Stripe, Square, or Paddle. You might want to store some information in your database for quick access, such as what external ID to use. A model might look a lot like this.

from django.db import models
from django.contrib.auth.models import User

class PaymentMethod(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    account_mask = models.TextField()
    external_service_id = models.TextField()
    external_service_type = models.TextField()

Django REST Framework introduces several concepts that make adding a REST API to interact with this model incredibly easy. The two primary concepts are Viewsets and Serializers.

The relationship between these concepts and Django is outlined in the following image.

A flowchart showing how Django REST Framework fits into Django.

Serializers are a superclass that tells DRF how to convert your models into an API response. For example, we’ll eventually want our API to response to respond with a JSON object with all of the fields above as keys (as well as the ID). We can use a serializer to tell DRF which fields to include. In serializers.py, you can include this.

from rest_framework import serializers
from .models import PaymentMethod

class PaymentMethodSerializer(serializers.ModelSerializer):
    class Meta:
        model = PaymentMethod
        fields = '__all__'

There are a ton of different types of serializers, but ModelSerializer is probably the one that I use the most. The example above gives us the basic functionality of converting any model into an API response, but you can further customize it by overriding its to_representation method.

Viewsets are the heart of the Django REST framework. They define all of the routing for our model. We can easily create all of our CRUD routes with the following code.

from rest_framework import viewsets

from .models import PaymentMethod
from .serializers import PaymentMethodSerializer

class PaymentMethodViewset(viewsets.ModelViewSet):
    queryset = PaymentMethod.objects.all()
    serializer_class = PaymentMethodSerializer

After some URL management, which I have laid out in the code example, you can get routes to list all payment methods, view a specific payment method, create a new payment method, update an existing payment method, or delete a payment method.

I routed mine to /api/billing/paymentmethods, so I can create new payment methods for two users (with IDs 1 and 2) with the following curl commands:

curl -X POST \
		 -H 'Content-Type: application/json' \
     --data '{"user": 1, "account_mask": "1234", "external_service_id": "external1", "external_service_type": "stripe"}' \
		'http://localhost:8000/api/billing/paymentmethods/?format=json'

And:

curl -X POST \
		 -H 'Content-Type: application/json' \
     --data '{"user": 2, "account_mask": "5678", "external_service_id": "external2", "external_service_type": "square"}' \
		'http://localhost:8000/api/billing/paymentmethods/?format=json'

These should both succeed. Likewise, if you visit, http://localhost:8000/api/billing/paymentmethods/?format=json, you should be able to see both of the accounts.

Securing Django REST Framework ViewSets

There are several security concerns with our setup. Firstly, I was able to create accounts for both users despite not being logged in. In practice, this means that anyone with an internet connection could start adding payment methods to any arbitrary user in our database.

Also, I was able to see both users’ account information despite not being either user. Even if I were logged in as one of the users, I would still be able to see all the payment methods and create new payment methods for any other user.

Using Permissions to Require Authentication

Django REST Framework makes it easy to block unauthenticated requests using the IsAuthenticated permission. All you have to do is add it to your viewset like this.

from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated

from .models import PaymentMethod
from .serializers import PaymentMethodSerializer

class PaymentMethodViewset(viewsets.ModelViewSet):
    queryset = PaymentMethod.objects.all()
    serializer_class = PaymentMethodSerializer
    permission_classes = [IsAuthenticated]

You can verify this works by going to http://localhost:8000/api/billing/paymentmethods/?format=json in an incognito or private window. You should get a 403 error. The easiest way to verify that you can see this route when authenticated is to log in as the admin user and go to that route in the same window.

Django REST Framework has all sorts of permissions that you can view here.

It’s also very easy to create custom permissions by overriding the BasePermission class like this.

from rest_framework import permissions

class HasValidSubscription(permissions.BasePermission):
    message = "Your subscription is invalid"

    def has_permission(self, request: HttpRequest, view) -> bool:
        return (
            request.user is not None and
            is_user_subscription_status_valid(request.user)
        )

Using QuerySets to Limit a User to their own data

Our viewset currently has an issue where we can see all users’ payment methods when we go to the list route. We can override the viewset’s get_queryset function to make sure that a user can only access their own data.

from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated

from .models import PaymentMethod
from .serializers import PaymentMethodSerializer

class PaymentMethodViewset(viewsets.ModelViewSet):
    serializer_class = PaymentMethodSerializer
    permission_classes = [IsAuthenticated]

    def get_queryset(self):
        return PaymentMethod.objects.filter(user=self.request.user)

If we go to the list page, we should only see our own payment methods. However, we can still create payment methods assigned to other users. This is a similarly easy fix since we can override the viewset’s perform_create function like this.

from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated

from .models import PaymentMethod
from .serializers import PaymentMethodSerializer

class PaymentMethodViewset(viewsets.ModelViewSet):
    serializer_class = PaymentMethodSerializer
    permission_classes = [IsAuthenticated]

    def get_queryset(self):
        return PaymentMethod.objects.filter(user=self.request.user)

    def perform_create(self, serializer):
        serializer.save(user=self.request.user)

And there you have it! Now our viewset is pretty secure from an application perspective. Users can only access their own data and create new data for themselves. And we did it with very little code.

Back