Email Authentication In Django: Designing a Modern System Rest Framework Without the Traditional Username

Kevin Kim
Django Unleashed
8 min readFeb 10, 2024

--

Picture this: You are developing an API using the Django Rest Framework, you find yourself scuffling with the conventional username-based authentication system. While typically speaking there is nothing wrong with using a username, they tend to have limitations especially as the system grows and security concerns heighten.

What if there was a more intuitive, user-friendly and secure way to handle authentication? What if, instead of relying on persistent usernames, you could harness the familiarity of email addresses ?

In this article, we’ll embark on a journey to revolutionise user authentication in Django Rest Framework. We’ll explore the advantages of shifting away from usernames and delve into the practical steps of creating a custom user table that centers around emails. By the end, you’ll not only understand the intricacies of this modern approach but also be equipped to implement it seamlessly in your own projects.

A brief summary of the points we are going to touch are:

  • Setting Up a DRF Project
  • Creating a Custom User Model in DRF
  • Configuring DRF Authentication
  • Token-Based Authentication with Email
  • Creating API Endpoints
  • Conclusion

Creating Your Django Application

Open up a terminal and navigate to the directory you want to create this project on:

mkdir django-test-email-auth

Navigate to Project Directory:

cd django-test-email-auth

Creating a Virtual Environment

MacOS and Ubuntu:

Installing a Virtual Environment

pip install virtualenv

Creating a Virtual Environment

python -m venv venv

Replace venv with your preferred virtual environment name.

Activate the Virtual Environment

source venv/bin/activate

Windows:

Activate the Virtual Environment

Installation and creation commands on Windows are alike, except for activation:

.\venv\Scripts\activate

Install Dependencies:

pip install djangorestframework djangorestframework-simplejwt django

Creating a Django Project:

Run the following command to create a new Django project. Replace “projectname” with the desired name for your project:

django-admin startproject projectname

Navigate to Project Directory:

Move into the newly created project directory using the cd command:

cd projectname

Explore the Project Structure:

The startproject command creates a directory with the specified project name. Inside, you'll find various files and folders that make up the initial structure of your Django project.

Run the Development Server (Optional):

To see if your project is set up correctly, run the following command to start the development server:

python manage.py runserver

Visit http://127.0.0.1:8000/ in your browser to view the Django welcome page.

Creating a Django App:

Execute the subsequent command to establish a new Django app, which will serve as our primary focus.

python manage.py startapp users

Integrating Installed Dependencies and Newly Created App into settings.py

Within your project directory, access the file named settings.py and locate the section pertaining to INSTALLED_APPS.

INSTALLED_APPS = [
'django.contrib.admin',
'users',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework_simplejwt'
'rest_framework',
]

Above is a direct representation of how it should look like.

Incorporating an API Directory within the Users App.

By adding an API directory, it’s beneficial since we are clearly stating that the contained files are specifically related to API functionalities. This makes it easier for developers (including yourself and collaborators) to understand the purpose of each component.

Add the following files inside the API folder, i will explain the purpose of each of them as we continue to delve deeper into the article.

serializers.py
views.py
urls.py

Your Project structure should look like this:

projectname/                  # Root directory for the Django project
├── projectname/ # Project-level settings and configurations
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── users/ # Django app directory for user-related functionalities
│ ├── api/ # Directory dedicated to API-related files
│ │ ├── __init__.py
│ │ ├── views.py # API views
│ │ ├── serializers.py # Serializers for API data
│ │ ├── urls.py # URL patterns for the API
│ │ └── ... # Additional API-related files
│ ├── migrations/ # Database migration files
│ ├── __init__.py
│ ├── admin.py # Django admin configurations
│ ├── apps.py # App configuration
│ ├── models.py # Database models
│ ├── tests.py # Unit tests
│ └── views.py # Views for HTML pages (if applicable)
├── venv/ # Virtual environment directory (created if using venv)
├── manage.py # Django management script
└── db.sqlite3 # SQLite database file (or other database files)

Creating a Custom User Model in DRF

In your models.py , add the following lines of code:

from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
from django.db import models
class CustomUserManager(BaseUserManager):
def create_user(self, email, password=None, **extra_fields):
if not email:
raise ValueError('The Email field must be set')
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)

Creating a Custom User Model:

