Django Rest Framework authentication: the easy way


Sun 23 January 2022

In this tutorial you'll learn how to implement Django Rest Framework authentication in your web application by leveraging the built-in Django session framework.

This approach is way simpler (and secure) than other popular methods such as JWT, and has only one requirement: your frontend (think Vue.js, React, ...) and your backend should be served by the same domain.

I created an example project on GitHub you can use to follow along with this tutorial.

Why you should avoid JWT for Django Rest Framework authentication

JWT (Json Web Token) is a very popular method to provide authentication in APIs. If you are developing a modern web application with Vue.js or React as the frontend and Django Rest Framework as the backend, there is an high probability that you are considering JWT as the best method to implement authentication.

The reality is that JWT is just one method, and unfortunately not the simpler, nor the most reliable. JWT is not supported out-of-the-box in Django Rest Framework and requires additional libraries and additional configuration for your project.

Also, implementing JWT in a secure way is quite challenging, and this is due to its complex design. Quoting the words of James Bennet, a long time Django project contributor:

"JWT is over-complex, puts too much power in the attacker's hands, has too many configuration knobs, and makes poor cryptographic choices. This is why we see vulnerabilities in JWT libraries and JWT-using systems again and again and again."

Here is some examples of JWT vulnerabilities found in the wild.

The good news is that if you can control the domain of both your backend and your frontend, you can use a much simpler method: Django sessions.

Django Rest Framework settings

Django Rest Framework comes with built-in session based authentication. To use it you have to add this in your Django settings module:

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.SessionAuthentication',
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ],
}

Remember that authentication deals with recognizing the users that are connecting to your API, while permissions deals with giving access to some resources to the users.

In the DEFAULT_AUTHENTICATION_CLASSES list you are configuring only the good old Django sessions to authenticate users. In the DEFAULT_PERMISSION_CLASSES list you are requiring that only authenticated users will access your endpoints.

Where is my token?

Django sessions are based by default on a session cookie stored on the client. There's no need for a "Token", an "Authorization" header or something like that.

If you can store that session cookie on the client and send it on every request to your API you will authenticate the user.

Django Rest Framework authentication endpoint

Now it's time to write a very simple view to let the users authenticate with a username/password.

We'll need a serializer that will take the username and password from the request and will perform the actual authentication using Django authenication framework.

Create a serializers.py file in your app and add this code to it:

from django.contrib.auth import authenticate

from rest_framework import serializers

class LoginSerializer(serializers.Serializer):
    """
    This serializer defines two fields for authentication:
      * username
      * password.
    It will try to authenticate the user with when validated.
    """
    username = serializers.CharField(
        label="Username",
        write_only=True
    )
    password = serializers.CharField(
        label="Password",
        # This will be used when the DRF browsable API is enabled
        style={'input_type': 'password'},
        trim_whitespace=False,
        write_only=True
    )

    def validate(self, attrs):
        # Take username and password from request
        username = attrs.get('username')
        password = attrs.get('password')

        if username and password:
            # Try to authenticate the user using Django auth framework.
            user = authenticate(request=self.context.get('request'),
                                username=username, password=password)
            if not user:
                # If we don't have a regular user, raise a ValidationError
                msg = 'Access denied: wrong username or password.'
                raise serializers.ValidationError(msg, code='authorization')
        else:
            msg = 'Both "username" and "password" are required.'
            raise serializers.ValidationError(msg, code='authorization')
        # We have a valid user, put it in the serializer's validated_data.
        # It will be used in the view.
        attrs['user'] = user
        return attrs

Then we can use this serializer in a login view. Add this to your views.py file:

from rest_framework import permissions
from rest_framework import views
from rest_framework.response import Response

from . import serializers

class LoginView(views.APIView):
    # This view should be accessible also for unauthenticated users.
    permission_classes = (permissions.AllowAny,)

    def post(self, request, format=None):
        serializer = serializers.LoginSerializer(data=self.request.data,
            context={ 'request': self.request })
        serializer.is_valid(raise_exception=True)
        user = serializer.validated_data['user']
        login(request, user)
        return Response(None, status=status.HTTP_202_ACCEPTED)

