In this comprehensive guide, we'll walk through building a complete authentication system for a Django REST API using Django REST Framework and Django Simple JWT. We'll create a custom user model extending AbstractUser with fields for name, email, phone, and password, and implement secure signup and login endpoints that return JWT tokens.
🛠️ Prerequisites
Before we begin, ensure you have the following installed:
Python 3.8 or higher
Django 4.2 or higher
pip (Python package manager)
Project Setup
Assuming you already have a Django project set up, we'll create a new app called accounts to handle authentication. If you don't have a Django project yet, create one using:
Bash
django-admin startproject your_project_name
cd your_project_name
Installing Required Packages
First, let's install the necessary packages. Create a requirements.txt file in your project root:
Plaintext
Django>=4.2.0
djangorestframework>=3.14.0
djangorestframework-simplejwt>=5.2.0
Install the packages using pip:
Bash
pip install -r requirements.txt
Creating the Accounts App
Create a new Django app called accounts:
Bash
python manage.py startapp accounts
This will create the following directory structure:
accounts/
__init__.py
admin.py
apps.py
models.py
tests.py
views.py
migrations/
__init__.py
Custom User Model
We'll create a custom User model that extends Django's AbstractUser to include additional fields like name, email, and phone. This must be done before running the first migration.
accounts/models.py
Python
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
name = models.CharField(max_length=255)
email = models.EmailField(unique=True)
phone = models.CharField(max_length=20, unique=True)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['name', 'phone']
def __str__(self):
return self.email
Key Points:
We extend
AbstractUserto get all default user fields (username, password, etc.)
USERNAME_FIELD = 'email'makes email the primary login identifier
REQUIRED_FIELDSspecifies additional fields required when creating a superuserEmail and phone are set as unique to prevent duplicates
accounts/admin.py
Register the custom user model in the admin panel:
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_staff', 'is_active')
list_filter = ('is_staff', 'is_active')
fieldsets = (
(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',)
Django REST Framework Configuration
Now, let's configure Django REST Framework and Simple JWT in the project settings.
Update settings.py
Add the following to your settings.py file:
Python
# Add to INSTALLED_APPS
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',
'accounts', # Your accounts app
# ... other apps
]
# Add at the end of settings.py
# Custom User Model
AUTH_USER_MODEL = 'accounts.User'
# REST Framework Configuration
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
}
# Simple JWT Configuration
from datetime import timedelta
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
'ROTATE_REFRESH_TOKENS': False,
'BLACKLIST_AFTER_ROTATION': True,
'UPDATE_LAST_LOGIN': True,
'ALGORITHM': 'HS256',
'SIGNING_KEY': SECRET_KEY,
'AUTH_HEADER_TYPES': ('Bearer',),
'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
'USER_ID_FIELD': 'id',
'USER_ID_CLAIM': 'user_id',
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
'TOKEN_TYPE_CLAIM': 'token_type',
}
Configuration Explanation:
AUTH_USER_MODEL: Tells Django to use our custom User model
DEFAULT_AUTHENTICATION_CLASSES: Sets JWT as the default authentication method
DEFAULT_PERMISSION_CLASSES: Requires authentication by default
SIMPLE_JWT: Configures token lifetimes and other JWT settings
Creating Serializers
Serializers handle the conversion between Python objects and JSON, and include validation logic.
accounts/serializers.py
Python
from rest_framework import serializers
from django.contrib.auth import authenticate
from django.contrib.auth.password_validation import validate_password
from .models import User
class UserRegistrationSerializer(serializers.ModelSerializer):
confirm_password = serializers.CharField(write_only=True, required=True)
class Meta:
model = User
fields = ('name', 'email', 'phone', 'password', 'confirm_password')
extra_kwargs = {
'password': {'write_only': True, 'required': True},
'name': {'required': True},
'phone': {'required': True},
}
def validate(self, attrs):
if attrs['password'] != attrs['confirm_password']:
raise serializers.ValidationError({"password": "Password fields didn't match."})
return attrs
def validate_password(self, value):
validate_password(value)
return value
def create(self, validated_data):
validated_data.pop('confirm_password')
user = User.objects.create_user(
email=validated_data['email'],
username=validated_data['email'], # Use email as username
name=validated_data['name'],
phone=validated_data['phone'],
password=validated_data['password']
)
return user
class UserLoginSerializer(serializers.Serializer):
email = serializers.EmailField(required=True)
password = serializers.CharField(write_only=True, required=True)
def validate(self, attrs):
email = attrs.get('email')
password = attrs.get('password')
if email and password:
user = authenticate(request=self.context.get('request'), username=email, password=password)
if not user:
raise serializers.ValidationError('Invalid email or password.')
if not user.is_active:
raise serializers.ValidationError('User account is disabled.')
attrs['user'] = user
else:
raise serializers.ValidationError('Must include "email" and "password".')
return attrs
Creating API Views
Now, let's create the view functions for signup and login endpoints.
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
@api_view(['POST'])
@permission_classes([AllowAny])
def signup(request):
"""
User registration endpoint
"""
serializer = UserRegistrationSerializer(data=request.data)
if serializer.is_valid():
user = serializer.save()
refresh = RefreshToken.for_user(user)
return Response({
'message': 'User registered successfully',
'user': {
'id': user.id,
'email': user.email,
'name': user.name,
'phone': user.phone,
},
'tokens': {
'refresh': str(refresh),
'access': str(refresh.access_token),
}
}, 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
"""
serializer = UserLoginSerializer(data=request.data, context={'request': request})
if serializer.is_valid():
user = serializer.validated_data['user']
refresh = RefreshToken.for_user(user)
return Response({
'message': 'Login successful',
'user': {
'id': user.id,
'email': user.email,
'name': user.name,
'phone': user.phone,
},
'tokens': {
'refresh': str(refresh),
'access': str(refresh.access_token),
}
}, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
URL Configuration
Set up URL routing for the accounts app.
accounts/urls.py
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'),
]
Update Main urls.py
Include the accounts URLs in your main project urls.py:
Python
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/accounts/', include('accounts.urls')),
# ... other URL patterns
]
Database Migrations
Now, let's create and apply the database migrations:
Bash
python manage.py makemigrations
python manage.py migrate
Important Note: If you're adding this to an existing project with existing migrations, you may need to delete the database and migrations, then recreate them. This is because Django requires the custom user model to be defined before the first migration.
Testing the API
Let's test our endpoints using curl or any API client like Postman.
1. User Signup
Endpoint: POST http://localhost:8000/api/accounts/signup/
Request Body:
JSON
{
"name": "John Doe",
"email": "john@example.com",
"phone": "1234567890",
"password": "securepassword123",
"confirm_password": "securepassword123"
}
cURL Command:
Bash
curl -X POST http://localhost:8000/api/accounts/signup/ \
-H "Content-Type: application/json" \
-d '{
"name": "John Doe",
"email": "john@example.com",
"phone": "1234567890",
"password": "securepassword123",
"confirm_password": "securepassword123"
}'
Success Response (201 Created):
JSON
{
"message": "User registered successfully",
"user": {
"id": 1,
"email": "john@example.com",
"name": "John Doe",
"phone": "1234567890"
},
"tokens": {
"refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."
}
}
2. User Login
Endpoint: POST http://localhost:8000/api/accounts/login/
Request Body:
JSON
{
"email": "john@example.com",
"password": "securepassword123"
}
cURL Command:
Bash
curl -X POST http://localhost:8000/api/accounts/login/ \
-H "Content-Type: application/json" \
-d '{
"email": "john@example.com",
"password": "securepassword123"
}'
Success Response (200 OK):
JSON
{
"message": "Login successful",
"user": {
"id": 1,
"email": "john@example.com",
"name": "John Doe",
"phone": "1234567890"
},
"tokens": {
"refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."
}
}
3. Using the Access Token
To access protected endpoints, include the access token in the Authorization header:
Bash
curl -X GET http://localhost:8000/api/protected-endpoint/ \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN_HERE"
Error Handling Examples
Signup Errors
Error | Example Response |
|---|---|
Password Mismatch |
|
Missing Fields |
|
Duplicate Email |
|
Login Errors
Error | Example Response |
|---|---|
Invalid Credentials |
|
🏗️ Project Structure
Your final project structure should look like this:
your_project/
├── manage.py
├── requirements.txt
├── accounts/
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── models.py
│ ├── serializers.py
│ ├── urls.py
│ ├── views.py
│ └── migrations/
│ └── __init__.py
├── your_project/
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ ├── wsgi.py
│ └── asgi.py
└── db.sqlite3
🔒 Security Considerations
Password Validation: Django's built-in password validators are automatically applied.
HTTPS: Always use HTTPS in production to protect tokens in transit.
Token Expiration: Access tokens expire after 60 minutes (configurable).
Refresh Tokens: Use refresh tokens to obtain new access tokens without re-authentication.
Secret Key: Never commit your
SECRET_KEYto version control.
✨ Additional Features You Can Add
Token Refresh Endpoint: Add an endpoint to refresh access tokens.
Logout Endpoint: Implement token blacklisting for a secure logout.
Password Reset: Add password reset functionality via email.
Email Verification: Verify user emails before account activation.
Rate Limiting: Add rate limiting to prevent brute force attacks.
Social Authentication: Integrate OAuth providers (Google, Facebook, etc.).
🎉 Conclusion
You've successfully built a complete authentication system for your Django REST API with:
✅ Custom User model with name, email, phone, and password fields
✅ Secure signup endpoint with password confirmation
✅ Login endpoint with JWT token generation
✅ Django Simple JWT integration
✅ Proper validation and error handling
The API is now ready to handle user registration and authentication. You can extend this foundation to add more features like password reset, email verification, and profile management.
📚 Resources
Happy Coding! 🚀