class CustomUser(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(unique=True)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
company_name = models.CharField(null=False, max_length=20)
department_code = models.CharField(max_length=10, null=False)

objects = CustomUserManager()

USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []

def __str__(self):
return self.email

User Model Configuration in Settings:

In your project’s settings.py, update the AUTH_USER_MODEL to point to your custom user model.

AUTH_USER_MODEL = 'users.CustomUser'

Define serializers.py class:

from rest_framework.serializers import ModelSerializer, Serializer
from ..models import CustomUser
  • Imports the necessary classes for creating a Django Rest Framework (DRF) serializer and the UserProfile model.

Class Definition:

class UserModelSerializer(ModelSerializer):
  • Defines a serializer class named UserModelSerializer that inherits from DRF's ModelSerializer.

Meta Class:

class Meta:
model = CustomUser
fields = ['email', 'password', 'phone', 'company_name', 'job_title', 'office_situated']
extra_kwargs = {'password': {'write_only': True, 'min_length': 5}}
  • The Meta class provides metadata for the serializer.
  • model: Specifies the model that the serializer is based on (UserProfile).
  • fields: Lists the fields from the model to be included in the serialized data.
  • extra_kwargs: Specifies extra options for fields, such as making the password field write-only and setting a minimum length of 5 characters.

create Method:

def create(self, validated_data):
user = CustomUser.objects.create_user(**validated_data)
return user
  • Overrides the create method to handle user creation based on the validated data.
  • It calls the create_user method on the UserProfile model (assuming it has a custom manager for user creation) with the provided validated data.

to_representation Method:

def to_representation(self, instance):
"""Overriding to remove Password Field when returning Data"""
ret = super().to_representation(instance)
ret.pop('password', None)
return ret
  • Overrides the to_representation method to customize how the serializer represents instances when converting to JSON.
  • It removes the ‘password’ field from the representation to ensure that sensitive information is not exposed when returning data.

Overall, this serializer is designed for user registration. Handling fields such as email and password.

Defining a Generic View for Creation of a user

In this instance, we are going to use the ListCreateAPIView , this will handle both Create and List operations.

from rest_framework.generics import ListCreateAPIView
from rest_framework.permissions import IsAuthenticated

from .models import CustomUser
from .serializers import UserModelSerializer

class UserProfileListCreateView(ListCreateAPIView):
"""Generic View for Listing and Creating User Profiles"""

queryset = CustomUser.objects.all()
serializer_class = UserModelSerializer
permission_classes = [AllowAny]

def create(self, validated_data):
user = Customser.objects.create_user(**validated_data)
return user

The code above does the following:

  • Define a class named UserProfileListCreateView that inherits from ListCreateAPIView.
  • queryset: Specifies the queryset for the view, in this case, all user profiles.
  • serializer_class: Specifies the serializer to use for serialization and deserialization.
  • permission_classes: Specifies the permissions required for accessing this view. Here, it's set to allow any users. Adjust this according to your authentication and authorization requirements.
  • This create method is where you handle the creation of the user

User Authentication with Email

In this article, we will opt for Simple JWT as our authentication method in Django and specifically in the Django Rest Framework. Why choose Simple JWT? Simply put, it offers a straightforward implementation and utilizes tokens for authentication.

from django.contrib.auth import authenticate
from rest_framework import exceptions
from rest_framework.generics import ListCreateAPIView
from rest_framework_simplejwt.views import TokenObtainPairView
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer


class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
username_field = 'email'

def validate(self, attrs):
credentials = {
'email': attrs.get('email'),
'password': attrs.get('password')
}

user = authenticate(**credentials)

if user:
if not user.is_active:
raise exceptions.AuthenticationFailed('User is deactivated')

data = {}
refresh = self.get_token(user)

data['refresh'] = str(refresh)
data['access'] = str(refresh.access_token)

return data
else:
raise exceptions.AuthenticationFailed('No active account found with the given credentials')


class CustomTokenObtainPairView(TokenObtainPairView):
serializer_class = CustomTokenObtainPairSerializer

Summary of what the code does:

  • The CustomTokenObtainPairSerializer This is a custom serializer class that extends TokenObtainPairSerializer from SimpleJWT.
  • username_field: Specifies that the email field should be used as the username field for authentication.
  • validate method: Overrides the default validation method to customize the authentication process.
  • It retrieves the email and password from the input attrs.
  • Calls the authenticate function with the provided credentials, attempting to authenticate the user.
  • If authentication is successful, it checks if the user is active. If not, it raises an AuthenticationFailed exception.
  • If the user is active, it generates a pair of tokens (refresh and access) using self.get_token(user).
  • Returns a dictionary containing the refresh and access tokens as strings.

The CustomTokenObtainPairView class:

This is a custom view class that extends TokenObtainPairView from SimpleJWT.

  • serializer_class: Specifies that the custom serializer (CustomTokenObtainPairSerializer) should be used for token generation.

Define the URL in api/urls.py:

from django.urls import path
from .views import UserListAPiCreateView
from .views import CustomTokenObtainPairView

urlpatterns = [
path('register/', UserListAPiCreateView.as_view(), name='register'),
path('token/', CustomTokenObtainPairView.as_view(), name='token_obtain_pair')
]

Specify the URL path in the main project’s urls.py configuration file.

from django.contrib import admin
from django.views.generic import TemplateView
from django.urls import path, include


urlpatterns = [
path('admin/', admin.site.urls),
path('api/users/', include('users.api.urls')),
]

This configuration includes routes for rendering a template at the root URL (''), accessing the Django admin interface at '/admin/', and including API-related URL patterns for the 'users' app under the '/api/users/' path.

Applying default configurations for RestFramework.

In your settings.py add the following configurations:

REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
)
}
  • DEFAULT_AUTHENTICATION_CLASSES: This setting in REST_FRAMEWORK specifies the default authentication classes that DRF should use for authenticating incoming requests.
  • 'rest_framework_simplejwt.authentication.JWTAuthentication': This particular authentication class is from the djangorestframework-simplejwt package, and it's designed to authenticate requests using JSON Web Tokens (JWT).

With this configurations:

  1. JWT Authentication is Enabled: Any endpoint that requires authentication in your Django Rest Framework project will use JWT as the default authentication mechanism.
  2. JSON Web Tokens (JWT): When a user successfully logs in or authenticates, a JWT token will be issued. This token can be included in subsequent requests to authenticate the user without the need for sending credentials (like username and password) with every request.

Summary: this configuration ensures that JWT authentication is the default method used for authenticating requests in your Django Rest Framework project.

Conclusion

Creating a custom user table with email authentication serves as a foundational step. Depending on your project’s structure and objectives, there’s a vast array of additional features to explore. Fortunately, upcoming articles will delve into topics such as admin features, custom permissions, and more. Stay tuned for the next articles. Until then, happy coding!

--

--

Kevin Kim
Django Unleashed

Creating awesome stuff and sharing it here! Passionate about Tech, AI and anything that includes Growth.