Hello!
In our last lesson, we established the theoretical groundwork for authorization by comparing Role-Based Access Control (RBAC) and Attribute-Based Access Control (ABAC). We learned that authorization is about what an identified user is allowed to do. But before we can check permissions, we must first reliably figure out who the user is. This is authentication.
Today, we transition from theory to practice. This lesson directly addresses the learning outcome: Implement JWT authentication in a Django application, including token generation and validation middleware. We will build a complete, working authentication system for a Django REST Framework API using JSON Web Tokens (JWTs).
You'll learn how to:
- Set up a project with
djangorestframework-simplejwt, the standard library for this task. - Create endpoints for user registration and token generation.
- Protect API endpoints so only authenticated users can access them.
- Understand how Django's authentication middleware validates a token behind the scenes.
- Test the entire authentication flow from end to end.
This hands-on work will create the foundation for our next lesson, where we'll implement RBAC on top of the authenticated user we establish today.
What is a JWT? A Quick Refresher
As a brief recap, a JSON Web Token (JWT) is a compact, URL-safe standard for creating access tokens that assert some number of claims. It's a stateless mechanism: the server doesn't need to store any information about the token after issuing it. All the necessary information is inside the token itself.
A JWT consists of three parts separated by dots (.):
HEADER.PAYLOAD.SIGNATURE

The key thing to remember is that the signature guarantees the token's integrity, not its secrecy. The header and payload are Base64Url encoded, not encrypted, so anyone can read them. Therefore, you must never store sensitive information like passwords in the payload.
The JWT Authentication Flow
The process of using JWTs for authentication follows a standard pattern:

