Email Authentication In Django: Designing a Modern System Rest Framework Without the Traditional Username
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'sModelSerializer
.
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 theUserProfile
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 fromListCreateAPIView
. 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 extendsTokenObtainPairSerializer
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 inREST_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 thedjangorestframework-simplejwt
package, and it's designed to authenticate requests using JSON Web Tokens (JWT).
With this configurations:
- JWT Authentication is Enabled: Any endpoint that requires authentication in your Django Rest Framework project will use JWT as the default authentication mechanism.
- 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!