📧 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:
- User Model: Add
is_email_verifiedandemail_verification_tokenfields. - Email Utility: Create a utility function to send verification emails.
- Settings: Configure Django email backend.
- Views: Update signup/login views and add verification endpoints.
- URLs: Add routes for email verification.
- 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: falseandemail_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
warningmessage:"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
- Install
python-decouple:pip install python-decouple - Use
config()in yoursettings.pyto securely read credentials from a.envfile.
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')
# ...