Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 61

JWT Authentication in Django

Recommended resources

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

Anatomy of a JSON Web Token (JWT)
This image breaks down a JWT into its three parts: the **Header** (metadata like the algorithm), the **Payload** (the claims, such as user ID and expiration), and the **Signature** (which verifies the token hasn't been tampered with).

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:

JWT Authentication Flow Diagram
This diagram shows the complete authentication cycle. The client logs in, receives a JWT, and includes it in the header of subsequent requests. The server validates this token to grant access to protected resources.
  1. Login: The client sends credentials (e.g., email and password) to a login endpoint.
  2. 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.
  3. 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).
  4. Authenticated Requests: For every subsequent request to a protected API endpoint, the client includes the JWT in the Authorization header, typically using the Bearer scheme:
    Authorization: Bearer <your_jwt_token>
  5. 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:

  1. An endpoint to register new users.
  2. 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.

  1. Run the server: python manage.py runserver

  2. 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 OK response with the user's data (excluding the password).

  3. 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 access and refresh tokens. Copy the access token.

  4. Try to access the protected endpoint without a token:

    curl http://127.0.0.1:8000/api/user/
    

    You'll receive a 401 Unauthorized error, as expected.

  5. 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 OK response 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:

  1. Looks for the Authorization header in the incoming request.
  2. If found, it expects the value to be Bearer <token>. It parses out the <token> part.
  3. It then uses the underlying PyJWT library to attempt to decode the token.

How to Handle JWTs in Python

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, PyJWT raises an exception, JWTAuthentication catches it, and the authentication fails (resulting in the 401 error).
  • If decoding is successful, JWTAuthentication reads the user_id from 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 set request.user and request.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-simplejwt is 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_CLASSES setting enables the JWTAuthentication class to run on every request, acting as the validation middleware.
  • The JWTAuthentication class validates the token's signature and expiration, then fetches the user from the database and attaches it to the request object.
  • 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.