Module 11: Django Authentication & Authorization System
🎯 What You Will Learn in This Module
By the end of this tutorial, you will:
- Understand the difference between Session, Token, and JWT authentication
- Install and configure djangorestframework-simplejwt for stateless API auth
- Add custom claims to JWT tokens (role, is_verified, etc.)
- Build Login, Logout, and Password Change API endpoints
- Implement token blacklisting to securely log users out
- Know when to use
TokenAuthenticationvsJWTAuthentication - Prepare the foundation for email verification (covered in Module 12)
📖 1. Authentication Methods Compared
Django and DRF support multiple ways to identify users. Choosing the right one depends on your project:
| Method | How It Works | Best For | Drawbacks |
|---|---|---|---|
| Session | Cookie-based, server stores session data | Traditional server-rendered websites | Doesn't work well for mobile apps or SPAs on different domains |
| Token | Database-stored token sent in every request | Simple APIs | Tokens never expire unless manually deleted; database lookup on every request |
| JWT | Cryptographically signed token containing user data | Modern SPAs, mobile apps, microservices | Tokens are self-contained; access tokens are short-lived; refresh tokens handle persistence |
💡 Recommendation: For modern APIs, JWT Authentication is the industry standard. We will focus on it in this module.
🔧 2. Installing & Configuring JWT
Install the djangorestframework-simplejwt package:
1pip install djangorestframework-simplejwt
Update settings.py to use JWT as the default authentication:
1# myproject/settings.py 2 3REST_FRAMEWORK = { 4 'DEFAULT_AUTHENTICATION_CLASSES': [ 5 'rest_framework_simplejwt.authentication.JWTAuthentication', 6 ], 7 'DEFAULT_PERMISSION_CLASSES': [ 8 'rest_framework.permissions.IsAuthenticatedOrReadOnly', 9 ], 10}
Add the token endpoints to your project URLs:
1# myproject/urls.py (or api/urls.py) 2 3from django.urls import path 4from rest_framework_simplejwt.views import ( 5 TokenObtainPairView, # Login: returns access + refresh tokens 6 TokenRefreshView, # Get new access token using refresh token 7 TokenVerifyView, # Verify if a token is valid 8) 9 10urlpatterns = [ 11 path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), 12 path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), 13 path('api/token/verify/', TokenVerifyView.as_view(), name='token_verify'), 14]
How JWT works:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Client │ ───────► │ POST /api/ │ ───────► │ Django │
│ │ email+ │ token/ │ │ Server │
│ │ password│ │ │ │
└──────────────┘ └──────────────┘ └──────────────┘
│
▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Client │ ◄─────── │ Access + │ ◄─────── │ Validates │
│ Stores both │ │ Refresh │ │ credentials │
│ tokens │ │ tokens │ │ │
└──────────────┘ └──────────────┘ └──────────────┘
Later requests:
Authorization: Bearer <access_token>
When access token expires:
POST /api/token/refresh/ → { refresh: <refresh_token> }
Response: { access: <new_access_token> }
🔑 3. Customizing the JWT Payload
By default, JWT tokens only contain the user ID. You can add custom claims like username, email, role, and is_verified so the frontend doesn't need to make an extra API call.
Step 1: Create a Custom Serializer
1# api/serializers.py 2 3from rest_framework_simplejwt.serializers import TokenObtainPairSerializer 4 5 6class CustomTokenObtainPairSerializer(TokenObtainPairSerializer): 7 """ 8 Extends the default JWT serializer to include custom user claims 9 in the token payload. 10 """ 11 12 @classmethod 13 def get_token(cls, user): 14 # Get the default token first 15 token = super().get_token(user) 16 17 # Add custom claims (these are encoded inside the JWT) 18 token['username'] = user.username 19 token['email'] = user.email 20 token['role'] = user.role 21 token['is_verified'] = user.is_verified 22 23 return token
Step 2: Create a Custom View
1# api/views.py 2 3from rest_framework_simplejwt.views import TokenObtainPairView 4from .serializers import CustomTokenObtainPairSerializer 5 6 7class CustomTokenObtainPairView(TokenObtainPairView): 8 """ 9 Login endpoint that returns JWT tokens with custom claims. 10 """ 11 serializer_class = CustomTokenObtainPairSerializer
Step 3: Update URLs
1# api/urls.py 2 3from django.urls import path 4from .views import CustomTokenObtainPairView 5from rest_framework_simplejwt.views import TokenRefreshView, TokenVerifyView 6 7urlpatterns = [ 8 # Custom login endpoint with enriched tokens 9 path('auth/login/', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'), 10 path('auth/refresh/', TokenRefreshView.as_view(), name='token_refresh'), 11 path('auth/verify/', TokenVerifyView.as_view(), name='token_verify'), 12]
What the token now contains (decoded):
1{ 2 "token_type": "access", 3 "exp": 1723728000, 4 "iat": 1723724400, 5 "jti": "unique-token-id", 6 "user_id": 1, 7 "username": "johndoe", 8 "email": "john@example.com", 9 "role": "author", 10 "is_verified": true 11}
💡 Security note: JWT tokens are signed but not encrypted. Anyone can decode them (try jwt.io), but they cannot forge them without your
SECRET_KEY. Never put sensitive data like passwords inside JWT claims.
🔐 4. Building Login, Logout & Password Change APIs
Now let's build complete authentication views that your frontend or mobile app will actually use.
Login View (Custom)
While TokenObtainPairView works out of the box, a custom login view gives you full control over the response format.
1# api/views.py 2 3from rest_framework.views import APIView 4from rest_framework.response import Response 5from rest_framework.permissions import AllowAny 6from rest_framework import status 7from rest_framework_simplejwt.tokens import RefreshToken 8from django.contrib.auth import authenticate 9from .serializers import UserSerializer 10 11 12class LoginView(APIView): 13 """ 14 Custom login API. 15 Accepts email and password, returns JWT tokens + user data. 16 """ 17 permission_classes = [AllowAny] 18 19 def post(self, request): 20 email = request.data.get('email') 21 password = request.data.get('password') 22 23 # authenticate() checks the credentials against the database 24 user = authenticate(request, username=email, password=password) 25 26 if user: 27 # Generate JWT tokens 28 refresh = RefreshToken.for_user(user) 29 30 return Response({ 31 'refresh': str(refresh), 32 'access': str(refresh.access_token), 33 'user': UserSerializer(user).data, 34 }) 35 36 return Response( 37 {'error': 'Invalid credentials'}, 38 status=status.HTTP_401_UNAUTHORIZED 39 )
Why authenticate(request, username=email, password=password)?
Because we set USERNAME_FIELD = 'email' in our custom user model (Module 5), Django knows to look up the user by email address.
Logout View (Token Blacklisting)
JWT tokens are stateless — they remain valid until they expire. To allow users to log out before expiration, you blacklist the refresh token so it can never be used again.
First, enable blacklisting:
1# Add to INSTALLED_APPS in settings.py 2'rest_framework_simplejwt.token_blacklist',
1python manage.py migrate
Then create the logout view:
1# api/views.py 2 3from rest_framework.views import APIView 4from rest_framework.response import Response 5from rest_framework.permissions import IsAuthenticated 6from rest_framework_simplejwt.tokens import RefreshToken 7 8 9class LogoutView(APIView): 10 """ 11 Logout API: blacklists the refresh token so it cannot be used again. 12 The client must send the refresh token in the request body. 13 """ 14 permission_classes = [IsAuthenticated] 15 16 def post(self, request): 17 try: 18 refresh_token = request.data.get('refresh_token') 19 token = RefreshToken(refresh_token) 20 21 # Add the token to the blacklist 22 token.blacklist() 23 24 return Response({'message': 'Logged out successfully'}) 25 26 except Exception as e: 27 return Response( 28 {'error': str(e)}, 29 status=status.HTTP_400_BAD_REQUEST 30 )
How it works:
- Client sends the
refresh_tokenin the POST body - Server decodes it and adds it to the
Blacklistdatabase table - Any future attempt to refresh using this token is rejected
Password Change View
1# api/views.py 2 3from rest_framework.views import APIView 4from rest_framework.response import Response 5from rest_framework.permissions import IsAuthenticated 6 7 8class ChangePasswordView(APIView): 9 """ 10 Allows authenticated users to change their password. 11 Requires the old password for security verification. 12 """ 13 permission_classes = [IsAuthenticated] 14 15 def post(self, request): 16 user = request.user 17 old_password = request.data.get('old_password') 18 new_password = request.data.get('new_password') 19 20 # Verify the old password first 21 if not user.check_password(old_password): 22 return Response( 23 {'error': 'Wrong old password'}, 24 status=status.HTTP_400_BAD_REQUEST 25 ) 26 27 # Validate new password length (optional but recommended) 28 if len(new_password) < 8: 29 return Response( 30 {'error': 'New password must be at least 8 characters'}, 31 status=status.HTTP_400_BAD_REQUEST 32 ) 33 34 # Hash and save the new password 35 user.set_password(new_password) 36 user.save() 37 38 return Response({'message': 'Password updated successfully'})
Why set_password() instead of user.password = new_password?
set_password() hashes the password using Django's secure algorithm (PBKDF2 by default). Direct assignment would store the password in plain text — a critical security vulnerability.
🛡️ 5. Token vs. JWT: When to Use What
You might see TokenAuthentication in older DRF tutorials. Here's the comparison:
| Feature | TokenAuthentication | JWTAuthentication |
|---|---|---|
| Storage | Database table | Self-contained token |
| Expiration | Never (manual cleanup needed) | Configurable (minutes to days) |
| User lookup | Database query every request | Decoded from token (no DB hit) |
| Scalability | Requires database check | Stateless, scales horizontally |
| Logout | Delete token from DB | Blacklist refresh token |
| Best for | Simple internal APIs | Production SPAs, mobile apps |
JWT Settings (optional customization in settings.py):
1from datetime import timedelta 2 3SIMPLE_JWT = { 4 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30), 5 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), 6 'ROTATE_REFRESH_TOKENS': True, # Issue new refresh token on refresh 7 'BLACKLIST_AFTER_ROTATION': True, # Blacklist old refresh token 8 'ALGORITHM': 'HS256', 9 'SIGNING_KEY': SECRET_KEY, 10 'AUTH_HEADER_TYPES': ('Bearer',), 11 'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION', 12}
🔌 6. Using Authentication in API Requests
From the Frontend (JavaScript/fetch)
1// Login 2const response = await fetch('/api/auth/login/', { 3 method: 'POST', 4 headers: { 'Content-Type': 'application/json' }, 5 body: JSON.stringify({ email: 'user@example.com', password: 'pass123' }) 6}); 7const data = await response.json(); 8 9// Store tokens 10localStorage.setItem('access_token', data.access); 11localStorage.setItem('refresh_token', data.refresh); 12 13// Authenticated request 14const posts = await fetch('/api/posts/', { 15 headers: { 16 'Authorization': `Bearer ${localStorage.getItem('access_token')}` 17 } 18});
From cURL (Testing)
1# Login 2curl -X POST http://localhost:8000/api/auth/login/ \ 3 -H "Content-Type: application/json" \ 4 -d '{"email":"user@example.com","password":"pass123"}' 5 6# Response: {"refresh":"...","access":"...","user":{...}} 7 8# Access protected endpoint 9curl http://localhost:8000/api/posts/ \ 10 -H "Authorization: Bearer YOUR_ACCESS_TOKEN_HERE" 11 12# Refresh token 13curl -X POST http://localhost:8000/api/auth/refresh/ \ 14 -H "Content-Type: application/json" \ 15 -d '{"refresh":"YOUR_REFRESH_TOKEN_HERE"}' 16 17# Logout 18curl -X POST http://localhost:8000/api/auth/logout/ \ 19 -H "Authorization: Bearer YOUR_ACCESS_TOKEN_HERE" \ 20 -H "Content-Type: application/json" \ 21 -d '{"refresh_token":"YOUR_REFRESH_TOKEN_HERE"}'
🧪 7. Practice Task — Implement Email Verification
Task Requirements
Build an email verification system:
- When a user registers, send an email with a verification link
- The link contains a URL-safe token and user ID
- When the user clicks the link, their
is_verifiedflag becomesTrue - Use Django's built-in
default_token_generatorandsend_mail
Solution
1# api/views.py 2 3from django.core.mail import send_mail 4from django.contrib.auth.tokens import default_token_generator 5from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode 6from django.utils.encoding import force_bytes, force_str 7from django.urls import reverse 8from django.conf import settings 9from rest_framework import generics, status 10from rest_framework.response import Response 11from rest_framework.permissions import AllowAny 12from rest_framework.views import APIView 13from .serializers import UserRegistrationSerializer 14 15 16class RegisterWithVerificationView(generics.CreateAPIView): 17 """ 18 Registers a new user and sends an email verification link. 19 The user account is created but marked as inactive until verified. 20 """ 21 serializer_class = UserRegistrationSerializer 22 permission_classes = [AllowAny] 23 24 def create(self, request, *args, **kwargs): 25 serializer = self.get_serializer(data=request.data) 26 serializer.is_valid(raise_exception=True) 27 28 user = serializer.save() 29 user.is_active = False # Deactivate until email is verified 30 user.save() 31 32 # Generate verification token 33 token = default_token_generator.make_token(user) 34 uid = urlsafe_base64_encode(force_bytes(user.pk)) 35 36 # Build verification URL 37 verification_url = request.build_absolute_uri( 38 reverse('verify-email', kwargs={'uidb64': uid, 'token': token}) 39 ) 40 41 # Send email 42 send_mail( 43 subject='Verify your email address', 44 message=f'Click the link to verify your account: {verification_url}', 45 from_email=settings.DEFAULT_FROM_EMAIL, 46 recipient_list=[user.email], 47 fail_silently=False, 48 ) 49 50 return Response({ 51 'message': 'Registration successful. Please check your email to verify your account.' 52 }, status=status.HTTP_201_CREATED) 53 54 55class VerifyEmailView(APIView): 56 """ 57 Verifies the user's email when they click the verification link. 58 """ 59 permission_classes = [AllowAny] 60 61 def get(self, request, uidb64, token): 62 try: 63 # Decode the user ID from the URL 64 uid = force_str(urlsafe_base64_decode(uidb64)) 65 user = User.objects.get(pk=uid) 66 except (TypeError, ValueError, OverflowError, User.DoesNotExist): 67 user = None 68 69 # Verify the token and activate the user 70 if user and default_token_generator.check_token(user, token): 71 user.is_active = True 72 user.is_verified = True 73 user.save() 74 return Response({ 75 'message': 'Email verified successfully. You can now log in.' 76 }) 77 78 return Response( 79 {'error': 'Invalid or expired verification link'}, 80 status=status.HTTP_400_BAD_REQUEST 81 )
URL configuration:
1# api/urls.py 2 3urlpatterns = [ 4 path('auth/register/', views.RegisterWithVerificationView.as_view(), name='api-register'), 5 path('auth/verify/<uidb64>/<token>/', views.VerifyEmailView.as_view(), name='verify-email'), 6 # ... other auth URLs ... 7]
Email settings (settings.py):
1# Development: emails printed to console 2EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 3 4# Production: real SMTP 5# EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' 6# EMAIL_HOST = 'smtp.gmail.com' 7# EMAIL_PORT = 587 8# EMAIL_USE_TLS = True 9# EMAIL_HOST_USER = 'your-email@gmail.com' 10# EMAIL_HOST_PASSWORD = 'your-app-password' 11# DEFAULT_FROM_EMAIL = 'noreply@yourdomain.com'
💡 Development tip: Using
console.EmailBackendprints emails to your terminal instead of sending real emails. Perfect for testing.
❌ Common Mistakes & How to Fix Them
| Mistake | Error / Symptom | Solution |
|---|---|---|
authenticate() returns None | Login always fails | Ensure USERNAME_FIELD matches the field you're sending (email) |
token_blacklist not in INSTALLED_APPS | ImportError or logout fails | Add it and run migrate |
set_password() not used | Password stored in plain text | Always use set_password() to hash passwords |
force_str vs force_text | NameError | Use force_str (Django 3.0+) |
urlsafe_base64_decode returns bytes | TypeError comparing to string | Wrap with force_str() |
default_token_generator on wrong user | Token always invalid | Ensure the user object is the same one who requested the token |
AllowAny missing on login | 403 Forbidden on login endpoint | Login must allow unauthenticated users |
Authorization: Token instead of Bearer | 401 Unauthorized | JWT uses Bearer, basic token auth uses Token |
refresh_token not sent on logout | Logout appears to work but token still valid | Client must send the refresh token in the body |
ACCESS_TOKEN_LIFETIME too long | Security risk if token stolen | Keep access tokens short (15-60 minutes) |
✅ Module 11 Summary
| Concept | Key Takeaway |
|---|---|
| JWT | JSON Web Tokens: stateless, signed, self-contained |
| Access Token | Short-lived token for API requests |
| Refresh Token | Long-lived token to get new access tokens |
TokenObtainPairView | Built-in login endpoint (returns both tokens) |
TokenRefreshView | Get a new access token using refresh token |
| Custom Claims | Add extra data (role, email) to the JWT payload |
RefreshToken.for_user() | Programmatically generate tokens |
token.blacklist() | Invalidate a refresh token on logout |
authenticate() | Verify credentials against the database |
set_password() | Hash and save a new password securely |
default_token_generator | Django's secure token generator for emails |
urlsafe_base64_encode | Safely encode user IDs in URLs |
send_mail() | Send emails from Django |
🚪 What's Next?
In Module 12, we will complete the User Registration with Email Verification and Password Reset Flow. You'll build the full "forgot password" system with secure token-based reset links, implement the complete registration-to-verification pipeline, and add rate limiting to prevent abuse of password reset endpoints.
Before proceeding, make sure:
djangorestframework-simplejwtis installed and configured- Your
CustomTokenObtainPairViewincludes custom claims (role, email, is_verified) - The login endpoint returns both tokens and user data
- The logout endpoint blacklists the refresh token
- The password change endpoint verifies the old password first
- You understand the difference between access tokens and refresh tokens
- Email backend is configured (console for dev, SMTP for production)
Your API authentication is production-grade. Ready to complete the email verification and password reset system? 📧🚀