Django Auth with Simple JWT - Email Verification

Contents

📧 Building a Django REST API with JWT Authentication - Part 2: Adding Email Verification

Introduction

Welcome to Part 2 of our Django REST API authentication series! In Part 1, we built a complete authentication system with signup and login endpoints using Django Simple JWT. Now, we'll enhance that system by adding email verification functionality.

Email verification is a crucial security feature that ensures users register with valid email addresses and helps prevent spam accounts. In this guide, we'll implement:

  • Email verification tokens
  • Automatic verification email sending upon signup
  • Email verification endpoint
  • Resend verification email functionality
  • Email verification status tracking

🛠️ Prerequisites

Before starting, make sure you have:

  • Completed Part 1 of this series.
  • A working Django REST API with authentication endpoints.
  • Basic understanding of Django models, views, and URLs.

Overview of Changes

We'll be making the following changes to add email verification:

  1. User Model: Add is_email_verified and email_verification_token fields.
  2. Email Utility: Create a utility function to send verification emails.
  3. Settings: Configure Django email backend.
  4. Views: Update signup/login views and add verification endpoints.
  5. URLs: Add routes for email verification.
  6. Admin: Update admin interface to show verification status.

1. Updating the User Model

We need to add fields to track email verification status in our User model and a method to generate the unique token.

accounts/models.py

Update your existing User model to include email verification fields:

Python

 

from django.contrib.auth.models import AbstractUser
from django.db import models
import uuid


class User(AbstractUser):
    name = models.CharField(max_length=255)
    email = models.EmailField(unique=True)
    phone = models.CharField(max_length=20, unique=True)
    is_email_verified = models.BooleanField(default=False)
    email_verification_token = models.CharField(max_length=100, blank=True, null=True)
    
    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['name', 'phone']
    
    def __str__(self):
        return self.email
    
    def generate_email_verification_token(self):
        """Generate a unique token for email verification"""
        token = str(uuid.uuid4())
        self.email_verification_token = token
        self.save()
        return token

Why UUID? UUIDs (Universally Unique Identifiers) are perfect for verification tokens because they are unique, unpredictable, and stateless, making them hard to guess.

2. Creating Email Utility Functions

Create a new utility file to handle the email sending process.

accounts/utils.py

Create this new file in your accounts app:

Python

 

from django.core.mail import send_mail
from django.conf import settings
from django.urls import reverse


def send_verification_email(user, request):
    """
    Send email verification link to the user
    """
    token = user.generate_email_verification_token()
    
    # Build the verification URL
    verification_url = request.build_absolute_uri(
        reverse('accounts:verify-email', kwargs={'token': token})
    )
    
    subject = 'Verify Your Email Address'
    message = f'''
    Hello {user.name},
    
    Thank you for registering! Please verify your email address by clicking the link below:
    
    {verification_url}
    
    If you didn't create an account, please ignore this email.
    
    Best regards,
    Your App Team
    '''
    
    try:
        send_mail(
            subject=subject,
            message=message,
            from_email=settings.DEFAULT_FROM_EMAIL,
            recipient_list=[user.email],
            fail_silently=False,
        )
        return True
    except Exception as e:
        print(f"Error sending email: {e}")
        return False

3. Configuring Email Settings

Configure Django to handle sending emails by adding these settings to your settings.py file.

Update settings.py

Add email configuration after your STATIC_URL setting:

Python

 

STATIC_URL = '/static/'

# --- Email Configuration ---
# Use console backend for testing:
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 

# For production/real sending, use SMTP:
# EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
# EMAIL_HOST = 'smtp.gmail.com'  # Change to your email provider
# EMAIL_PORT = 587
# EMAIL_USE_TLS = True
# EMAIL_HOST_USER = 'your-email@gmail.com'
# EMAIL_HOST_PASSWORD = 'your-app-password' 
# DEFAULT_FROM_EMAIL = 'your-email@gmail.com'

💡 Tip: For Gmail, you must Enable 2-Step Verification and Generate an App Password to use for EMAIL_HOST_PASSWORD, as standard passwords won't work with SMTP access.

4. Updating Views for Email Verification

Update the existing signup and login views, and add two new views: verify_email and resend_verification_email.

accounts/views.py

Python

 

from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework_simplejwt.tokens import RefreshToken
from .serializers import UserRegistrationSerializer, UserLoginSerializer
from .utils import send_verification_email
from .models import User


@api_view(['POST'])
@permission_classes([AllowAny])
def signup(request):
    """User registration endpoint with email verification"""
    serializer = UserRegistrationSerializer(data=request.data)
    if serializer.is_valid():
        user = serializer.save()
        
        # Send verification email after successful signup
        email_sent = send_verification_email(user, request)
        
        refresh = RefreshToken.for_user(user)
        return Response({
            'message': 'User registered successfully. Please check your email to verify your account.',
            # ... other response data ...
            'user': {
                'id': user.id,
                'email': user.email,
                'name': user.name,
                'phone': user.phone,
                'is_email_verified': user.is_email_verified,
            },
            'tokens': {
                'refresh': str(refresh),
                'access': str(refresh.access_token),
            },
            'email_sent': email_sent,
        }, status=status.HTTP_201_CREATED)
    return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


