Lesson 3: Adding Authentication with Djoser and SimpleJWT
Adding Authentication with Djoser and SimpleJWT (Updated)
This is an updated version that includes the required configuration you must add (Djoser URLs + DRF/JWT settings), plus advanced options: email-as-username, custom user model, email verification, and proper logout using token blacklisting.
✅ What You’ll Build
- Token-based auth (access + refresh) with SimpleJWT
- Plug-and-play endpoints from Djoser
- Optional custom User model that uses email as the login field
- Email verification (activation links) and password resets
- Token blacklisting for secure logout
0) Install Dependencies
pip install djangorestframework djoser djangorestframework-simplejwt
# For email previews in dev (optional):
pip install django-allauth # not required, but useful if you later integrate social auth
1) Required Settings (Must Add)
settings.py:
INSTALLED_APPS = [
# Django core
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# Third-party
"rest_framework",
"djoser",
"rest_framework_simplejwt",
"rest_framework_simplejwt.token_blacklist", # for logout/blacklist
]
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework_simplejwt.authentication.JWTAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
}
from datetime import timedelta
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=60),
"REFRESH_TOKEN_LIFETIME": timedelta(days=7),
"ROTATE_REFRESH_TOKENS": True, # optional: rotate refresh on use
"BLACKLIST_AFTER_ROTATION": True, # required if you want secure logout
"AUTH_HEADER_TYPES": ("Bearer",),
}
urls.py (project root):
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
# Djoser (base & JWT endpoints)
path("api/auth/", include("djoser.urls")),
path("api/auth/", include("djoser.urls.jwt")),
]
Now you have:
POST /api/auth/jwt/create/(login)POST /api/auth/jwt/refresh/POST /api/auth/jwt/verify/POST /api/auth/users/(register)GET /api/auth/users/me/- plus password reset / activation endpoints (configure email below)
2) (Recommended) Use Email as Username with a Custom User Model
Create an app for users:
python manage.py startapp users
users/models.py:
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
username = models.CharField(max_length=150, unique=False, blank=True)
email = models.EmailField(unique=True)
USERNAME_FIELD = "email"
REQUIRED_FIELDS = [] # no username
def __str__(self):
return self.email
settings.py:
INSTALLED_APPS += ["users"]
AUTH_USER_MODEL = "users.User"
Important: create this custom user model before your first migration in a new project.
If migrating an existing project, follow Django’s official guide for swapping user models.
Run migrations:
python manage.py makemigrations
python manage.py migrate
3) Configure Djoser for Email Login + Optional Features
settings.py:
DJOSER = {
# Tell Djoser to use email for login
"LOGIN_FIELD": "email",
# What fields are exposed/required
"USER_ID_FIELD": "id",
"USER_CREATE_PASSWORD_RETYPE": True, # ask to type password twice on registration
"SEND_ACTIVATION_EMAIL": True, # enable activation flow
"SEND_CONFIRMATION_EMAIL": True, # email on successful password change
# Password reset / activation URLs your frontend will handle:
"PASSWORD_RESET_CONFIRM_URL": "reset-password/{uid}/{token}",
"ACTIVATION_URL": "activate/{uid}/{token}",
# Serializers (use Djoser's defaults or plug your own)
"SERIALIZERS": {
"user_create": "djoser.serializers.UserCreateSerializer",
"user": "djoser.serializers.UserSerializer",
"current_user": "djoser.serializers.UserSerializer",
},
}
Dev email backend to preview links in the console:
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
DEFAULT_FROM_EMAIL = "noreply@example.com"
In production, switch to SMTP or a provider (e.g., SES, Postmark, SendGrid).
4) Token Blacklist (Secure Logout)
Since we enabled rest_framework_simplejwt.token_blacklist, add routes to blacklist refresh tokens on logout.
You can either use Djoser’s built-in /jwt/logout/ (DRF view) or create a tiny custom endpoint:
auth/views.py (optional custom):
from rest_framework import status, permissions
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework_simplejwt.tokens import RefreshToken, TokenError
class LogoutView(APIView):
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
refresh = request.data.get("refresh")
if not refresh:
return Response({"detail": "Refresh token required."}, status=400)
try:
token = RefreshToken(refresh)
token.blacklist()
except TokenError:
return Response({"detail": "Invalid token."}, status=400)
return Response(status=status.HTTP_205_RESET_CONTENT)
urls.py:
from django.urls import path, include
from auth.views import LogoutView # adjust path to your app
urlpatterns = [
path("admin/", admin.site.urls),
path("api/auth/", include("djoser.urls")),
path("api/auth/", include("djoser.urls.jwt")),
path("api/auth/jwt/logout/", LogoutView.as_view()), # optional custom
]
Client flow:
- On logout, send
POST /api/auth/jwt/logout/with the refresh token to blacklist it.
5) End-to-End Flow (Examples)
Register
POST /api/auth/users/
{
"email": "dev@example.com",
"password": "SuperStrongPassword123",
"re_password": "SuperStrongPassword123"
}
If SEND_ACTIVATION_EMAIL=True, user must activate via emailed link:
GET /api/auth/users/activation/?uid=...&token=...
(or your frontend consumes ACTIVATION_URL and calls the confirm endpoint)
Login
POST /api/auth/jwt/create/
{
"email": "dev@example.com",
"password": "SuperStrongPassword123"
}
Response:
{
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Include on requests:
Authorization: Bearer <access_token>
Refresh
POST /api/auth/jwt/refresh/
{
"refresh": "<refresh_token>"
}
Logout (Blacklist)
POST /api/auth/jwt/logout/
{
"refresh": "<refresh_token>"
}
6) Make Views Public or Private
Global default is authenticated. Make a view public as needed:
from rest_framework.permissions import AllowAny
from rest_framework.viewsets import ModelViewSet
from .models import Quote
from .serializers import QuoteSerializer
class QuoteViewSet(ModelViewSet):
queryset = Quote.objects.all().order_by("-created_at")
serializer_class = QuoteSerializer
permission_classes = [AllowAny] # public
7) Fly.io Secrets for Production
Set secrets before deploy:
flyctl secrets set DJANGO_SECRET_KEY='supersecret'
flyctl secrets set DEBUG='False'
flyctl secrets set DEFAULT_FROM_EMAIL='noreply@example.com'
# SMTP creds if you use a real email backend in prod:
flyctl secrets set EMAIL_HOST='smtp.sendgrid.net'
flyctl secrets set EMAIL_HOST_USER='apikey'
flyctl secrets set EMAIL_HOST_PASSWORD='your_sendgrid_api_key'
flyctl secrets set EMAIL_PORT='587'
flyctl secrets set EMAIL_USE_TLS='True'
Then:
flyctl deploy
Troubleshooting
- 401 Unauthorized: Missing
Authorization: Bearer <token>header or expired token. - Activation not working: Check console email output, confirm
DJOSERURLs and email backend. - Custom user errors: Ensure
AUTH_USER_MODELis set before first migration on new projects. - Logout not invalidating: Confirm
token_blacklistapp is installed and blacklist flags enabled inSIMPLE_JWT.
Conclusion
You now have a production-grade authentication layer:
- Djoser for endpoints and flows
- SimpleJWT for modern tokens
- Email-as-username with a custom user model
- Email verification + password resets
- Secure logout with token blacklisting
It’s clean, repeatable, and works with any frontend (Vue, Flutter, React) — perfect for indie SaaS and client projects.
Written by Bailey Burnsed — Senior Software Engineer, Founder of BaileyBurnsed.dev