DEV Community

Cover image for Building a Secure JWT Authentication System with Django
kihuni
kihuni

Posted on

Building a Secure JWT Authentication System with Django

I recently implemented a custom authentication system for my Django project, Shopease-API, using JSON Web Tokens (JWT).

The system includes:

  • Email verification at signup to ensure that email addresses are unique and valid.
  • Secure login with access and refresh tokens for seamless user sessions.
  • Token-based authentication using rest_framework_simplejwt for robust security.

In this post, I’ll walk you through the step-by-step process of building this system, from setting up the custom user model to creating the authentication endpoints. Whether you’re a developer diving into the Django REST Framework, this journey offers valuable insights and practical tips!

Why Choose JWT for Shopease-API?

Shopease-API is an e-commerce platform I’m building using Django, designed to handle user authentication, product listings, carts, and orders. I opted for JWT for its stateless nature, scalability, and compatibility with modern APIs. My specific needs included:

  • Email-based authentication: Users sign in with their email and password, eliminating the need for usernames.
  • Email verification: This enforces unique email addresses and validates user input.
  • Secure token management: Access tokens are used for short-term authentication, while refresh tokens allow for session renewal.
  • Logout with token blacklisting: This feature prevents the reuse of tokens.
  • Auto-login after signup: To ensure a frictionless user experience.

I chose rest_framework_simplejwt for its reliable JWT implementation, which includes token blacklisting. The authentication system is integrated within a modular users' Django app, keeping the codebase clean and maintainable.

The Authentication Flow

Here’s how the Shopease-API authentication system works:

  • Signup (/api/auth/register/): Users register with an email and password. The system validates the email for uniqueness, hashes the password, and returns access and refresh tokens for auto-login.
  • Login (/api/auth/login/): Users authenticate with email and password, receiving new access and refresh tokens.
  • Logout (/api/auth/logout/): Users send their refresh token to blacklist it, requiring an access token in the header for authentication.
  • Token Refresh (/api/auth/token/refresh/): Users refresh their access token using the refresh token

authentication flow

Step-by-Step Implementation

Let’s break down the code, organized in the user's app.

Step 1: Custom User Model (users/models.py)

I created a CustomUser model using Django’s AbstractBaseUser to support email-based authentication.

from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager
from django.db import models
from django.utils import timezone

class CustomUserManager(BaseUserManager):
    def create_user(self, email, password=None, **extra_fields):
        if not email:
            raise ValueError("Email must be provided")
        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, email, password=None, **extra_fields):
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)
        return self.create_user(email, password, **extra_fields)

class CustomUser(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(unique=True)
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)
    date_joined = models.DateTimeField(default=timezone.now)

    objects = CustomUserManager()
    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []

    def __str__(self):
        return self.email
Enter fullscreen mode Exit fullscreen mode

Key Features:

  • email is the unique identifier (USERNAME_FIELD).
  • unique=True ensures email uniqueness at the database level.
  • set_password hashes passwords securely.

Step 2: Serializers (users/serializers.py)

Serializers handle data validation, including email verification.

from rest_framework import serializers
from .models import CustomUser
 “

from rest_framework import serializers
from .models import CustomUser
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from django.core.validators import EmailValidator
from django.core.exceptions import ValidationError

class RegisterSerializer(serializers.ModelSerializer):
    password = serializers.CharField(write_only=True, min_length=8)

    class Meta:
        model = CustomUser
        fields = ['email', 'password']

    def validate_email(self, value):
        # Validate email format
        validator = EmailValidator()
        try:
            validator(value)
        except ValidationError:
            raise serializers.ValidationError("Invalid email format")
        # Check email uniqueness
        if CustomUser.objects.filter(email=value).exists():
            raise serializers.ValidationError("Email already exists")
        return value

    def create(self, validated_data):
        return CustomUser.objects.create_user(**validated_data)

class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
    @classmethod
    def get_token(cls, user):
        token = super().get_token(user)
        return token
Enter fullscreen mode Exit fullscreen mode

Key Features:

  • RegisterSerializer validates email format and uniqueness using EmailValidator and a custom check.
  • CustomTokenObtainPairSerializer supports email-based login (leveraging USERNAME_FIELD).

Step 3: Views (users/views.py)

Views define the API endpoints.

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status, generics
from rest_framework.permissions import IsAuthenticated
from rest_framework_simplejwt.views import TokenObtainPairView
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework_simplejwt.exceptions import TokenError
from .models import CustomUser
from .serializers import RegisterSerializer, CustomTokenObtainPairSerializer

