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

Top comments (0)