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