Django REST Framework User Authentication: Under the Hood (HTTP Basic Authentication)

What is Authentication?
It is not uncommon for newbies to confuse authentication with authorization. Authentication is the process of verifying the user’s identity while authorization is granting the user some definite permission to do some definite task. What do we mean by verifying the user’s identity? — Simple. We identify that the user is who they say they are. How do we do that? We ask the user to provide some kind of credential e.g. — their username and password, some kind of string value provided by the server or anything that will help us to verify the user’s claim about their identity.

Authentication in Django REST Framework
Django REST Framework comes with multiple authentication schemes built-in. It’s easy to hook up one or more of those schemes in Django’s settings.py file. This post is intended to those who want to see how those schemes work in the context of the REST Framework. Let’s dive in.

HTTP Basic Authentication
HTTP Basic Authentication scheme is defined in RFC 7617. User credentials such as — username and password are transmitted in the Authorization HTTP header as a base64 encoded string. The server will then do the following —

1. Extract the value from Authorization HTTP header.
2. Check if the authentication scheme told by client is Basic
3. Decode the base64 encoded string. It will give a string of format username:passowrd. Extract username and password from the decoded string.
4. Verify that the username, password combination matches with server’s data source.

Django REST Framework has built-in support for HTTP Basic Authentication. All you need to do is include the following in your settings.py file —

REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.BasicAuthentication',
]
}

Since we are going to see how these built-in classes work, we will now code this BasicAuthentication class by ourselves. I’ll assume that you already have a django project ready and djangorestframework installed in your environment. Add rest_framework to the list of INSTALLED_APPS in settings.py if you haven’t already. We will create a new app named myapp and add it to INSTALLED_APPS list.

We will use the default User model from django.contrib.auth.models . For the purpose of this article no other models are required. Let’s get rid of myapp/models.py, myapp/admin.py, myapp/tests.py . Our application consists of only one view with an authenticated GET request.

# myapp/views.pyfrom rest_framework import status
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated


class AuthenticatedView(APIView):
permission_classes = [IsAuthenticated, ]

def get(self, request):
msg = {'message': f'Hi {request.user.username}! Congratulations on being authenticated!'}
return Response(msg, status=status.HTTP_200_OK)

Let’s update the ulrs accordingly

# authmechanics/urls.pyfrom django.urls import path

from myapp.views import AuthenticatedView

urlpatterns = [
path('', AuthenticatedView.as_view()),
]

Now we’ll be able to access the view at localhost:8000 . Before we do that, let’s create an user with username: aladin and password: opensesame via django shell. Our request to localhost:8000/ results in to a 403 FORBIDDEN HTTP response.

This has happened because — We did not tell rest_framework about how to authenticate users in settings.py file.

When you need to authenticate users in some way other than the default mechanisms provided by DRF, you have to implement it yourself. In order to tell DRF how to authenticate users, DRF requires you to provide a class with a method name authenticate that will receive a request object as argument and return a tuple of User and auth objects. These will be accessible via request.user and request.auth attributes. The auth object is any extra information that you may want to provide about the authenticated user. You may choose to return None too in place of the auth object. But returning None for user will result in to the request being considered as unauthenticated.

To help us, DRF comes with rest_framework.authentication.BaseAuthentication class. We just have to inherit from this class and override the authenticate method. Let’s create a new file myapp/authentication.py make a class BasicAuthentication which inherits from rest_framework.authentication.BaseAuthentication class.

# myapp/authentication.pyfrom rest_framework.authentication import BaseAuthentication
class BasicAuthentication(BaseAuthentication):
def authenticate(self, request):
pass

Let’s recap what the server does during HTTP Basic Authentication. First step is to extract the Authorization header. We can access HTTP headers in a dictionary attribute of request object called request.META where, the name of the headers are dictionary keys and their respective values are the well, values! Django does a small thing in between — adds a http_ prefix before the header name and capitalizes the resultant string i.e. our Authorization header becomes HTTP_AUTHORIZATION key in request.META

# myapp/authentication.pyfrom rest_framework.authentication import BaseAuthentication
def get_authorization_header(request):
auth = request.META.get("HTTP_AUTHORIZATION", "")
return auth

class BasicAuthentication(BaseAuthentication):
def authenticate(self, request):
auth = get_authorization_header(request).split()

The Authorization header in HTTP Basic Authentication looks like — Authorization: Basic dXNlxm5hbWU6cGFzc3dvcmQ=
We have spit our header in order to extract the credential part of the header. We also have to ensure that the authentication schema is of HTTP Basic Authentication type.

# myapp/authentication.pyfrom rest_framework import exceptions
from rest_framework.authentication import BaseAuthentication

def get_authorization_header(request):
auth = request.META.get("HTTP_AUTHORIZATION", "")
return auth

class BasicAuthentication(BaseAuthentication):
def authenticate(self, request):
auth = get_authorization_header(request).split()