Mount your view in the project urls.py:

from django.contrib import admin
from django.urls import path

from users import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('login/', views.LoginView.as_view()),
]

Finally you can migrate the database, create a test user and launch the Django development server:

$ ./manage.py migrate
$ ./manage.py createsuperuser
(... follow instructions ...)
$ ./manage.py runserver

Test authentication using HTTPie

I really like HTTPie command line HTTP client. Maybe because it is written in Python and based on Python requests. :)

You can install it using

$ pip install httpie

It is one of the few packages that I like to install system wide.

After installing HTTPie, let's test a wrong login:

$ http POST :8000/login/ username=guguweb password=wrongpassword
HTTP/1.1 400 Bad Request
( ... some other response headers ... )

{
    "non_field_errors": [
        "Access denied: wrong username or password."
    ]
}

As you can see the error message is returned and the user is not logged in.

Now let's try with the right credentials:

$ http POST :8000/login/ username=guguweb password=mysecret
HTTP/1.1 202 Accepted
( ... some other response headers ... )
Set-Cookie: csrftoken=ALo4uq(...)lLoayts; expires=Tue, 24 Jan 2023 11:00:26 GMT; Max-Age=31449600; Path=/; SameSite=Lax
Set-Cookie: sessionid=l5p5zqjkhuijhjdb84; expires=Tue, 08 Feb 2022 11:00:26 GMT; HttpOnly; Max-Age=1209600; Path=/;

The user is correctly logged in and a session cookie named sessionid has been returned to our client.

If we will persist that session cookie in each request, our user will be persistently authenticated.

A new endpoint to retrieve the user profile

Let's write a view that will let the user to retrieve his user profile. Of course this view will require an already authenticated user.

First of all we need a new serializer that will be used to return the profile information. Add this to the serializers.py file introduced earlier:

from django.contrib.auth.models import User

class UserSerializer(serializers.ModelSerializer):

    class Meta:
        model = User
        fields = [
            'username',
            'email',
            'first_name',
            'last_name',
        ]

Now you can add a view to retrieve the user profile. Remember that we added rest_framework.permissions.IsAuthenticated in our DEFAULT_PERMISSION_CLASSES setting, so each view will require that the user is authenticated, unless otherwise specified.

Add this to your views.py file:

class ProfileView(generics.RetrieveAPIView):
    serializer_class = serializers.UserSerializer

    def get_object(self):
        return self.request.user

And mount the view to an endpoint in your urls.py file:

urlpatterns = [
    ...
    path('profile/', views.ProfileView.as_view()),
]

Test the user profile endpoint using HTTPie

Let's test a unauthenticated call to our new endpoint:

$ http GET :8000/profile/
HTTP/1.1 403 Forbidden
( ... some other response headers ... )

{
    "detail": "Authentication credentials were not provided."
}

The endpoint is correctly protected from unauthenticated requests. Now let's try to add an header with our session cookie.

Where I can find the session cookie value?

You can copy the sessionid value from the previous login request, it was something like this Set-Cookie: sessionid=l5p5zqjkhuijhjdb84;

$ http GET :8000/profile/ Cookie:sessionid=l5p5zqjkhuijhjdb84
HTTP/1.1 200 OK
( ... some other response headers ... )

{
    "email": "[email protected]",
    "first_name": "Augusto",
    "last_name": "Destrero",
    "username": "guguweb"
}

Conclusions

In this tutorial you learned how to implement Django Rest Framework authentication in your web application by leveraging the built-in Django session framework.

This approach assumes that you can control the domain of both your frontend and your backend, and the two layers of the web application can be served from the same top level domain. If this is not the case, you cannot rely on browser cookies to persist the user session between subsequent requests.

In many cases this assumption is reasonable and this approach is way simper to configure and implement with respect to other token based approaches.

Don't forget to clone/fork my example project on GitHub to actually see this project running.

In a follow up post I will show you how to implement a simple Vue.js based application that will connect to the REST API implemented in this tutorial.

Have fun with Django Rest Framework!


Share: