Complete Django REST Framework Authentication (JWT + Email)
by @arada • Updated 3 weeks, 3 days ago
Full DRF authentication system with JWT tokens, email verification, password reset, bcrypt hashing, and custom auth backend
Python
# Complete Django REST Framework Authentication
Full-featured authentication system using DRF with JWT tokens, email verification, password reset, and bcrypt password hashing.
## Features
- JWT token-based authentication
- Email/password registration
- Login with email verification
- Forgot/reset password flow
- Email verification with tokens
- Bcrypt password hashing
- Custom JWT authentication backend
## Installation
```bash
pip install djangorestframework PyJWT passlib bcrypt
```
## 1. Settings Configuration
```python
# settings.py
from datetime import timedelta
import environ
env = environ.Env()
INSTALLED_APPS = [
# ...
"rest_framework",
"corsheaders", # If needed for frontend
# Your apps
]
MIDDLEWARE = [
"corsheaders.middleware.CorsMiddleware", # If needed
# ...
]
# REST Framework
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"api.authentication.JWTAuthentication", # Custom JWT auth
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
"DEFAULT_RENDERER_CLASSES": [
"rest_framework.renderers.JSONRenderer",
],
}
# JWT Configuration
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(days=7),
"REFRESH_TOKEN_LIFETIME": timedelta(days=30),
"ALGORITHM": "HS256",
"SIGNING_KEY": env("SECRET_KEY"),
"AUTH_HEADER_TYPES": ("Bearer",),
"USER_ID_FIELD": "id",
"USER_ID_CLAIM": "user_id",
}
# CORS (if using frontend)
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000",
"http://localhost:19006", # Expo
]
CORS_ALLOW_CREDENTIALS = True
```
## 2. User Model
```python
# core/models.py
import uuid
from django.db import models
from django.utils import timezone
class User(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
email = models.EmailField(unique=True)
password_hash = models.CharField(max_length=255)
display_name = models.CharField(max_length=255)
is_active = models.BooleanField(default=True)
# Email verification
email_verified = models.BooleanField(default=False)
email_verification_token = models.CharField(max_length=255, null=True, blank=True)
email_verification_sent_at = models.DateTimeField(null=True, blank=True)
# Password reset
password_reset_token = models.CharField(max_length=255, null=True, blank=True)
password_reset_expires = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = "users"
```
## 3. Auth Utilities
```python
# api/auth_utils.py
import jwt
from datetime import datetime, timedelta
from typing import Optional, Dict
from passlib.hash import bcrypt as passlib_bcrypt
from django.conf import settings
def create_access_token(data: Dict[str, str], expires_delta: Optional[timedelta] = None) -> str:
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(days=7)
to_encode.update({
"exp": expire,
"iat": datetime.utcnow(),
})
if "sub" in to_encode and "user_id" not in to_encode:
to_encode["user_id"] = to_encode["sub"]
return jwt.encode(
to_encode,
settings.SIMPLE_JWT["SIGNING_KEY"],
algorithm=settings.SIMPLE_JWT["ALGORITHM"]
)
def verify_token(token: str) -> Optional[Dict]:
try:
payload = jwt.decode(
token,
settings.SIMPLE_JWT["SIGNING_KEY"],
algorithms=[settings.SIMPLE_JWT["ALGORITHM"]]
)
user_id = payload.get("user_id") or payload.get("sub")
email = payload.get("email")
if user_id is None:
return None
return {"user_id": user_id, "email": email}
except (jwt.ExpiredSignatureError, jwt.InvalidTokenError):
return None
def get_password_hash(password: str) -> str:
return passlib_bcrypt.hash(password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
try:
return passlib_bcrypt.verify(plain_password, hashed_password)
except Exception:
return False
```
## 4. Custom Authentication Backend
```python
# api/authentication.py
from rest_framework import authentication, exceptions
from core.models import User
from .auth_utils import verify_token
class JWTAuthentication(authentication.BaseAuthentication):
keyword = "Bearer"
def authenticate(self, request):
auth_header = request.META.get("HTTP_AUTHORIZATION", "")
if not auth_header:
return None
try:
keyword, token = auth_header.split()
except ValueError:
return None
if keyword != self.keyword:
return None
return self.authenticate_credentials(token)
def authenticate_credentials(self, token):
user_info = verify_token(token)
if not user_info:
raise exceptions.AuthenticationFailed("Invalid or expired token")
try:
user = User.objects.get(id=user_info["user_id"], is_active=True)
except User.DoesNotExist:
raise exceptions.AuthenticationFailed("User not found or inactive")
return (user, token)
```
## 5. Serializers
```python
# api/serializers.py
from rest_framework import serializers
from core.models import User
from passlib.hash import bcrypt as passlib_bcrypt
class RegisterSerializer(serializers.Serializer):
email = serializers.EmailField()
password = serializers.CharField(write_only=True, min_length=8, max_length=72)
display_name = serializers.CharField(required=False, allow_blank=True)
def validate_password(self, value):
password_bytes = value.encode("utf-8")
if len(password_bytes) > 72:
raise serializers.ValidationError("Password too long (max 72 bytes)")
if len(password_bytes) < 8:
raise serializers.ValidationError("Password must be at least 8 characters")
return value
def create(self, validated_data):
password = validated_data.pop("password")
email = validated_data.get("email")
display_name = validated_data.get("display_name") or email.split("@")[0]
user = User.objects.create(
email=email,
display_name=display_name,
password_hash=passlib_bcrypt.hash(password)
)
return user
class LoginSerializer(serializers.Serializer):
email = serializers.EmailField()
password = serializers.CharField(write_only=True, max_length=72)
class AuthResponseSerializer(serializers.Serializer):
access_token = serializers.CharField()
token_type = serializers.CharField(default="bearer")
user_id = serializers.UUIDField()
email = serializers.EmailField()
display_name = serializers.CharField(required=False)
class UserSerializer(serializers.ModelSerializer):
user_id = serializers.UUIDField(source="id", read_only=True)
class Meta:
model = User
fields = ["user_id", "email", "display_name", "created_at"]
read_only_fields = ["user_id", "created_at"]
```
## 6. Views
```python
# api/views.py
import secrets
from datetime import timedelta
from django.utils import timezone
from rest_framework import status
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import AllowAny, IsAuthenticated
from core.models import User
from .serializers import RegisterSerializer, LoginSerializer, AuthResponseSerializer, UserSerializer
from .auth_utils import create_access_token, verify_password, get_password_hash
class RegisterView(APIView):
permission_classes = [AllowAny]
def post(self, request):
serializer = RegisterSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
# Check if user exists
if User.objects.filter(email=serializer.validated_data["email"]).exists():
return Response(
{"detail": "Email already registered"},
status=status.HTTP_400_BAD_REQUEST
)
# Create user
user = serializer.save()
# Generate verification token
token = secrets.token_urlsafe(32)
user.email_verification_token = token
user.email_verification_sent_at = timezone.now()
user.save()
# TODO: Send verification email
# email_service.send_verification_email(user.email, token)
# Create JWT access token
access_token = create_access_token(
data={"sub": str(user.id), "email": user.email}
)
return Response(
AuthResponseSerializer({
"access_token": access_token,
"token_type": "bearer",
"user_id": user.id,
"email": user.email,
"display_name": user.display_name,
}).data,
status=status.HTTP_201_CREATED
)
class LoginView(APIView):
permission_classes = [AllowAny]
def post(self, request):
serializer = LoginSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
# Find user
try:
user = User.objects.get(email=serializer.validated_data["email"])
except User.DoesNotExist:
return Response(
{"detail": "Invalid email or password"},
status=status.HTTP_401_UNAUTHORIZED
)
# Check if active
if not user.is_active:
return Response(
{"detail": "User account is disabled"},
status=status.HTTP_403_FORBIDDEN
)
# Verify password
if not verify_password(serializer.validated_data["password"], user.password_hash):
return Response(
{"detail": "Invalid email or password"},
status=status.HTTP_401_UNAUTHORIZED
)
# Create JWT access token
access_token = create_access_token(
data={"sub": str(user.id), "email": user.email}
)
return Response(
AuthResponseSerializer({
"access_token": access_token,
"token_type": "bearer",
"user_id": user.id,
"email": user.email,
"display_name": user.display_name,
}).data
)
class CurrentUserView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request):
return Response(UserSerializer(request.user).data)
class ForgotPasswordView(APIView):
permission_classes = [AllowAny]
def post(self, request):
email = request.data.get("email")
try:
user = User.objects.get(email=email, is_active=True)
# Generate reset token (expires in 1 hour)
token = secrets.token_urlsafe(32)
user.password_reset_token = token
user.password_reset_expires = timezone.now() + timedelta(hours=1)
user.save()
# TODO: Send password reset email
# email_service.send_password_reset_email(user.email, token)
except User.DoesNotExist:
pass # Don't reveal if user exists
return Response({
"message": "If an account exists, a password reset link has been sent."
})
class ResetPasswordView(APIView):
permission_classes = [AllowAny]
def post(self, request):
token = request.data.get("token")
password = request.data.get("password")
try:
user = User.objects.get(
password_reset_token=token,
password_reset_expires__gt=timezone.now()
)
user.password_hash = get_password_hash(password)
user.password_reset_token = None
user.password_reset_expires = None
user.save()
return Response({"message": "Password reset successfully."})
except User.DoesNotExist:
return Response(
{"detail": "Invalid or expired reset token"},
status=status.HTTP_400_BAD_REQUEST
)
class VerifyEmailView(APIView):
permission_classes = [AllowAny]
def get(self, request, token):
try:
user = User.objects.get(email_verification_token=token)
# Check if expired (24 hours)
if user.email_verification_sent_at:
expiry = user.email_verification_sent_at + timedelta(hours=24)
if timezone.now() > expiry:
return Response(
{"detail": "Verification token expired"},
status=status.HTTP_400_BAD_REQUEST
)
user.email_verified = True
user.email_verification_token = None
user.save()
return Response({"message": "Email verified successfully."})
except User.DoesNotExist:
return Response(
{"detail": "Invalid verification token"},
status=status.HTTP_400_BAD_REQUEST
)
```
## 7. URL Configuration
```python
# api/urls.py
from django.urls import path
from . import views
urlpatterns = [
# Auth endpoints
path("auth/register", views.RegisterView.as_view(), name="register"),
path("auth/login", views.LoginView.as_view(), name="login"),
path("auth/me", views.CurrentUserView.as_view(), name="current-user"),
path("auth/forgot-password", views.ForgotPasswordView.as_view(), name="forgot-password"),
path("auth/reset-password", views.ResetPasswordView.as_view(), name="reset-password"),
path("auth/verify-email/<str:token>", views.VerifyEmailView.as_view(), name="verify-email"),
]
```
## 8. Usage Examples
### Register
```bash
curl -X POST http://localhost:8000/api/auth/register \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "password": "securepass123", "display_name": "John Doe"}'
```
### Login
```bash
curl -X POST http://localhost:8000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "password": "securepass123"}'
```
### Get Current User
```bash
curl http://localhost:8000/api/auth/me \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
```
### Forgot Password
```bash
curl -X POST http://localhost:8000/api/auth/forgot-password \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com"}'
```
### Reset Password
```bash
curl -X POST http://localhost:8000/api/auth/reset-password \
-H "Content-Type: application/json" \
-d '{"token": "reset_token_here", "password": "newpassword123"}'
```
## Key Features
1. **JWT Tokens**: 7-day expiry, HS256 algorithm
2. **Bcrypt Hashing**: Secure password storage with passlib
3. **Email Verification**: Token-based with 24-hour expiry
4. **Password Reset**: Token-based with 1-hour expiry
5. **Custom Auth Backend**: Seamless integration with DRF permissions
6. **Security**: No user enumeration, proper error messages
## Security Notes
- Passwords limited to 72 bytes (bcrypt requirement)
- Email verification tokens expire in 24 hours
- Password reset tokens expire in 1 hour
- Forgot password doesn't reveal if email exists
- Inactive users cannot log in
- JWT tokens verified on every request
Created December 21, 2025
• Last updated December 21, 2025
• Public
Discussion 0
Login to join the discussion.
Keep Comments Focused
This snippet is optimized for AI models. Please keep comments constructive and relevant—suggest improvements, report issues, or ask clarifying questions. Excessive off-topic discussion may reduce the snippet's effectiveness for AI parsing.
No comments yet. Be the first to share your thoughts!