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 handDeleted. simplejwt builds and verifies tokens
authentication.py: you parsed the header, handled expiry and errorsA short subclass that only says how to find your User
One access tokenAccess token and refresh token, with a ready-made expiry system
Manual expiry settingSIMPLE_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

bash
pip install djangorestframework-simplejwt

You 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.

python
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.Username
bash
python manage.py makemigrations
python manage.py migrate

If you already did this earlier, skip it.

Step 2: Create roles and the first admin (shell)

python
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.

python
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.

python
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.

python
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)

python
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 user

Step 7: Views (views.py)

Imports:

python
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, RoleBasedWriteMixin

Login (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.

python
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.

python
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)

python
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

python
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 = ProjectSerializer

Step 8: URLs (employees/urls.py)

python
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/

json
{"Username": "admin", "Password": "Admin@123"}

Response:

json
{
  "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.

RequestResult
GET /api/employees/ without a token401
GET /api/employees/ with an expired or bad token401 with a message
GET /api/employees/?search=dev as Employee role200
POST /api/employees/ as Employee role403
POST /api/employees/ as Manager or Admin201
DELETE /api/departments/1/ as Manager403
POST /api/register/ as non-Admin403
POST /api/login/ with a wrong password401

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

Popular posts from this blog

Interview Tips: Dot NET Framework vs Net CORE

PRACTICE ASSIGNMENTS

OOP Concept in real-time scenario: Mobile Device Management Software