Django Auth with Simple JWT

Contents

 

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 AbstractUser to get all default user fields (username, password, etc.)

  • USERNAME_FIELD = 'email' makes email the primary login identifier

  • REQUIRED_FIELDS specifies additional fields required when creating a superuser

  • Email 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

{"password": ["Password fields didn't match."]}

Missing Fields

{"email": ["This field is required."], "name": ["This field is required."]}

Duplicate Email

{"email": ["user with this email already exists."]}

Login Errors

Error

Example Response

Invalid Credentials

{"non_field_errors": ["Invalid email or password."]}

🏗️ 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

  1. Password Validation: Django's built-in password validators are automatically applied.

  2. HTTPS: Always use HTTPS in production to protect tokens in transit.

  3. Token Expiration: Access tokens expire after 60 minutes (configurable).

  4. Refresh Tokens: Use refresh tokens to obtain new access tokens without re-authentication.

  5. Secret Key: Never commit your SECRET_KEY to 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! 🚀