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 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 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_dat

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
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
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
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)
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')),
]
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'),
]
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'
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.
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/.
- Logout with Blacklisting: The token_blacklist app ensures refresh tokens are invalidated on 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