JWT USING DJANGO REST FRAMEWORK - SIMPLEJWT
JWT with djangorestframework-simplejwt
What changes compared with the PyJWT version
| Before (PyJWT) | Now (simplejwt) |
|---|---|
jwt_utils.py: you wrote create/decode by hand | Deleted. simplejwt builds and verifies tokens |
authentication.py: you parsed the header, handled expiry and errors | A short subclass that only says how to find your User |
| One access token | Access token and refresh token, with a ready-made expiry system |
| Manual expiry setting | SIMPLE_JWT settings dict |
Roles, permissions, serializers and ViewSets stay almost the same.
Why a subclass is needed: simplejwt assumes Django’s user model (AUTH_USER_MODEL). Your User is a separate table (UserId, Username, Password, RoleId), and your project already has migrations and an admin superuser, so switching AUTH_USER_MODEL now would break migrations. The subclass keeps your model and still uses simplejwt for everything else.
Step 0: Install
pip install djangorestframework-simplejwtYou can remove PyJWT usage. simplejwt installs its own dependency.
Step 1: Models (models.py, unchanged)
What: Role and User. Why: each user has one role, and passwords are stored only as hashes.
from django.contrib.auth.hashers import make_password, check_password
class Role(models.Model):
RoleId = models.AutoField(primary_key=True)
RoleName = models.CharField(max_length=30, unique=True) # Admin, Manager, Employee
def __str__(self):
return self.RoleName
class User(models.Model):
UserId = models.AutoField(primary_key=True)
Username = models.CharField(max_length=50, unique=True)
Password = models.CharField(max_length=255) # stores the HASH
RoleId = models.ForeignKey(Role, on_delete=models.PROTECT)
def set_password(self, raw_password):
self.Password = make_password(raw_password)
def check_password(self, raw_password):
return check_password(raw_password, self.Password)
# DRF checks request.user.is_authenticated, so our custom model must provide it
@property
def is_authenticated(self):
return True
def __str__(self):
return self.Usernamepython manage.py makemigrations
python manage.py migrateIf you already did this earlier, skip it.
Step 2: Create roles and the first admin (shell)
from employees.models import Role, User
admin_role = Role.objects.create(RoleName='Admin')
Role.objects.create(RoleName='Manager')
Role.objects.create(RoleName='Employee')
u = User(Username='admin', RoleId=admin_role)
u.set_password('Admin@123') # hashes the password
u.save()Step 3: Settings (settings.py)
What: Tell simplejwt how long tokens live and which field identifies your user.
Why: USER_ID_FIELD defaults to id, but your primary key is UserId. Without this, token creation fails with an AttributeError.
from datetime import timedelta
REST_FRAMEWORK = {
# Every request is checked by our subclass (Step 4)
'DEFAULT_AUTHENTICATION_CLASSES': [
'employees.authentication.CustomJWTAuthentication',
],
# Secure by default: login is required unless an endpoint opts out
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
}
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60), # short-lived: used on every request
'REFRESH_TOKEN_LIFETIME': timedelta(days=1), # long-lived: only used to get a new access token
'USER_ID_FIELD': 'UserId', # your primary key column name
'USER_ID_CLAIM': 'user_id', # name stored inside the token
'AUTH_HEADER_TYPES': ('Bearer',), # Authorization: Bearer <token>
}simplejwt signs tokens with your SECRET_KEY by default. You don’t need to add it to INSTALLED_APPS.
Step 4: Authentication class (employees/authentication.py)
What: Override one method, get_user.
Why: simplejwt already reads the header, checks the signature, and checks expiry. The only part it can’t know is where your users live.
from rest_framework_simplejwt.authentication import JWTAuthentication
from rest_framework_simplejwt.exceptions import InvalidToken, AuthenticationFailed
from rest_framework_simplejwt.settings import api_settings
from .models import User
class CustomJWTAuthentication(JWTAuthentication):
# Called after the token is verified. Must return the user object,
# which DRF puts in request.user.
def get_user(self, validated_token):
try:
user_id = validated_token[api_settings.USER_ID_CLAIM]
except KeyError:
raise InvalidToken('Token has no user identification.')
try:
# select_related loads the role in the same query
# (a deleted user or changed role takes effect immediately)
return User.objects.select_related('RoleId').get(pk=user_id)
except User.DoesNotExist:
raise AuthenticationFailed('User not found.', code='user_not_found')Compare with the 40-line PyJWT version. Header parsing, 401 responses, and expired/invalid token messages are all inherited.
Step 5: Role-based permissions (employees/permissions.py)
What: Permission classes that check the user’s role name.
Why: simplejwt handles authentication (who are you). Roles are your own rule, so authorization stays custom.
from rest_framework.permissions import BasePermission, IsAuthenticated
def role_required(*allowed_roles):
"""Builds a permission class that allows only the given role names."""
class RolePermission(BasePermission):
message = f'Access denied. Required role: {", ".join(allowed_roles)}.'
def has_permission(self, request, view):
user = request.user
return bool(
user
and user.is_authenticated
and user.RoleId.RoleName in allowed_roles
)
return RolePermission
# Any logged-in user can READ; only listed roles can WRITE
class RoleBasedWriteMixin:
write_roles = ('Admin',) # override in each ViewSet
def get_permissions(self):
if self.action in ('list', 'retrieve'):
return [IsAuthenticated()]
return [role_required(*self.write_roles)()]Step 6: Serializers (serializers.py)
from .models import User
class LoginSerializer(serializers.Serializer):
Username = serializers.CharField()
Password = serializers.CharField(write_only=True) # accepted, never returned
class RegisterSerializer(serializers.ModelSerializer):
Password = serializers.CharField(write_only=True, min_length=6)
class Meta:
model = User
fields = ['UserId', 'Username', 'Password', 'RoleId']
def create(self, validated_data):
raw_password = validated_data.pop('Password')
user = User(**validated_data)
user.set_password(raw_password) # hash, never save the raw value
user.save()
return userStep 7: Views (views.py)
Imports:
from rest_framework import viewsets, status, filters
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import AllowAny
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework_simplejwt.exceptions import TokenError, InvalidToken
from rest_framework_simplejwt.settings import api_settings
from .models import Department, Employee, Project, User
from .serializers import (
DepartmentSerializer, EmployeeSerializer, ProjectSerializer,
LoginSerializer, RegisterSerializer,
)
from .permissions import role_required, RoleBasedWriteMixinLogin (public)
What: Check the password, then let simplejwt create the tokens.
Why: RefreshToken.for_user(user) reads user.UserId (from USER_ID_FIELD) and builds a signed token. Claims you add to the refresh token are copied into the access token, so the client can read the role without an extra call.
class LoginView(APIView):
authentication_classes = [] # nobody has a token yet
permission_classes = [AllowAny] # public endpoint
def post(self, request):
serializer = LoginSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
username = serializer.validated_data['Username']
password = serializer.validated_data['Password']
try:
user = User.objects.select_related('RoleId').get(Username=username)
except User.DoesNotExist:
user = None
# Same message for "no such user" and "wrong password",
# so attackers can't discover which usernames exist.
if user is None or not user.check_password(password):
return Response({'detail': 'Invalid username or password.'},
status=status.HTTP_401_UNAUTHORIZED)
refresh = RefreshToken.for_user(user) # creates the token pair
# Custom claims (readable by anyone, so never put secrets here)
refresh['username'] = user.Username
refresh['role_id'] = user.RoleId.RoleId
refresh['role_name'] = user.RoleId.RoleName
return Response({
'roleid': user.RoleId.RoleId,
'username': user.Username,
'token': str(refresh.access_token), # send this with every request
'refresh': str(refresh), # keep this to get a new access token
})Refresh (public)
What: Exchange a refresh token for a new access token.
Why: The access token expires quickly (60 minutes). This lets the user stay logged in without typing the password again.
simplejwt’s built-in TokenRefreshView looks the user up in Django’s user table, which doesn’t contain your users. So this small view replaces it.
class RefreshView(APIView):
authentication_classes = []
permission_classes = [AllowAny]
def post(self, request):
try:
refresh = RefreshToken(request.data.get('refresh')) # verifies signature and expiry
except TokenError as e:
raise InvalidToken(e.args[0])
# Make sure the user still exists
if not User.objects.filter(pk=refresh[api_settings.USER_ID_CLAIM]).exists():
return Response({'detail': 'User not found.'},
status=status.HTTP_401_UNAUTHORIZED)
return Response({'token': str(refresh.access_token)})Register (Admin only)
class RegisterView(APIView):
# Without this, anyone could create an Admin account
permission_classes = [role_required('Admin')]
def post(self, request):
serializer = RegisterSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)Protect your ViewSets
class DepartmentViewSet(RoleBasedWriteMixin, viewsets.ModelViewSet):
write_roles = ('Admin',)
queryset = Department.objects.all()
serializer_class = DepartmentSerializer
class EmployeeViewSet(RoleBasedWriteMixin, viewsets.ModelViewSet):
write_roles = ('Admin', 'Manager')
queryset = Employee.objects.select_related('DepartmentId').prefetch_related('projects')
serializer_class = EmployeeSerializer
filter_backends = [filters.SearchFilter]
search_fields = ['Contact', 'Designation']
class ProjectViewSet(RoleBasedWriteMixin, viewsets.ModelViewSet):
write_roles = ('Admin', 'Manager')
queryset = Project.objects.prefetch_related('Employees')
serializer_class = ProjectSerializerStep 8: URLs (employees/urls.py)
from .views import (DepartmentViewSet, EmployeeViewSet, ProjectViewSet,
LoginView, RefreshView, RegisterView)
urlpatterns = [
path('login/', LoginView.as_view(), name='login'),
path('token/refresh/', RefreshView.as_view(), name='token_refresh'),
path('register/', RegisterView.as_view(), name='register'),
path('', include(router.urls)), # keep the router include last
]Step 9: Test in Postman
1. Login: POST http://127.0.0.1:8000/api/login/
{"Username": "admin", "Password": "Admin@123"}Response:
{
"roleid": 1,
"username": "admin",
"token": "eyJhbGciOiJIUzI1NiIs...",
"refresh": "eyJhbGciOiJIUzI1NiIs..."
}2. Call a protected API: Authorization tab, type Bearer Token, paste token.
3. Refresh: POST /api/token/refresh/ with {"refresh": "<refresh value>"} returns a new token.
| Request | Result |
|---|---|
GET /api/employees/ without a token | 401 |
GET /api/employees/ with an expired or bad token | 401 with a message |
GET /api/employees/?search=dev as Employee role | 200 |
POST /api/employees/ as Employee role | 403 |
POST /api/employees/ as Manager or Admin | 201 |
DELETE /api/departments/1/ as Manager | 403 |
POST /api/register/ as non-Admin | 403 |
POST /api/login/ with a wrong password | 401 |
Tip: paste the access token at jwt.io to see user_id, username, role_id, role_name, exp. It also proves the payload is readable, so never put passwords in it.
Comments
Post a Comment