This article assumes you're familiar with Django and the basics of the Django REST framework web framework.
Implementing a token authentication in REST framework is not quite straightforward. After browsing the docs and scanning through blog posts, I found an "easy" way to implement the TokenAuthentication scheme.
Before delving in, it's best to explain just a bit what Django REST framework is, and some of its essential concepts.
Django REST Framework (DRF).
The Django REST framework is python framework for building decoupled and "API-first" web application. It allows you to convert your existing Django models to format that translated to and from JSON.
As the name suggests, DRF is built on top of Django framework. In fact, it was built to enable building REST API using the Django framework.
Key Concepts of DRF
Serializer
"Serializer" in DRF allow the serializing and deserializing Django model instances into different representation, such as json. This is done by creating a serializer.py
file in the desired directory followed by the corresponding code responsible for doing the serialization.
View
Just as Django has the views.py
file responsible for building logic and presenting data to the client in different forms. DRF builds on top that idea and provide its own views for presenting data. They are two ways to represent views in DRF: function-based views and class-based views.
In this post, I'll be using class-based views that'll inherit from REST framework's APIView
.
Why the APIView
?
- It's optimize for content negotiation and setting the correct renderer on the response.
- It catches all
APIException
exceptions and returns appropriate responses. - Incoming requests are inspected and appropriate permission and/or throttle checks will be run before dispatching the request to the handler method.
NOTE
Some developers prefer the naming the views file
api.py
. This is to keep separated from Django'sviews.py
file.
Requests and responses
The REST framework provides a Response
object that extends Django's HttpRequest
. The extension allows it to provide a robust way of parsing requests. The Response
object provide a request.data
attribute that's similar to Django view's request.POST
but its more suitable for building web APIs.
Routing
The REST framework has the router
module for API URL routing to Django. It provides a consistent way of writing view logic to a set of URLs. To create routes for URLs, you create a router.py
file in the desired directory and then include
it in the Django URLconf (URL configuration).
The implementation in this post will not be using the REST framwork router, but Django's URLconf.
Token authentication
This is an HTTP authentication scheme that uses token as means for verifying and granting access to clients. Only clients with valid token granted access. A token is passed as a payload to the HTTP Authorization
header for every request. The server receives the token and checks it with what it has stored. If the tokens match, then the client is verified and given access.
Installation and Configuration
To set up an isolated environment, you have use a virtual environment.
Installing Django & DRF
If the virtual environment is set up and activated, run pip install django djang-restframework
in the terminal to install Django and DRF.
To start a project, run python django-admin startproject [project name]
to start a new project. I'll be calling mine authentication.
Installed Apps
Django has to recognize and use the REST framework and the token authentication scheme. Add them to the list of installed apps in the settings.py file.
# Application definition
INSTALLED_APPS = [
...
"rest_framework",
"rest_framework.authtoken"
...
]
Setting Authentication Class & Permission.
Still in the settings.py file, the default authentication scheme will be set to TokenAuthentication
and permission to AllowAny
. The AllowAny
is a general setting that applies to all views
of application, it allows the client to have access to all available views of the web app. However, this can be further streamlined to restrict certain views to authenticated users only, which you'll see as you move on.
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.TokenAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.AllowAny",
]
}
Migration.
To apply changes that require database integration (Django comes with default SQLite), they have to be migrated. Run python manage.py migrate
to apply such changes.
TokenAuthentication.
TokenAuthentication is differs from SessonAuthentication. No state is created on the server when using a token authentication scheme. Only the validity of the token is checked. If there's a match, the user is granted access, or more definitely, is authenticated.
Create User.
A user account is needed to test the implementations. Django's manage.py
command utility can be used to create a super user account.
python manage.py createsuperuser --username johndoe --email johndoe@gmail.com
Test View
Let's create a view with permission set only for authenticated users. Requests sent to the view with an invalid token won't be granted access. The view will handle a GET request that return a message of Hello, World! for valid tokens.
# views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
class HelloView(APIView):
permissions_classes = [IsAuthenticated] # <-------- Only authenticated users can access this view
def get(self, request):
context = {"message": "Hello, World!"} # <------ Response to the client
return Response(context)
Add url pattern and view to the URLconf file, urls.py
# urls.py
from django.urlsconf import path
from authentication.views import HelloView
urls_patterns = [
path("/api-token-auth", HelloView.as_view())
]
Let's test the view. A request without token will be sent.
Python has a lightweight HTTP client package called httpie. Install it and follow along.
..authentication> http http://localhost:8000/api-is-authenticated
HTTP/1.1 401 Unauthorized
Allow: GET, HEAD, OPTIONS
...
Content-Type: application/json
...
WWW-Authenticate: Token # <----------- Take note of this
...
{
"detail": "Authentication credentials were not provided."
}
From the above, the request wasn't authenticated because no [valid] token was associated with the request. Also, if you inspect the header, you'd notice the www-Authentication: Token
payload. That is to tell the client that a token is needed to access data on the http://localhost:8000/api-token-auth
URL. Later on, we'll test the view again, but with a valid token.
Registration
The registration authentication is responsible for handling registration and creating user account. JSON request from the client will hold the credentials required to create the account. If credentials are valid, the account will be created.
To check if data is valid, it has to cross-check with serialized model data.
Create a serializers.py
file and write the following code:
# serializers.py
from django.contrib.auth.models import User
from rest_framework import serializers
class RegistrationSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = [
"id",
"username",
"first_name",
"last_name",
"email",
"password",
"is_active",
"is_staff"
]
extra_kwargs = {"id": {"read_only": True}, "password": {"write_only": True}}
read_only_fields
def create(self, validated_data):
username = validated_data["username"]
first_name = validated_data["first_name"]
last_name = validated_data["last_name"]
email = validated_data["email"]
password = validated_data["password"]
user = User.objects.create_user(
username=username,
first_name=first_name,
last_name=last_name,
email=email,
password=password,
)
return user
For the view code to handle logic for incoming request. The request will be a POST request.
The view will pass incoming data to the RegistrationSerializer
class for validating and creating the user. The serializer will return an object that tells if the data is valid or not. The view will check if data is valid. If valid, it'll save it, generate token, and return response. If not valid, it'll return error response.
# views.py
from django.contrib.auth.models import User
from authentication.serializers import RegistrationSerializer
from rest_framework import serializers
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.authtoken.models import Token
from rest_framework import permissions
class RegistrationView(APIView):
"""Registeration View"""
def post(self, request, *args, **kwargs):
"""Handles post request logic"""
registration_serializer = RegistrationSerializer(data=request.data)
# Generate tokens for existing users
for user in User.objects.all():
if not user:
break
else:
try:
Token.objects.get(user_id=user.id)
except Token.DoesNotExist:
Token.objects.create(user=user)
if registration_class.is_valid():
user = registration_serializer.save()
token = Token.objects.create(user=user)
return Response(
{
"user": {
"id": serializer.data["id"],
"first_name": serializer.data["first_name"],
"last_name": serializer.data["last_name"],
"username": serializer.data["username"],
"email": serializer.data["email"],
"is_active": serializer.data["is_active"],
"is_staff": serializer.data["is_staff"],
},
"status": {
"message": "User created",
"code": f"{status.HTTP_200_OK} OK",
},
"token": token.key,
}
)
return Response(
{
"error": serializer.errors,
"status": f"{status.HTTP_203_NON_AUTHORITATIVE_INFORMATION} \
NON AUTHORITATIVE INFORMATION",
}
)
next, create the URL pattern and add the view responsible for handling request to the URL.
# urls.py from django.urlsconf import path from authentication.views import RegistrationViewurls_patterns = [ #... path("/api-register-auth", RegistrationView.as_view())]
Lastly, test the RegistrationView
. It should return the appropriate response with a token if required and valid credentials are provided.
http --json POST http://localhost:8000/api/auth/register username="JamesChe" email="jamesuche@gmail.com" first_name="james" last_name="uche" password="jamesuche123456"{ "status": { "code": "200 OK", "message": "User created" }, "token": "b4784f96c3c65387bc8ea6463d5d4658cb32b0ac", "user": { "id": 2, "first_name": "james", "last_name": "uche", "username": "JamesChe", "email": "jamesuche@gmail.com", "is_active": true, "is_staff" true, }}
Login
Implementing Login authentication using the REST framework is quite straightforward. There are two way to achieve setting up a login view: using built-in view or a custom view.
Using obtain_auth_token
view.
REST framework provides the built-in obtain_auth_token
view for creating and retrieving tokens for authenticated user.
Clients can obtain token using registered credential. The username and password are what's required. Once the view confirms the credentials, It'll obtain the token if it exists or create a new one.
In the urls.py file, import as follows: from rest_framework.authtoken.views import obtain_auth_token
then add the view to the URLconf.
# urls.pyfrom rest_framework.authtoken.views import obtain_auth_tokenurl_patterns + [ path("/api-token-auth", obtain_auth_token.as_views())]
NOTE:
The naming of the URL pattern can be whatever you want.
To get token, the client have to send a post request to the URL. The client must provide the username and password of the registered user.
..authentication> http post http//localhost:800/api-token-auth/ username="johndoe" password=123456HTTP/1.1 200 OKAllow: OPTION, POSTSContent-Type: application/json......{ "token": "bad492c010451bcba6acf7437706b8dd30eb11d5"}
Remember the earlier HelloView
view? Let's test it by sending a GET request to its URL, this time with the token we just got.
..authentication> http http://localhost:8000/api-hello "Authorization: Token bad492c010451bcba6acf7437706b8dd30eb11d5"HTTP/1.1 200 OKAllow: GET, HEAD, OPTIONS.........{ "message": "Hello, World!" # <------------ Message because user token is valid.}
And there! We're now able to access the view without restriction. the expected Hello, World! message is returned rather than a restriction message.
Using Custom View
Certain cases require extra data about the authenticated user be sent as response to the client. That "extra" data could be on a database. To retrieve such data, a serializer is needed to convert them to python native code format for easy serializing. The view will handle the necessary related logic and send response to the client.
Two serializer classes are needed for this: one to verify the user credentials and the other to return the necessary response.
# Serializers.pyfrom django.contrib.auth.models import Userfrom rest_framework import serializersclass UserLoginSerializer(serializers.Serializer): """Login serializer""" username = serializers.CharField(required=True) password = serializers.CharField(required=True, read_only=True)class UserLoginResponse(serializers.ModelSerializer): """Response serializer""" class Meta: model = User fields = "id, username, first_name, last_name, email, password, is_active, is_staff" read_only_fields = ["id", "password", "is_active", "is_staff"]
Next, in the views.py file, the view has to check if the incoming data from the request is a valid data using the serializer. If it's not it'll return a serializer error. if the data is valid, the credentials will be authenticate and a response message with the generated or obtained token will be sent back to the client.
# views.pyfrom django.contrib.auth.models import Userfrom authentication.serializers import LoginSerializerfrom authentication.serializers import LoginResponseSerializerfrom rest_framework import serializersfrom rest_framework.views import APIViewfrom rest_framework.response import Responsefrom rest_framework import statusfrom rest_framework.authtoken.models import Tokenfrom rest_framework import permissionsfrom django.contrib.auth import authenticateclass LoginView(APIView): """Login View""" permissions_classes = [permissions.isAuthenticated] def post(self, request, *args, **kwargs): login_serializer = LoginSerializer(data=request.data) if login_serializer.is_valid(): user = authenticate(request, **serializer.data) if user is not None: response_class = LoginResponseSerializer(user) token, created_token = Token.objects.get_or_create(user_id=user.id) if isinstance(created_token, Token): token = created_token.key return Response( { "user": response_serializer.data, "status": { "message": "User Authenticated", "code": f"{status.HTTP_200_OK} OK", }, "token": token.key, } ) else: raise serializers.ValidationError( { "error": { "message": "Invalid Username or Password. Please try again", "status": f"{status.HTTP_400_BAD_REQUEST} BAD REQUEST", } } ) return( { "error": serializer.errors, "status": f"{status.HTTP_403_FORBIDDEN} FORBIDDEN" } )
Create URL pattern and add view to URLConf file.
# urls.pyfrom django.urls import pathfrom authentication.views import LoginViewurlpatterns = [ #... #... path("/api-login-auth", views.LoginView),]
To test out the custom view, send a POST request with the required user credentials as JSON.
..authentication> http --json post http//localhost:800/api-login-auth username="johndoe" password=123456HTTP/1.1 200 OKAllow: OPTION, POSTS...Content-Type: application/json......{ "status": { "code": "200 OK", "message": "User Authenticated" }, "token": "bad492c010451bcba6acf7437706b8dd30eb11d5", "user": { "email": "johndoe@gmail.com", "first_name": "", "id": 1, "is_active": true, "is_staff": true, "last_name": "", "username": "johndoe" }}
Conclusion
Implementing TokenAuthentication in Django REST framework can be steep at first. But it start to make sense when you understand the concept: Rather having a session on the server, a token is instead created and is used to verify the user on every request.
I recommend using the REST framework APIView classes-bases for easy work. Only when keep repeating yourself should you look into other class-based to reduce repetition.
If all the application needs is just the token, then use the built-in obtain_auth_token
view for that.
Customs views may need extra data returned to the user, in this case you should create extra serializer classes to achieve that.
I'll appreciate your feedback on this post. Cheers!
Top comments (1)
a lot of typos in code, i.e.
permissions_classes
should bepermission_classes
andurls_patterns
should beurlpatterns