@api_view(['POST'])
@permission_classes([AllowAny])
def login(request):
    """User login endpoint with email verification status"""
    serializer = UserLoginSerializer(data=request.data, context={'request': request})
    if serializer.is_valid():
        user = serializer.validated_data['user']
        refresh = RefreshToken.for_user(user)
        
        response_data = {
            'message': 'Login successful',
            # ... other response data ...
            'user': {
                'id': user.id,
                'email': user.email,
                'name': user.name,
                'phone': user.phone,
                'is_email_verified': user.is_email_verified,
            },
            'tokens': {
                'refresh': str(refresh),
                'access': str(refresh.access_token),
            }
        }
        
        # Add warning if email is not verified
        if not user.is_email_verified:
            response_data['warning'] = 'Please verify your email address to access all features.'
        
        return Response(response_data, status=status.HTTP_200_OK)
    return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


@api_view(['GET'])
@permission_classes([AllowAny])
def verify_email(request, token):
    """Email verification endpoint"""
    try:
        user = User.objects.get(email_verification_token=token)
        
        if user.is_email_verified:
            return Response({
                'message': 'Email already verified.'
            }, status=status.HTTP_200_OK)
        
        # Verify the email
        user.is_email_verified = True
        user.email_verification_token = None  # Clear the token after verification
        user.save()
        
        return Response({
            'message': 'Email verified successfully! You can now access all features.'
        }, status=status.HTTP_200_OK)
    except User.DoesNotExist:
        return Response({
            'error': 'Invalid or expired verification token.'
        }, status=status.HTTP_400_BAD_REQUEST)


@api_view(['POST'])
@permission_classes([AllowAny])
def resend_verification_email(request):
    """Resend verification email endpoint"""
    email = request.data.get('email')
    
    if not email:
        return Response({
            'error': 'Email is required.'
        }, status=status.HTTP_400_BAD_REQUEST)
    
    try:
        user = User.objects.get(email=email)
        
        if user.is_email_verified:
            return Response({
                'message': 'Email is already verified.'
            }, status=status.HTTP_200_OK)
        
        # Send new verification email
        email_sent = send_verification_email(user, request)
        
        if email_sent:
            return Response({
                'message': 'Verification email sent successfully. Please check your inbox.'
            }, status=status.HTTP_200_OK)
        else:
            return Response({
                'error': 'Failed to send verification email. Please try again later.'
            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
    except User.DoesNotExist:
        return Response({
            'error': 'User with this email does not exist.'
        }, status=status.HTTP_404_NOT_FOUND)

5. Adding New URL Routes

Add routes for the new email verification endpoints in your accounts app.

accounts/urls.py

Update your URL configuration:

Python

 

from django.urls import path
from . import views

app_name = 'accounts'

urlpatterns = [
    path('signup/', views.signup, name='signup'),
    path('login/', views.login, name='login'),
    path('verify-email/<str:token>/', views.verify_email, name='verify-email'),
    path('resend-verification-email/', views.resend_verification_email, name='resend-verification-email'),
]

6. Updating Admin Interface

Update the admin interface to show email verification status.

accounts/admin.py

Python

 

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from .models import User


@admin.register(User)
class UserAdmin(BaseUserAdmin):
    list_display = ('email', 'name', 'phone', 'is_email_verified', 'is_staff', 'is_active')
    list_filter = ('is_staff', 'is_active', 'is_email_verified')
    fieldsets = (
        # ... rest of the fieldsets remain the same ...
        (None, {'fields': ('email', 'password')}),
        ('Personal info', {'fields': ('name', 'phone', 'username')}),
        ('Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}),
        ('Important dates', {'fields': ('last_login', 'date_joined')}),
    )
    add_fieldsets = (
        (None, {
            'classes': ('wide',),
            'fields': ('email', 'name', 'phone', 'password1', 'password2'),
        }),
    )
    search_fields = ('email', 'name', 'phone')
    ordering = ('email',)

7. Creating and Running Migrations

Since we've added new fields to the User model, we must update the database schema.

Step 1: Create Migrations

Bash

 

python manage.py makemigrations accounts

Step 2: Apply Migrations

Bash

 

python manage.py migrate

8. Testing Email Verification

You can now test all the new functionality using cURL or Postman.

1. Test Signup (Sends Verification Email)

Endpoint: POST http://localhost:8000/api/accounts/signup/

Request Body:

JSON

 

{
"name": "Jane Doe",
"email": "jane@example.com",
"phone": "9876543210",
"password": "securepass123",
"confirm_password": "securepass123"
}

Result: The response will show is_email_verified: false and email_sent: true. If using the console backend, the full email and verification link will be printed in your terminal.

2. Test Login (Shows Warning)

Endpoint: POST http://localhost:8000/api/accounts/login/

Request Body:

JSON

 

{
"email": "jane@example.com",
"password": "securepass123"
}

Result: The response will include a warning message: "Please verify your email address to access all features."

3. Test Email Verification

Endpoint: GET http://localhost:8000/api/accounts/verify-email/<token>/

Action: Copy the unique token from the signup email/console and paste it into the URL.

Success Response: {"message": "Email verified successfully! You can now access all features."}

4. Test Resend Verification Email

Endpoint: POST http://localhost:8000/api/accounts/resend-verification-email/

Request Body:

JSON

 

{
"email": "jane@example.com"
}

Result: If the email is unverified, you'll receive a new email and the response: {"message": "Verification email sent successfully. Please check your inbox."}

9. Email Configuration for Production

For production, never hardcode credentials. Use environment variables and a dedicated Email Service Provider (ESP).

Using Environment Variables

  1. Install python-decouple: pip install python-decouple
  2. Use config() in your settings.py to securely read credentials from a .env file.

Python

 

# settings.py
from decouple import config

EMAIL_HOST = config('EMAIL_HOST')
EMAIL_HOST_USER = config('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD')
# ...