if not auth or auth[0].lower() != "basic":
return None

if len(auth) == 1:
raise exceptions.AuthenticationFailed("Invalid basic header. No credentials provided.")
if len(auth) > 2:
raise exceptions.AuthenticationFailed("Invalid basic header. Credential string is not properly formatted")

Next, we need to decode the base64 encoded credential and verify the user’s identity. Recap — Credential is a base64 encoded version of the string username:password

# myapp/authentication.pyimport base64

from django.contrib.auth import authenticate
from django.contrib.auth import get_user_model

from rest_framework import exceptions
from rest_framework.authentication import BaseAuthentication

def get_authorization_header(request):
auth = request.META.get("HTTP_AUTHORIZATION", "")
return auth

class BasicAuthentication(BaseAuthentication):
def authenticate(self, request):
auth = get_authorization_header(request).split()

if not auth or auth[0].lower() != "basic":
return None

if len(auth) == 1:
raise exceptions.AuthenticationFailed("Invalid basic header. No credentials provided.")
if len(auth) > 2:
raise exceptions.AuthenticationFailed("Invalid basic header. Credential string is not properly formatted")
try:
auth_decoded = base64.b64decode(auth[1]).decode("utf-8")
username, password = auth_decoded.split(":")
except (UnicodeDecodeError, ValueError):
raise exceptions.AuthenticationFailed("Invalid basic header. Credentials not correctly encoded")

return self.authenticate_credentials(username, password, request)

def authenticate_credentials(self, username, password, request=None):
credentials = {
get_user_model().USERNAME_FIELD: username,
"password": password
}

user = authenticate(request=request, **credentials)

if user is None:
raise exceptions.AuthenticationFailed("Invalid username or password")

if not user.is_active:
raise exceptions.AuthenticationFailed("User is inactive")

return user, None

Our BasicAuthentication class is now done. Let’s tell rest_framework to use this class for authentication.

# authmechanics/settings.pyREST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'myapp.authentication.BasicAuthentication',
]
}

Now make a authenticated request with postman. Open the authorization tab and select Basic Auth from the Type drop down. Put the username and password for our user aladin we created before.

Send the request. Viola! We have our intended response :D

Let’s see what happens when we remove the Authorization header. From Authorization Tab select No Auth for Type and send the request.

It behaves as expected- Without credentials in Authorization header we cannot access the view. But not quite done! Our Response status is 403 Forbidden . HTTP has distinct response for unauthenticated requests — 401 Unauthorized but our server is not doing so. In HTTP Authentication process, in case of an unauthenticated request, the server will response with a 401 Unauthorized and provide information about how to authenticate in WWW-Authenticate header. Our server is not providing any such information and returning 403 Forbidden because it is the default behavior for permissions in DRF (We have put IsAuthenticated permission class in our views permission_classes). We can provide this information from our Authentication class. The information format of WWW-Authenticate header is WWW-Authenticate: <Type> realm=<realm> Our Type is Basic and the realm part is optional if omitted then is populated by the hostname of the server. Since, we design REST APIs with DRF, we will say our reaml is api . So our WWW-Authenticate header will look like WWW-Authenticate: Basic realm=api .

rest_framework.authentication.BaseAuthentication has another method named authenticate_header that returns a string. This string is then set as a value of the WWW-Authenticate header in response. We will override authenticate_header in our BasicAuthentication class.

#myapp/authentication.py class BasicAuthentication(BaseAuthentication):
www_authenticate_realm = "api"

def authenticate_header(self, request):
return f"Basic realm={self.www_authenticate_realm}"

def authenticate(self, request):
auth = get_authorization_header(request).split()

if not auth or auth[0].lower() != "basic":
return None

if len(auth) == 1:
raise exceptions.AuthenticationFailed("Invalid basic header. No credentials provided.")
if len(auth) > 2:
raise exceptions.AuthenticationFailed("Invalid basic header. Credential string is not properly formatted")

try:
auth_decoded = base64.b64decode(auth[1]).decode("utf-8")
username, password = auth_decoded.split(":")
except (UnicodeDecodeError, ValueError):
raise exceptions.AuthenticationFailed("Invalid basic header. Credentials not correctly encoded")

return self.authenticate_credentials(username, password, request)

def authenticate_credentials(self, username, password, request=None):
credentials = {
get_user_model().USERNAME_FIELD: username,
"password": password
}

user = authenticate(request=request, **credentials)

if user is None:
raise exceptions.AuthenticationFailed("Invalid username or password")

if not user.is_active:
raise exceptions.AuthenticationFailed("User is inactive")

return user, None

Any unauthenticated requests will now receive a 401 Unauthorized response.

We also have the WWW-Authenticate header with the value we returned from authenticate_header method.

The full source code is available here

Also checkout Apploye :D

Senior Software Engineer at Apploye Inc.