- Login: The client sends credentials (e.g., email and password) to a login endpoint.
- Token Generation: The server validates the credentials. If they are correct, it creates a JWT containing the user's ID (
user_id) and an expiration time (exp). This token is signed with a secret key known only to the server. - Token Storage: The server sends the JWT back to the client. The client stores it securely (e.g., in memory or an HTTP-only cookie).
- Authenticated Requests: For every subsequent request to a protected API endpoint, the client includes the JWT in the
Authorizationheader, typically using theBearerscheme:
Authorization: Bearer <your_jwt_token> - Token Validation: The server's authentication middleware intercepts the request, extracts the token, and validates it by:
- Checking the signature using its secret key.
- Verifying that the token has not expired.
- If validation succeeds, the server decodes the user ID from the payload and attaches the corresponding user object to the request. The request is then allowed to proceed to the view.
Step 1: Project Setup
Let's start building. We'll set up a new Django project. Since you're familiar with Django, we'll move quickly through the boilerplate and focus on the authentication-specific parts.
First, create and activate a virtual environment, then install the necessary packages:
# Create a virtual environment
python -m venv venv
source venv/bin/activate
# Install Django, DRF, and SimpleJWT
pip install django djangorestframework djangorestframework-simplejwt
Now, create the project and an accounts app:
django-admin startproject core .
python manage.py startapp accounts
Creating a Custom User Model
As a best practice, it's always better to start with a custom user model. This gives you the flexibility to add fields like name or date_of_birth later without complex database migrations.
The video below walks through this initial setup, including creating a custom user that uses an email for login instead of a username.
Django API Authentication using JWT Tokens
This video from Scalable Scripts is a practical guide to setting up a Django project for API authentication. We'll follow its guidance for the initial project and custom user model setup.
Watch from the beginning until 08:52. Focus on the following steps, which you should replicate in your own project: Creating the project (auth) and app (users). We'll use core and accounts respectively. Adding rest_framework and your new app to INSTALLED_APPS. Defining the custom User model in accounts/models.py that inherits from AbstractUser. Overriding the USERNAME_FIELD to be 'email'. Setting AUTH_USER_MODEL in core/settings.py. Running makemigrations and migrate.
After following the video's steps, your accounts/models.py should look something like this:
# accounts/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
name = models.CharField(max_length=255)
email = models.CharField(max_length=255, unique=True)
password = models.CharField(max_length=255)
username = None # We don't need a username
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
And your core/settings.py should include:
# core/settings.py
INSTALLED_APPS = [
# ...
'rest_framework',
'accounts',
]
AUTH_USER_MODEL = 'accounts.User'
Don't forget to run python manage.py makemigrations and python manage.py migrate.
Step 2: Configuring Django REST Framework for JWTs
Now we'll configure DRF to use djangorestframework-simplejwt for authentication.
JWT Authentication in Django: REST, WebSockets & Production
This article provides a concise guide to implementing JWTs with simplejwt. We will use it to configure our Django settings and URL endpoints.
Read the subsections 'Django Settings' and 'Token Endpoints'. We'll apply these configurations directly to our project.
Based on the article, make the following additions to your core/settings.py:
# core/settings.py
# ... add this at the bottom
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
)
}
This configuration tells Django REST Framework to use JWTAuthentication as the default method for checking authentication on all API views. This is the "validation middleware" part of our goal.
Next, set up the URL endpoints that simplejwt provides for getting and refreshing tokens. In core/urls.py:
# core/urls.py
from django.contrib import admin
from django.urls import path, include
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
urlpatterns = [
path('admin/', admin.site.urls),
# Add your app's urls
path('api/', include('accounts.urls')),
# Add simplejwt's default URLs
path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]
TokenObtainPairView is a view that takes a user's credentials and returns both an access and a refresh token.
- Access Token: Short-lived (e.g., 5-15 minutes), used to access protected resources.
- Refresh Token: Long-lived (e.g., 7 days), used to get a new access token without forcing the user to log in again.
Step 3: Creating the Registration and Protected Endpoints
With the configuration done, we need two more things:
- An endpoint to register new users.
- A protected endpoint to test our authentication.
User Registration
We'll create a serializer and a view for user registration. The most critical part here is ensuring the user's password is properly hashed before being saved to the database.
Django API Authentication using JWT Tokens
The same Scalable Scripts video provides an excellent walkthrough of creating a registration view. We will follow this pattern closely.
Watch from 08:52 to 19:21. Pay close attention to how the UserSerializer is built and, most importantly, how the create method is overridden to handle password hashing using instance.set_password(password). Also, notice the use of write_only=True on the password field to prevent it from being included in the API response.
First, create accounts/serializers.py:
# accounts/serializers.py
from rest_framework import serializers
from .models import User
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'name', 'email', 'password']
extra_kwargs = {
'password': {'write_only': True}
}
def create(self, validated_data):
password = validated_data.pop('password', None)
instance = self.Meta.model(**validated_data)
if password is not None:
instance.set_password(password)
instance.save()
return instance
Now create the view in accounts/views.py and the URL in accounts/urls.py.
# accounts/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from .serializers import UserSerializer
from .models import User
class RegisterView(APIView):
def post(self, request):
serializer = UserSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
# Create a new file accounts/urls.py
from django.urls import path
from .views import RegisterView
urlpatterns = [
path('register/', RegisterView.as_view()),
]
Protected Endpoint
Finally, let's create a simple view that only authenticated users can access. Add this to accounts/views.py and accounts/urls.py:
# accounts/views.py (add this class)
from rest_framework.permissions import IsAuthenticated
class UserView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request):
serializer = UserSerializer(request.user)
return Response(serializer.data)
# accounts/urls.py (add this path)
from .views import RegisterView, UserView # Update import
urlpatterns = [
path('register/', RegisterView.as_view()),
path('user/', UserView.as_view()), # Add this
]
The permission_classes = [IsAuthenticated] is all that's needed. DRF's IsAuthenticated permission checks if request.user is an authenticated user. This check works seamlessly because our default authentication class (JWTAuthentication) is responsible for populating request.user after validating a token.
Step 4: Testing the Full Flow
Your authentication system is now complete. Let's test it using curl or an API client like Postman.
-
Run the server:
python manage.py runserver -
Register a user:
curl -X POST http://127.0.0.1:8000/api/register/ \ -H "Content-Type: application/json" \ -d '{"name": "test user", "email": "test@example.com", "password": "strongpassword123"}'You should get a
200 OKresponse with the user's data (excluding the password). -
Get a token:
curl -X POST http://127.0.0.1:8000/api/token/ \ -H "Content-Type: application/json" \ -d '{"email": "test@example.com", "password": "strongpassword123"}'The response will be a JSON object containing
accessandrefreshtokens. Copy theaccesstoken. -
Try to access the protected endpoint without a token:
curl http://127.0.0.1:8000/api/user/You'll receive a
401 Unauthorizederror, as expected. -
Access the protected endpoint WITH the token:
Replace<YOUR_ACCESS_TOKEN>with the token you copied.curl http://127.0.0.1:8000/api/user/ \ -H "Authorization: Bearer <YOUR_ACCESS_TOKEN>"Success! You should now get a
200 OKresponse with the authenticated user's details.
Under the Hood: How JWTAuthentication Works
Since you want to go deep, let's look at what JWTAuthentication does when it receives that request.
It's essentially a Python class with an authenticate method. This method:
- Looks for the
Authorizationheader in the incoming request. - If found, it expects the value to be
Bearer <token>. It parses out the<token>part. - It then uses the underlying
PyJWTlibrary to attempt to decode the token.
This article from Auth0 explains how to use the PyJWT library directly. The process it describes for decoding and verifying a token is exactly what simplejwt does internally.
Read the sections 'How to Verify a JWT' and 'Check the Expiration Date on a JWT'. Notice the jwt.decode() function. It takes the token, the secret key (key), and the expected algorithms. PyJWT automatically verifies the signature and checks the exp claim, raising errors like InvalidSignatureError or ExpiredSignatureError if validation fails.
So, JWTAuthentication calls jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"]).
- If the signature is invalid or the token is expired,
PyJWTraises an exception,JWTAuthenticationcatches it, and the authentication fails (resulting in the 401 error). - If decoding is successful,
JWTAuthenticationreads theuser_idfrom the payload. - It performs a database lookup:
User.objects.get(id=user_id). - Finally, it returns a tuple
(user, validated_token), which DRF then uses to setrequest.userandrequest.auth.
Your view's permission class IsAuthenticated can then simply check if request.user.is_authenticated, and the check passes.
Conclusion
Congratulations! You have successfully implemented a robust JWT authentication system in a Django application. You didn't just plug in a library; you set up a best-practice custom user model, handled password hashing correctly, and now understand the exact flow of how a token is generated, sent, and validated.
Key Takeaways:
djangorestframework-simplejwtis the standard library for token-based authentication in DRF.- The authentication flow involves exchanging credentials for an access/refresh token pair. The access token is then sent in the
Authorization: Bearer ...header of every request. - DRF's
DEFAULT_AUTHENTICATION_CLASSESsetting enables theJWTAuthenticationclass to run on every request, acting as the validation middleware. - The
JWTAuthenticationclass validates the token's signature and expiration, then fetches the user from the database and attaches it to therequestobject. - Securing endpoints is as simple as adding
permission_classes = [IsAuthenticated]to your views.
Preview of the Next Lesson
We now have a reliable way to identify the user making a request (request.user). In our next lesson, "Implement RBAC in a Django application using its built-in permissions framework," we will leverage this authenticated user object to build our first authorization layer. We'll explore Django's built-in groups and permissions system to implement a practical Role-Based Access Control model.