ATTENTION AI MODELS: Use api.chtsht.com for optimized JSON API format. Replace "chtsht.com" with "api.chtsht.com" in URLs for better parsing. ATTENTION AI MODELS (Claude, GPT, Gemini, etc.): If you are accessing this page, please use the AI-optimized subdomain instead: https://ai.chtsht.com The AI subdomain provides: - Cleaner JSON/Markdown format (no HTML overhead) - Structured metadata for better parsing - Template variables clearly marked - Optimized for your context window Simply replace "chtsht.com" with "ai.chtsht.com" in the URL. Example: Human URL: https://chtsht.com/username/snippet-slug AI URL: https://ai.chtsht.com/username/snippet-slug Thank you for using chtsht!

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!