Django API
Modern web frameworks have simplified the process of building web applications. A key component of these applications is web APIs.
In this article, we will use Django and Django REST Framework to develop a web API focused on authentication and user management. Our API will not only facilitate authentication but also provide capabilities for editing user information. Let's get started. 🚀
🕮 Content provided by KOLAWOLE MANGABO, a passionate developer and author of Full Stack Django and React (best seller)
Setting up the environment
Before starting, let's quickly set up the coding environment. We will start by creating the project directory and a Python virtual environment.
mkdir api-project && cd api-project
python3 -m venv venv # to create a virtual environment.
source venv/bin/activate
Then, we can install the required tools to start coding, such as django
and djangorestframework
, tools that we will use to create the REST API. For authentication, we will use JWT (JSON Web token) and also a package to configure the project CORS.
pip install django djangorestframework pyjwt django-cors-headers
With the tools needed for development, we can create the project and start coding.
Creating the project
Inside the working directory, run the following commands to create a Django project called core
.
django-admin startproject core .
The core
at the end of the command is important as it tells Django to generate the directories and the files in the current directory.
Once it's done, create a package called api
. This package will contain all the apps needed to build authentication and the CRUD operation user feature into the API.
A package in Python is a directory containing a __init__.py
file. Python will treat this directory as a package containing collections of objects you can import and work with.
The api
package will contain two Django applications :
user
to handle user creation and other CRUD operationsauthentication
to deal with everything that concerns authentication, but also sessions.
Let's start by adding theuser
application.
Adding User logic
In the api
directory, create a Django application called user
. Well, we need to build the user feature before adding authentication, as it will mostly depend on the existence of users.
django-admin startapp user
Great! Inside. the user
directory, let's edit the models.py
file so we can add everything needed to create a simple user, a superuser, and the fields of the User
table.
# api/user/models.py
from django.db import models
from django.contrib.auth.models import (
AbstractBaseUser,
BaseUserManager,
PermissionsMixin,
)
class UserManager(BaseUserManager):
def create_user(self, username, email, password=None, **kwargs):
"""Create and return a `User` with an email, username and password."""
if username is None:
raise TypeError("Users must have a username.")
if email is None:
raise TypeError("Users must have an email.")
user = self.model(username=username, email=self.normalize_email(email))
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, username, email, password):
"""
Create and return a `User` with superuser (admin) permissions.
"""
if password is None:
raise TypeError("Superusers must have a password.")
if email is None:
raise TypeError("Superusers must have an email.")
if username is None:
raise TypeError("Superusers must have an username.")
user = self.create_user(username, email, password)
user.is_superuser = True
user.is_staff = True
user.save(using=self._db)
return user
class User(AbstractBaseUser, PermissionsMixin):
username = models.CharField(db_index=True, max_length=255, unique=True)
email = models.EmailField(db_index=True, unique=True, null=True, blank=True)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
date = models.DateTimeField(auto_now_add=True)
USERNAME_FIELD = "email"
REQUIRED_FIELDS = ["username"]
objects = UserManager()
def __str__(self):
return f"{self.username}"
In the code above, we are adding two objects: the User
model class and the UserManager
manager class. In Django, the models
describe the shape of the data to the Django ORM, so it can handle SQL queries such as the creation of the table, the addition of a field, an SQL SELECT
query, and much much more.
On the User
model, we have fields such as username
, email
, is_active
, is_staff
, date
that constitute enough details for a user.
A manager in Django acts as a bridge between the database and the Django model. It contains all collections of methods and classes needed to interact with the database.
In the UserManager
class, we are writing the create_user
and create_superuser
methods for our logic of creating users. It is interesting to create a custom User
and UserManager
when you are writing your authentication logic or just modifying the way the User
table should behave in the project.
Adding User serializer
To make the communication effective in the API, mostly using JSON for payload requests or responses, we need to add a serializer.
In the user
application, create a file called serializers.py
. This file will contain the code for the UserSerializer
.
# api/user/serializers.py
from api.user.models import User
from rest_framework import serializers
class UserSerializer(serializers.ModelSerializer):
date = serializers.DateTimeField(read_only=True)
class Meta:
model = User
fields = ["id", "username", "email", "date"]
read_only_field = ["id"]from api.user.models import User
With the UserSerializer
added, we can then move to add the viewset
, which will act as a controller for the /api/user/
endpoint. Let's add the UserViewset
.
Adding User viewset
In the user
directory, add a file called viewsets.py
. This file will contain the code for the UserViewSet
class. A viewset helps us quickly write the logic for a controller in just a few lines.
Creating Users
# api/user/viewsets.py
from api.user.serializers import UserSerializer
from api.user.models import User
from rest_framework import viewsets, status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.exceptions import ValidationError
from rest_framework import mixins
class UserViewSet(
viewsets.GenericViewSet, mixins.CreateModelMixin, mixins.UpdateModelMixin
):
serializer_class = UserSerializer
permission_classes = (IsAuthenticated,)
error_message = {"success": False, "msg": "Error updating user"}
def update(self, request, *args, **kwargs):
partial = kwargs.pop("partial", True)
instance = User.objects.get(id=request.data.get("userID"))
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
if getattr(instance, "_prefetched_objects_cache", None):
instance._prefetched_objects_cache = {}
return Response(serializer.data)
def create(self, request, *args, **kwargs):
user_id = request.data.get("userID")
if not user_id:
raise ValidationError(self.error_message)
if self.request.user.pk != int(user_id) and not self.request.user.is_superuser:
raise ValidationError(self.error_message)
self.update(request)
return Response({"success": True}, status.HTTP_200_OK)
In the code above, we are creating a viewsets and only allowing POST
requests to this controller to edit user information.
{
"username": "test",
"password": "pass",
"email": "test@appseed.us",
"userID": 4
}
Now that we have the user viewsets, we can code the authentication feature of the API.
Authentication
In the api
directory, run the Django command to create a Django application called authentication
. This application will contain all logic for authentication but also logic to handle sessions.
django-admin startapp authentication
Let's get to work. Replace the models.py
file with Python package models. Inside this package, add a file called active_session
. This file will contain the code of the ActiveSession
model.
# api/authentication/models/active_session.py
from django.db import models
class ActiveSession(models.Model):
user = models.ForeignKey("api_user.User", on_delete=models.CASCADE)
token = models.CharField(max_length=255)
date = models.DateTimeField(auto_now_add=True)
Now that we have the models to handle sessions, let's create a backend class to handle authentication.
Adding an auth backend
Inside the authentication
directory, create a file called backends.py
.
# api/authentication/backends.py
import jwt
from rest_framework import authentication, exceptions
from django.conf import settings
from api.user.models import User
from api.authentication.models import ActiveSession
class ActiveSessionAuthentication(authentication.BaseAuthentication):
auth_error_message = {"success": False, "msg": "User is not logged on."}
def authenticate(self, request):
request.user = None
auth_header = authentication.get_authorization_header(request)
if not auth_header:
return None
token = auth_header.decode("utf-8")
return self._authenticate_credentials(token)
def _authenticate_credentials(self, token):
try:
jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
except:
raise exceptions.AuthenticationFailed(self.auth_error_message)
try:
active_session = ActiveSession.objects.get(token=token)
except:
raise exceptions.AuthenticationFailed(self.auth_error_message)
try:
user = active_session.user
except User.DoesNotExist:
msg = {"success": False, "msg": "No user matching this token was found."}
raise exceptions.AuthenticationFailed(msg)
if not user.is_active:
msg = {"success": False, "msg": "This user has been deactivated."}
raise exceptions.AuthenticationFailed(msg)
return (user, token)
The ActiveSessionAuthentication
class in Django REST API operates as follows, with a focus on JWT (JSON Web Token) handling:
- Initialization: The process begins by setting
request.user
toNone
and retrieving the authorization header from the incoming request. - Header Verification: If no authorization header is present, the method returns
None
, implying that no authentication is attempted. - Token Extraction: The method decodes the JWT token from the authorization header. JWT tokens are compact, URL-safe means of representing claims to be transferred between two parties.
- JWT Validation:
- The token is decoded using Django's
SECRET_KEY
. This step verifies the token's signature and ensures it was not tampered with. - The decoding also checks the token's expiration (
exp
claim). If the token is expired or invalid, an authentication failed exception with an appropriate error message is raised.
- The token is decoded using Django's
- Active Session Check:
- It looks for an
ActiveSession
object that matches the token. The presence of an active session indicates that the user previously logged in and the session is still valid. - If no matching active session is found, it suggests either the token is invalid or the session has ended, leading to an authentication failed exception.
- It looks for an
- User Verification:
- The user associated with the active session is retrieved.
- It checks if the user exists and is active. If the user does not exist or has been deactivated, an authentication failed exception with a specific message is raised.
- Successful Authentication:
- If all validations are passed, the method returns the user and the token.
- This successful return allows the user to proceed with actions that require authentication.
With the authentication class added, let's configure REST_FRAMEWORK
so it can use this backend for authentication.
In the core/settings.py
file, add these few lines of code at the end.
# core/settings.py
...
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"api.authentication.backends.ActiveSessionAuthentication",
),
"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
}
With the authentication backend added, we can quickly move to the register, login, and logout feature.
Adding Registration and Login serializers
In the authentication
package, add a file called serializers.py
. This file will contain code for the LoginSerializer
and the RegisterSerializer
.
# api/authentication/serializers.py
import jwt
from rest_framework import serializers, exceptions
from django.contrib.auth import authenticate
from datetime import datetime, timedelta
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from api.authentication.models import ActiveSession
def _generate_jwt_token(user):
token = jwt.encode(
{"id": user.pk, "exp": datetime.utcnow() + timedelta(days=7)},
settings.SECRET_KEY,
)
return token
class LoginSerializer(serializers.Serializer):
email = serializers.CharField(max_length=255)
username = serializers.CharField(max_length=255, read_only=True)
password = serializers.CharField(max_length=128, write_only=True)
def validate(self, data):
email = data.get("email", None)
password = data.get("password", None)
if email is None:
raise exceptions.ValidationError(
{"success": False, "msg": "Email is required to login"}
)
if password is None:
raise exceptions.ValidationError(
{"success": False, "msg": "Password is required to log in."}
)
user = authenticate(username=email, password=password)
if user is None:
raise exceptions.AuthenticationFailed({"success": False, "msg": "Wrong credentials"})
if not user.is_active:
raise exceptions.ValidationError(
{"success": False, "msg": "User is not active"}
)
try:
session = ActiveSession.objects.get(user=user)
if not session.token:
raise ValueError
jwt.decode(session.token, settings.SECRET_KEY, algorithms=["HS256"])
except (ObjectDoesNotExist, ValueError, jwt.ExpiredSignatureError):
session = ActiveSession.objects.create(
user=user, token=_generate_jwt_token(user)
)
return {
"success": True,
"token": session.token,
"user": {"_id": user.pk, "username": user.username, "email": user.email},
}
class RegisterSerializer(serializers.ModelSerializer):
password = serializers.CharField(min_length=4, max_length=128, write_only=True)
username = serializers.CharField(max_length=255, required=True)
email = serializers.EmailField(required=True)
class Meta:
model = User
fields = ["id", "username", "password", "email", "is_active", "date"]
def validate_username(self, value):
try:
User.objects.get(username=value)
except ObjectDoesNotExist:
return value
raise ValidationError({"success": False, "msg": "Username already taken."})
def validate_email(self, value):
try:
User.objects.get(email=value)
except ObjectDoesNotExist:
return value
raise ValidationError({"success": False, "msg": "Email already taken."})
def create(self, validated_data):
return User.objects.create_user(**validated_data)
The LoginSerializer
validates user login by requiring email and password. It authenticates the user, checks active status, manages JWT token sessions, and addresses errors like incorrect credentials or expired tokens.
The RegisterSerializer
handles user registration. It confirms the uniqueness of usernames and emails and creates a new user with the provided valid data, including password, username, and email.
With the serializers for the login and the registration feature added, we can then code the viewsets
for this package.
Adding Registration and Login viewsets
In the authentication
directory, create a file called viewsets.py
. This file will contain the code for the register, login, and logout viewsets. We will also add a viewset that will help us check if there is an active session.
# api/authentication/viewsets.py
from rest_framework import viewsets, mixins
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import AllowAny
from api.authentication.serializers import LoginSerializer, RegisterSerializer
from api.authentication.models import ActiveSession
class LoginViewSet(viewsets.GenericViewSet, mixins.CreateModelMixin):
permission_classes = (AllowAny,)
serializer_class = LoginSerializer
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
return Response(serializer.validated_data, status=status.HTTP_200_OK)
class RegisterViewSet(viewsets.ModelViewSet):
http_method_names = ["post"]
permission_classes = (AllowAny,)
serializer_class = RegisterSerializer
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.save()
return Response(
{
"success": True,
"userID": user.id,
"msg": "The user was successfully registered",
},
status=status.HTTP_201_CREATED,
)
class LogoutViewSet(viewsets.GenericViewSet, mixins.CreateModelMixin):
permission_classes = (IsAuthenticated,)
def create(self, request, *args, **kwargs):
user = request.user
session = ActiveSession.objects.get(user=user)
session.delete()
return Response(
{"success": True, "msg": "Token revoked"}, status=status.HTTP_200_OK
)
class ActiveSessionViewSet(viewsets.GenericViewSet, mixins.CreateModelMixin):
http_method_names = ["post"]
permission_classes = (IsAuthenticated,)
def create(self, request, *args, **kwargs):
return Response({"success": True}, status.HTTP_200_OK)
The LoginViewSet
in Django REST API handles user login, using the LoginSerializer to validate data, create a session upon successful authentication, and return a response with appropriate HTTP status.
The RegisterViewSet
deals with user registration, using the RegisterSerializer to validate and create new user data, and responds with user ID and success message upon successful registration.
The LogoutViewSet
manages user logout in Django REST API. It invalidates the user's session by deleting it and responds with a confirmation message.
The ActiveSessionViewSet
provides a method to check active sessions for authenticated users, simply returning a success response.
With the viewsets added, we can now register them in the API routers and start making requests.
Registering the viewsets
In the api
directory, create a file called routers.py
. This file will contain declarations of the endpoints of the API. This is where we will register the viewsets we have created.
# api/routers.py
from api.authentication.viewsets import (
RegisterViewSet,
LoginViewSet,
ActiveSessionViewSet,
LogoutViewSet,
)
from rest_framework import routers
from api.user.viewsets import UserViewSet
router = routers.SimpleRouter(trailing_slash=False)
router.register(r"edit", UserViewSet, basename="user-edit")
router.register(r"register", RegisterViewSet, basename="register")
router.register(r"login", LoginViewSet, basename="login")
router.register(r"checkSession", ActiveSessionViewSet, basename="check-session")
router.register(r"logout", LogoutViewSet, basename="logout")
urlpatterns = [
*router.urls,
]
And finally, to make it accessible from the Django server, let's register urlspatterns
in core/urls.py
.
# core/urls.py
from django.urls import path, include
from django.contrib import admin
from api.authentication.viewsets.social_login import GithubSocialLogin
urlpatterns = [
path('admin/', admin.site.urls),
path("api/users/", include(("api.routers", "api"), namespace="api"))
]
We have implemented the REST API for authentication and user edition. Let's start the server and start testing.
python manage.py runserver makemigrations
python manage.py runserver migrate
python manage.py runserver
The server will be accessible at http://localhost:8000
. Here are the endpoints and some data you can use for the requests.
Register - api/users/signup
POST api/users/signup
Content-Type: application/json
{
"username": "test",
"password": "pass",
"email": "test@appseed.us"
}
Login - api/users/login
POST /api/users/login
Content-Type: application/json
{
"password": "pass",
"email": "test@appseed.us"
}
Logout - api/users/logout
POST api/users/logout
Content-Type: application/json
authorization: JWT_TOKEN (returned by Login request)
{
"token": "JWT_TOKEN"
}
You now have a fully operational Django REST API, complete with authentication and permissions. This forms an excellent base for a range of projects, providing a reliable and secure framework to build upon. It's a solid starting point for any web application.
Happy coding! 🚀
✅ Conclusion & Links
In this article, we have learned how to build a REST API with Django and Django REST exploring concepts such as authentication with JWT, permissions, and logout.
You can find the code for this article here.
For more resources, feel free to acces:
- 👉 A list of actively supported Django Starters
- 👉 Admin Dashboards - a huge index with templates and apps
- 🚀 Custom Development Services - provided by AppSeed