class RegisterView(generics.CreateAPIView):
    queryset = CustomUser.objects.all()
    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()
        refresh = RefreshToken.for_user(user)
        return Response({
            "user": {"email": user.email},
            "refresh": str(refresh),
            "access": str(refresh.access_token),
        }, status=status.HTTP_201_CREATED)

class CustomTokenObtainPairView(TokenObtainPairView):
    serializer_class = CustomTokenObtainPairSerializer

class LogoutView(APIView):
    permission_classes = [IsAuthenticated]

    def post(self, request):
        try:
            refresh_token = request.data.get("refresh")
            if not refresh_token:
                return Response({"error": "Refresh token is required"}, status=status.HTTP_400_BAD_REQUEST)
            token = RefreshToken(refresh_token)
            token.blacklist()
            return Response({"message": "Successfully logged out"}, status=status.HTTP_205_RESET_CONTENT)
        except TokenError as e:
            return Response({"error": f"Invalid refresh token: {str(e)}"}, status=status.HTTP_400_BAD_REQUEST)
Enter fullscreen mode Exit fullscreen mode

Key Features:

  • RegisterView: Creates users and returns tokens for auto-login.
  • CustomTokenObtainPairView: Handles secure login with tokens.
  • LogoutView: Blacklists refresh tokens, requiring an access token for authentication.

Step 4: URLs (shopease/urls.py)

Include app URLs in the project's urls.py.

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/auth/', include('users.urls')),
]
Enter fullscreen mode Exit fullscreen mode

Step 5: URLs (users/urls.py)

Define the API endpoints.

from django.urls import path
from .views import RegisterView, CustomTokenObtainPairView, LogoutView
from rest_framework_simplejwt.views import TokenRefreshView

urlpatterns = [
    path('register/', RegisterView.as_view(), name='register'),
    path('login/', CustomTokenObtainPairView.as_view(), name='login'),
    path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
    path('logout/', LogoutView.as_view(), name='logout'),
]
Enter fullscreen mode Exit fullscreen mode

Step 6: Settings (shopease/settings.py)

Configure JWT and the custom user model.

from datetime import timedelta

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'rest_framework_simplejwt',
    'rest_framework_simplejwt.token_blacklist',
    'users',
]

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ),
}

SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
    'ROTATE_REFRESH_TOKENS': True,
    'BLACKLIST_AFTER_ROTATION': True,
    'AUTH_HEADER_TYPES': ('Bearer',),
    'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
}

AUTH_USER_MODEL = 'users.CustomUser'
Enter fullscreen mode Exit fullscreen mode

Key Features in Action

  • Email Verification: The RegisterSerializer uses EmailValidator and checks for existing emails, ensuring only valid, unique emails are accepted.

  • Auto-Login: The RegisterView returns tokens immediately after signup, streamlining the user experience.

registeruser

  • Secure Login: The CustomTokenObtainPairView authenticates users and issues access and refresh tokens, with access tokens expiring in 60 minutes for security.

  • Token-Based Auth: SimpleJWT’s JWTAuthentication validates access tokens for protected endpoints like /logout/.

loginUser

  • Logout with Blacklisting: The token_blacklist app ensures refresh tokens are invalidated on logout.

logout

Lessons Learned

  • Token Distinction: Access tokens authenticate requests, while refresh tokens handle session renewal or blacklisting. Mixing them up causes errors like 401.
  • Debugging: Logging request headers and payloads is essential for troubleshooting auth issues.
  • Validation: Combining model-level (unique=True) and serializer-level email checks ensures robust verification.
  • Testing: Early testing with Postman catches bugs before they impact the system.

That's a wrap! I’m a Django developer with a passion for creating scalable APIs. Let’s connect! GitHub

Heroku

Deploy with ease. Manage efficiently. Scale faster.

Leave the infrastructure headaches to us, while you focus on pushing boundaries, realizing your vision, and making a lasting impression on your users.

Get Started

Top comments (0)

ACI image

ACI.dev: Fully Open-source AI Agent Tool-Use Infra (Composio Alternative)

100% open-source tool-use platform (backend, dev portal, integration library, SDK/MCP) that connects your AI agents to 600+ tools with multi-tenant auth, granular permissions, and access through direct function calling or a unified MCP server.

Check out our GitHub!

👋 Kindness is contagious

Please show some love ❤️ or share a kind word in the comments if you found this useful!

Got it!