Compare Versions
Complete Django REST Framework Authentication (JWT + Email) | Version History
Version 2
December 21, 2025, 1:29 a.m.
arada
Version 3
December 21, 2025, 1:41 a.m.
arada
| Version 2 | Version 3 | ||||
|---|---|---|---|---|---|
| 10 | - Email verification with tokens | 10 | - Email verification with tokens | ||
| 11 | - Bcrypt password hashing | 11 | - Bcrypt password hashing | ||
| 12 | - Custom JWT authentication backend | 12 | - Custom JWT authentication backend | ||
| n | 13 | - Custom exception handler | n | ||
| 14 | 13 | ||||
| 15 | ## Installation | 14 | ## Installation | ||
| 16 | 15 | ||||
| 17 | ```bash | 16 | ```bash | ||
| n | 18 | pip install djangorestframework PyJWT passlib bcrypt django-cors-headers django-environ | n | 17 | pip install djangorestframework PyJWT passlib bcrypt |
| 19 | ``` | 18 | ``` | ||
| 20 | 19 | ||||
| 21 | ## 1. Settings Configuration | 20 | ## 1. Settings Configuration | ||
| 28 | env = environ.Env() | 27 | env = environ.Env() | ||
| 29 | 28 | ||||
| 30 | INSTALLED_APPS = [ | 29 | INSTALLED_APPS = [ | ||
| n | 31 | "django.contrib.admin", | n | 30 | # ... |
| 32 | "django.contrib.auth", | ||||
| 33 | "django.contrib.contenttypes", | ||||
| 34 | "django.contrib.sessions", | ||||
| 35 | "django.contrib.messages", | ||||
| 36 | "django.contrib.staticfiles", | ||||
| 37 | # Third party | ||||
| 38 | "rest_framework", | 31 | "rest_framework", | ||
| n | 39 | "corsheaders", | n | 32 | "corsheaders", # If needed for frontend |
| 40 | # Your apps | 33 | # Your apps | ||
| n | 41 | "core", | n | ||
| 42 | "api", | ||||
| 43 | ] | 34 | ] | ||
| 44 | 35 | ||||
| 45 | MIDDLEWARE = [ | 36 | MIDDLEWARE = [ | ||
| n | 46 | "django.middleware.security.SecurityMiddleware", | n | ||
| 47 | "corsheaders.middleware.CorsMiddleware", # MUST be before CommonMiddleware | 37 | "corsheaders.middleware.CorsMiddleware", # If needed | ||
| 48 | "django.contrib.sessions.middleware.SessionMiddleware", | 38 | # ... | ||
| 49 | "django.middleware.common.CommonMiddleware", | ||||
| 50 | "django.middleware.csrf.CsrfViewMiddleware", | ||||
| 51 | "django.contrib.auth.middleware.AuthenticationMiddleware", | ||||
| 52 | "django.contrib.messages.middleware.MessageMiddleware", | ||||
| 53 | "django.middleware.clickjacking.XFrameOptionsMiddleware", | ||||
| 54 | ] | 39 | ] | ||
| 55 | 40 | ||||
| n | 56 | # REST Framework Configuration | n | 41 | # REST Framework |
| 57 | REST_FRAMEWORK = { | 42 | REST_FRAMEWORK = { | ||
| 58 | "DEFAULT_AUTHENTICATION_CLASSES": [ | 43 | "DEFAULT_AUTHENTICATION_CLASSES": [ | ||
| 59 | "api.authentication.JWTAuthentication", # Custom JWT auth | 44 | "api.authentication.JWTAuthentication", # Custom JWT auth | ||
| 64 | "DEFAULT_RENDERER_CLASSES": [ | 49 | "DEFAULT_RENDERER_CLASSES": [ | ||
| 65 | "rest_framework.renderers.JSONRenderer", | 50 | "rest_framework.renderers.JSONRenderer", | ||
| 66 | ], | 51 | ], | ||
| n | 67 | "DEFAULT_PARSER_CLASSES": [ | n | ||
| 68 | "rest_framework.parsers.JSONParser", | ||||
| 69 | ], | ||||
| 70 | "EXCEPTION_HANDLER": "api.exceptions.custom_exception_handler", | ||||
| 71 | } | 52 | } | ||
| 72 | 53 | ||||
| 73 | # JWT Configuration | 54 | # JWT Configuration | ||
| 74 | SIMPLE_JWT = { | 55 | SIMPLE_JWT = { | ||
| 75 | "ACCESS_TOKEN_LIFETIME": timedelta(days=7), | 56 | "ACCESS_TOKEN_LIFETIME": timedelta(days=7), | ||
| 76 | "REFRESH_TOKEN_LIFETIME": timedelta(days=30), | 57 | "REFRESH_TOKEN_LIFETIME": timedelta(days=30), | ||
| n | 77 | "ROTATE_REFRESH_TOKENS": False, | n | ||
| 78 | "BLACKLIST_AFTER_ROTATION": True, | ||||
| 79 | "ALGORITHM": "HS256", | 58 | "ALGORITHM": "HS256", | ||
| 80 | "SIGNING_KEY": env("SECRET_KEY"), | 59 | "SIGNING_KEY": env("SECRET_KEY"), | ||
| 81 | "AUTH_HEADER_TYPES": ("Bearer",), | 60 | "AUTH_HEADER_TYPES": ("Bearer",), | ||
| 83 | "USER_ID_CLAIM": "user_id", | 62 | "USER_ID_CLAIM": "user_id", | ||
| 84 | } | 63 | } | ||
| 85 | 64 | ||||
| n | 86 | # CORS Configuration | n | 65 | # CORS (if using frontend) |
| 87 | CORS_ALLOWED_ORIGINS = [ | 66 | CORS_ALLOWED_ORIGINS = [ | ||
| n | 88 | "https://yourapp.com", | n | ||
| 89 | "http://localhost:3000", | 67 | "http://localhost:3000", | ||
| n | 90 | "http://localhost:19006", # Expo web | n | 68 | "http://localhost:19006", # Expo |
| 91 | "http://localhost:8081", # Expo dev | ||||
| 92 | ] | 69 | ] | ||
| 93 | CORS_ALLOW_CREDENTIALS = True | 70 | CORS_ALLOW_CREDENTIALS = True | ||
| 94 | ``` | 71 | ``` | ||
| 147 | "iat": datetime.utcnow(), | 124 | "iat": datetime.utcnow(), | ||
| 148 | }) | 125 | }) | ||
| 149 | 126 | ||||
| n | 150 | # Ensure user_id is in token | n | ||
| 151 | if "sub" in to_encode and "user_id" not in to_encode: | 127 | if "sub" in to_encode and "user_id" not in to_encode: | ||
| 152 | to_encode["user_id"] = to_encode["sub"] | 128 | to_encode["user_id"] = to_encode["sub"] | ||
| 153 | 129 | ||||
| 177 | return None | 153 | return None | ||
| 178 | 154 | ||||
| 179 | def get_password_hash(password: str) -> str: | 155 | def get_password_hash(password: str) -> str: | ||
| n | 180 | """Hash password using bcrypt.""" | n | ||
| 181 | return passlib_bcrypt.hash(password) | 156 | return passlib_bcrypt.hash(password) | ||
| 182 | 157 | ||||
| 183 | def verify_password(plain_password: str, hashed_password: str) -> bool: | 158 | def verify_password(plain_password: str, hashed_password: str) -> bool: | ||
| n | 184 | """Verify password against bcrypt hash.""" | n | ||
| 185 | try: | 159 | try: | ||
| 186 | return passlib_bcrypt.verify(plain_password, hashed_password) | 160 | return passlib_bcrypt.verify(plain_password, hashed_password) | ||
| 187 | except Exception: | 161 | except Exception: | ||
| 188 | return False | 162 | return False | ||
| 189 | ``` | 163 | ``` | ||
| 190 | 164 | ||||
| n | 191 | ## 4. Custom Exception Handler | n | ||
| 192 | |||||
| 193 | ```python | ||||
| 194 | # api/exceptions.py | ||||
| 195 | from rest_framework.views import exception_handler | ||||
| 196 | from rest_framework.response import Response | ||||
| 197 | from rest_framework import status | ||||
| 198 | |||||
| 199 | def custom_exception_handler(exc, context): | ||||
| 200 | """ | ||||
| 201 | Custom exception handler for consistent error format. | ||||
| 202 | Returns: {"detail": "error message"} | ||||
| 203 | """ | ||||
| 204 | # Call REST framework's default exception handler first | ||||
| 205 | response = exception_handler(exc, context) | ||||
| 206 | |||||
| 207 | if response is not None: | ||||
| 208 | # Customize error response format | ||||
| 209 | custom_response_data = { | ||||
| 210 | "detail": response.data.get("detail", str(exc)) | ||||
| 211 | } | ||||
| 212 | response.data = custom_response_data | ||||
| 213 | |||||
| 214 | return response | ||||
| 215 | ``` | ||||
| 216 | |||||
| 217 | ## 5. Custom Authentication Backend | 165 | ## 4. Custom Authentication Backend | ||
| 218 | 166 | ||||
| 219 | ```python | 167 | ```python | ||
| 220 | # api/authentication.py | 168 | # api/authentication.py | ||
| 223 | from .auth_utils import verify_token | 171 | from .auth_utils import verify_token | ||
| 224 | 172 | ||||
| 225 | class JWTAuthentication(authentication.BaseAuthentication): | 173 | class JWTAuthentication(authentication.BaseAuthentication): | ||
| n | 226 | """ | n | ||
| 227 | Custom JWT authentication for DRF. | ||||
| 228 | Verifies Bearer tokens from Authorization header. | ||||
| 229 | """ | ||||
| 230 | keyword = "Bearer" | 174 | keyword = "Bearer" | ||
| 231 | 175 | ||||
| 232 | def authenticate(self, request): | 176 | def authenticate(self, request): | ||
| 259 | return (user, token) | 203 | return (user, token) | ||
| 260 | ``` | 204 | ``` | ||
| 261 | 205 | ||||
| n | 262 | ## 6. Serializers | n | 206 | ## 5. Serializers |
| 263 | 207 | ||||
| 264 | ```python | 208 | ```python | ||
| 265 | # api/serializers.py | 209 | # api/serializers.py | ||
| 273 | display_name = serializers.CharField(required=False, allow_blank=True) | 217 | display_name = serializers.CharField(required=False, allow_blank=True) | ||
| 274 | 218 | ||||
| 275 | def validate_password(self, value): | 219 | def validate_password(self, value): | ||
| n | 276 | """Validate password length (bcrypt has 72-byte limit).""" | n | ||
| 277 | password_bytes = value.encode("utf-8") | 220 | password_bytes = value.encode("utf-8") | ||
| 278 | if len(password_bytes) > 72: | 221 | if len(password_bytes) > 72: | ||
| 279 | raise serializers.ValidationError("Password too long (max 72 bytes)") | 222 | raise serializers.ValidationError("Password too long (max 72 bytes)") | ||
| 313 | read_only_fields = ["user_id", "created_at"] | 256 | read_only_fields = ["user_id", "created_at"] | ||
| 314 | ``` | 257 | ``` | ||
| 315 | 258 | ||||
| n | 316 | ## 7. Views | n | 259 | ## 6. Views |
| 317 | 260 | ||||
| 318 | ```python | 261 | ```python | ||
| 319 | # api/views.py | 262 | # api/views.py | ||
| 326 | from rest_framework.permissions import AllowAny, IsAuthenticated | 269 | from rest_framework.permissions import AllowAny, IsAuthenticated | ||
| 327 | 270 | ||||
| 328 | from core.models import User | 271 | from core.models import User | ||
| n | 329 | from .serializers import ( | n | 272 | from .serializers import RegisterSerializer, LoginSerializer, AuthResponseSerializer, UserSerializer |
| 330 | RegisterSerializer, LoginSerializer, | ||||
| 331 | AuthResponseSerializer, UserSerializer | ||||
| 332 | ) | ||||
| 333 | from .auth_utils import create_access_token, verify_password, get_password_hash | 273 | from .auth_utils import create_access_token, verify_password, get_password_hash | ||
| 334 | 274 | ||||
| 335 | class RegisterView(APIView): | 275 | class RegisterView(APIView): | ||
| 504 | ) | 444 | ) | ||
| 505 | ``` | 445 | ``` | ||
| 506 | 446 | ||||
| n | 507 | ## 8. URL Configuration | n | 447 | ## 7. URL Configuration |
| 508 | 448 | ||||
| 509 | ```python | 449 | ```python | ||
| 510 | # api/urls.py | 450 | # api/urls.py | ||
| 522 | ] | 462 | ] | ||
| 523 | ``` | 463 | ``` | ||
| 524 | 464 | ||||
| n | 525 | ## 9. Usage Examples | n | 465 | ## 8. Usage Examples |
| 526 | 466 | ||||
| 527 | ### Register | 467 | ### Register | ||
| 528 | ```bash | 468 | ```bash | ||
| 531 | -d '{"email": "user@example.com", "password": "securepass123", "display_name": "John Doe"}' | 471 | -d '{"email": "user@example.com", "password": "securepass123", "display_name": "John Doe"}' | ||
| 532 | ``` | 472 | ``` | ||
| 533 | 473 | ||||
| n | 534 | **Response:** | n | ||
| 535 | ```json | ||||
| 536 | { | ||||
| 537 | "access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...", | ||||
| 538 | "token_type": "bearer", | ||||
| 539 | "user_id": "123e4567-e89b-12d3-a456-426614174000", | ||||
| 540 | "email": "user@example.com", | ||||
| 541 | "display_name": "John Doe" | ||||
| 542 | } | ||||
| 543 | ``` | ||||
| 544 | |||||
| 545 | ### Login | 474 | ### Login | ||
| 546 | ```bash | 475 | ```bash | ||
| 547 | curl -X POST http://localhost:8000/api/auth/login \ | 476 | curl -X POST http://localhost:8000/api/auth/login \ | ||
| 576 | 3. **Email Verification**: Token-based with 24-hour expiry | 505 | 3. **Email Verification**: Token-based with 24-hour expiry | ||
| 577 | 4. **Password Reset**: Token-based with 1-hour expiry | 506 | 4. **Password Reset**: Token-based with 1-hour expiry | ||
| 578 | 5. **Custom Auth Backend**: Seamless integration with DRF permissions | 507 | 5. **Custom Auth Backend**: Seamless integration with DRF permissions | ||
| n | 579 | 6. **Custom Exception Handler**: Consistent error format across all endpoints | n | ||
| 580 | 7. **CORS Support**: Configured for frontend integration | ||||
| 581 | 8. **Security**: No user enumeration, proper error messages | 508 | 6. **Security**: No user enumeration, proper error messages | ||
| 582 | |||||
| 583 | ## File Structure | ||||
| 584 | |||||
| 585 | ``` | ||||
| 586 | your_project/ | ||||
| 587 | ├── config/ | ||||
| 588 | │ └── settings.py # Settings configuration | ||||
| 589 | ├── core/ | ||||
| 590 | │ └── models.py # User model | ||||
| 591 | └── api/ | ||||
| 592 | ├── auth_utils.py # JWT & password utilities | ||||
| 593 | ├── authentication.py # Custom JWT authentication | ||||
| 594 | ├── exceptions.py # Custom exception handler | ||||
| 595 | ├── serializers.py # DRF serializers | ||||
| 596 | ├── views.py # API views | ||||
| 597 | └── urls.py # URL configuration | ||||
| 598 | ``` | ||||
| 599 | 509 | ||||
| 600 | ## Security Notes | 510 | ## Security Notes | ||
| 601 | 511 | ||||
| 605 | - Forgot password doesn't reveal if email exists | 515 | - Forgot password doesn't reveal if email exists | ||
| 606 | - Inactive users cannot log in | 516 | - Inactive users cannot log in | ||
| 607 | - JWT tokens verified on every request | 517 | - JWT tokens verified on every request | ||
| t | 608 | - CORS properly configured | t | ||
| 609 | - Custom exception handler prevents information leakage | ||||
--- Version 2+++ Version 3@@ -10,12 +10,11 @@ - Email verification with tokens
- Bcrypt password hashing
- Custom JWT authentication backend
-- Custom exception handler
## Installation
```bash
-pip install djangorestframework PyJWT passlib bcrypt django-cors-headers django-environ
+pip install djangorestframework PyJWT passlib bcrypt
```
## 1. Settings Configuration
@@ -28,32 +27,18 @@ env = environ.Env()
INSTALLED_APPS = [
- "django.contrib.admin",
- "django.contrib.auth",
- "django.contrib.contenttypes",
- "django.contrib.sessions",
- "django.contrib.messages",
- "django.contrib.staticfiles",
- # Third party
+ # ...
"rest_framework",
- "corsheaders",
+ "corsheaders", # If needed for frontend
# Your apps
- "core",
- "api",
]
MIDDLEWARE = [
- "django.middleware.security.SecurityMiddleware",
- "corsheaders.middleware.CorsMiddleware", # MUST be before CommonMiddleware
- "django.contrib.sessions.middleware.SessionMiddleware",
- "django.middleware.common.CommonMiddleware",
- "django.middleware.csrf.CsrfViewMiddleware",
- "django.contrib.auth.middleware.AuthenticationMiddleware",
- "django.contrib.messages.middleware.MessageMiddleware",
- "django.middleware.clickjacking.XFrameOptionsMiddleware",
+ "corsheaders.middleware.CorsMiddleware", # If needed
+ # ...
]
-# REST Framework Configuration
+# REST Framework
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"api.authentication.JWTAuthentication", # Custom JWT auth
@@ -64,18 +49,12 @@ "DEFAULT_RENDERER_CLASSES": [
"rest_framework.renderers.JSONRenderer",
],
- "DEFAULT_PARSER_CLASSES": [
- "rest_framework.parsers.JSONParser",
- ],
- "EXCEPTION_HANDLER": "api.exceptions.custom_exception_handler",
}
# JWT Configuration
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(days=7),
"REFRESH_TOKEN_LIFETIME": timedelta(days=30),
- "ROTATE_REFRESH_TOKENS": False,
- "BLACKLIST_AFTER_ROTATION": True,
"ALGORITHM": "HS256",
"SIGNING_KEY": env("SECRET_KEY"),
"AUTH_HEADER_TYPES": ("Bearer",),
@@ -83,12 +62,10 @@ "USER_ID_CLAIM": "user_id",
}
-# CORS Configuration
+# CORS (if using frontend)
CORS_ALLOWED_ORIGINS = [
- "https://yourapp.com",
"http://localhost:3000",
- "http://localhost:19006", # Expo web
- "http://localhost:8081", # Expo dev
+ "http://localhost:19006", # Expo
]
CORS_ALLOW_CREDENTIALS = True
```
@@ -147,7 +124,6 @@ "iat": datetime.utcnow(),
})
- # Ensure user_id is in token
if "sub" in to_encode and "user_id" not in to_encode:
to_encode["user_id"] = to_encode["sub"]
@@ -177,44 +153,16 @@ return None
def get_password_hash(password: str) -> str:
- """Hash password using bcrypt."""
return passlib_bcrypt.hash(password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
- """Verify password against bcrypt hash."""
try:
return passlib_bcrypt.verify(plain_password, hashed_password)
except Exception:
return False
```
-## 4. Custom Exception Handler
-
-```python
-# api/exceptions.py
-from rest_framework.views import exception_handler
-from rest_framework.response import Response
-from rest_framework import status
-
-def custom_exception_handler(exc, context):
- """
- Custom exception handler for consistent error format.
- Returns: {"detail": "error message"}
- """
- # Call REST framework's default exception handler first
- response = exception_handler(exc, context)
-
- if response is not None:
- # Customize error response format
- custom_response_data = {
- "detail": response.data.get("detail", str(exc))
- }
- response.data = custom_response_data
-
- return response
-```
-
-## 5. Custom Authentication Backend
+## 4. Custom Authentication Backend
```python
# api/authentication.py
@@ -223,10 +171,6 @@ from .auth_utils import verify_token
class JWTAuthentication(authentication.BaseAuthentication):
- """
- Custom JWT authentication for DRF.
- Verifies Bearer tokens from Authorization header.
- """
keyword = "Bearer"
def authenticate(self, request):
@@ -259,7 +203,7 @@ return (user, token)
```
-## 6. Serializers
+## 5. Serializers
```python
# api/serializers.py
@@ -273,7 +217,6 @@ display_name = serializers.CharField(required=False, allow_blank=True)
def validate_password(self, value):
- """Validate password length (bcrypt has 72-byte limit)."""
password_bytes = value.encode("utf-8")
if len(password_bytes) > 72:
raise serializers.ValidationError("Password too long (max 72 bytes)")
@@ -313,7 +256,7 @@ read_only_fields = ["user_id", "created_at"]
```
-## 7. Views
+## 6. Views
```python
# api/views.py
@@ -326,10 +269,7 @@ from rest_framework.permissions import AllowAny, IsAuthenticated
from core.models import User
-from .serializers import (
- RegisterSerializer, LoginSerializer,
- AuthResponseSerializer, UserSerializer
-)
+from .serializers import RegisterSerializer, LoginSerializer, AuthResponseSerializer, UserSerializer
from .auth_utils import create_access_token, verify_password, get_password_hash
class RegisterView(APIView):
@@ -504,7 +444,7 @@ )
```
-## 8. URL Configuration
+## 7. URL Configuration
```python
# api/urls.py
@@ -522,7 +462,7 @@ ]
```
-## 9. Usage Examples
+## 8. Usage Examples
### Register
```bash
@@ -531,17 +471,6 @@ -d '{"email": "user@example.com", "password": "securepass123", "display_name": "John Doe"}'
```
-**Response:**
-```json
-{
- "access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
- "token_type": "bearer",
- "user_id": "123e4567-e89b-12d3-a456-426614174000",
- "email": "user@example.com",
- "display_name": "John Doe"
-}
-```
-
### Login
```bash
curl -X POST http://localhost:8000/api/auth/login \
@@ -576,26 +505,7 @@ 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. **Custom Exception Handler**: Consistent error format across all endpoints
-7. **CORS Support**: Configured for frontend integration
-8. **Security**: No user enumeration, proper error messages
-
-## File Structure
-
-```
-your_project/
-├── config/
-│ └── settings.py # Settings configuration
-├── core/
-│ └── models.py # User model
-└── api/
- ├── auth_utils.py # JWT & password utilities
- ├── authentication.py # Custom JWT authentication
- ├── exceptions.py # Custom exception handler
- ├── serializers.py # DRF serializers
- ├── views.py # API views
- └── urls.py # URL configuration
-```
+6. **Security**: No user enumeration, proper error messages
## Security Notes
@@ -604,6 +514,4 @@ - 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
-- CORS properly configured
-- Custom exception handler prevents information leakage+- JWT tokens verified on every request