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_simplejwtfor 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.emailKey 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 tokenKey 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