JWT Based Authentication in Django using djangorestframework-simplejwt

A JSON Web Token (JWT) is like a secure, digital ID card for web apps. Instead of checking a database every single time a user clicks a page, the server gives the user a token when they log in. The user's browser attaches this token to every future request. The server looks at the token, instantly knows who the user is, and lets them in, saving a lot of database work.

With django, we can easily add JWT based authentication to our project using the djangorestframework-simplejwt package.

A JWT consists of three Base64URL-encoded parts separated by dots:

header.payload.signature
  • Header: token type and hashing algorithm (e.g., HS256)
  • Payload: claims: user ID, expiry time, roles, etc.
  • Signature: ensures the token hasn't been tampered.

Create a django project with dependencies

Create a virtual environment, and use it to created project in django. I assume readers knows basics of django and django rest framework.

For this article, we will use djangorestframework-simplejwt, the most popular and actively maintained JWT library for Django Rest Framework(DRF).

# create project folder
mkdir jwt_auth_demo
cd jwt_auth_demo

# create virtual environment
python -m venv .venv
# OR
python3 -m venv .venv

# activate virtual env
source .venv/bin/activate # linux based os & macos
# OR
.venv\Scripts\activate # windows

# install dependencies
pip install django
pip install django_rest_framework
pip install djangorestframework-simplejwt

# create django project inside jwt_auth_demo, [. tells this]
django-admin startproject core .

# create an app
django-admin startapp account

Configure Django Settings

Open core/settings.py and update it:

INSTALLED_APPS = [
    # ...
    'rest_framework',
    'rest_framework_simplejwt',
    'account' # our app
]

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ),
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    ),
}

Optionally, customize token lifetimes and behavior in core/settings.py.

from datetime import timedelta

SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
    'ROTATE_REFRESH_TOKENS': True,
    'BLACKLIST_AFTER_ROTATION': True,
    'ALGORITHM': 'HS256',
    'SIGNING_KEY': SECRET_KEY,
    'AUTH_HEADER_TYPES': ('Bearer',),
}

Tip: Keep ACCESS_TOKEN_LIFETIME short (5–15 minutes) and rely on refresh tokens for long-lived sessions.

Don't forget to migrate default table requried for django

python manage.py migrate

Set Up URLs

Update your path in core/urls.py:

from django.urls import path
from rest_framework_simplejwt.views import (
    TokenObtainPairView,
    TokenRefreshView,
    TokenVerifyView,
)

urlpatterns = [
    path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
    path('api/token/verify/', TokenVerifyView.as_view(), name='token_verify'),
]

This gives you three endpoints out of the box:

Endpoint Method Purpose
/api/token/ POST Login: returns access + refresh token
/api/token/refresh/ POST Get a new access token using refresh token
/api/token/verify/ POST Checks validity of a token

Create View

Now, we will create a ProfileView class on account/views.py:

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated

class ProfileView(APIView):
    permission_classes = [IsAuthenticated]

    def get(self, request):
        user = request.user
        return Response({
            'id': user.id,
            'username': user.username,
            'email': user.email,
        })

Create account/urls.py and add path for view. And, also update core/urls.py to make sure it finds account/urls.py

account/urls.py

from django.urls import path

from .views import ProfileView

urlpatterns = [
    path("profile", ProfileView.as_view(), name="profile"),
]

core/urls.py

from django.contrib import admin
from django.urls import path, include # include added
from rest_framework_simplejwt.views import (
    TokenObtainPairView,
    TokenRefreshView,
    TokenVerifyView,
)

urlpatterns = [
    # ...
    path("account/", include("account.urls")), # for account/urls.py
]

Test the Flow

Create a superuser in django and now we will test the views. We are using curl here, you can use Postman, Bruno or any other tools of your choice.

Obtain Tokens (Login)
curl -X POST http://localhost:8000/api/token/ \
  -H "Content-Type: application/json" \
  -d '{"username": "admin", "password": "admin"}'

Response:

{
  "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Access a Protected Endpoint
curl -X GET http://localhost:8000/account/profile \
  -H "Authorization: Bearer <your_access_token>"

You will get user info in json as response

Refresh an Expired Access Token
curl -X POST http://localhost:8000/api/token/refresh/ \
  -H "Content-Type: application/json" \
  -d '{"refresh": "<your_refresh_token>"}'

You will get new token as response


Customize the Token Payload

By default, the access token only includes the user ID. You can add extra claims (like roles or email) by customizing the serializer. Create a new file account/serializers.py and add this:

from rest_framework_simplejwt.serializers import TokenObtainPairSerializer

class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
    @classmethod
    def get_token(cls, user):
        token = super().get_token(user)

        # Add custom claims
        token['email'] = user.email
        token['is_staff'] = user.is_staff
        token['full_name'] = f'{user.first_name} {user.last_name}'.strip()

        return token

Register the custom view on account/views.py. Add new class.

from rest_framework_simplejwt.views import TokenObtainPairView
from .serializers import CustomTokenObtainPairSerializer

class CustomTokenObtainPairView(TokenObtainPairView):
    serializer_class = CustomTokenObtainPairSerializer

Update account/urls.py

from .views import CustomTokenObtainPairView

urlpatterns = [
    # ...
    path('api/token/', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'),
]

Enable Token Blacklisting

For logout to invalidate(blacklist) tokens server-side, enable the blacklist app in core/settings.py

INSTALLED_APPS = [
    # ...
    'rest_framework_simplejwt.token_blacklist',
]

Run migrations:

python manage.py migrate

Add a logout view on account/views.py

from rest_framework import status
from rest_framework.permissions import AllowAny
from rest_framework_simplejwt.tokens import RefreshToken


class LogoutView(APIView):

    authentication_classes = []  
    permission_classes = [AllowAny]

    def post(self, request):
        try:
            refresh_token = request.data['refresh']
            token = RefreshToken(refresh_token)
            token.blacklist()
            return Response({'detail': 'Successfully logged out.'}, status=status.HTTP_200_OK)
        except Exception:
            return Response({'detail': 'Invalid token.'}, status=status.HTTP_400_BAD_REQUEST)

Update account/urls.py to add path for LogoutView

from django.urls import path

from .views import ProfileView, CustomTokenObtainPairView, LogoutView

urlpatterns = [
    # ...
    path("logout/", LogoutView.as_view(), name="log_out"),
]

Comment out CsrfViewMiddleware in core/settings.py if you get Forbidden (403) CSRF verification failed.

Commenting CsrfViewMiddleware shouldn't be done on real projects. There are other ways to deal with 403 CSRF failed.

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    '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',
]

Complete Project Structure

jwt_auth_demo/
|-core/
|  |- settings.py       # JWT config here
|  |- urls.py           # Token endpoints here
|-account/
|  |- views.py          # Protected views + LogoutView
|  |- serializers.py    # CustomTokenObtainPairSerializer
|  |- urls.py
|- manage.py

Summary

JWT authentication in Django is really simple with djangorestframework-simplejwt. Workflow of JWT authentication:

  1. User POSTs credentials -> receives access + refresh tokens
  2. Client includes Authorization: Bearer <access_token> on each request
  3. When the access token expires, POST the refresh token to get a new one
  4. On logout, blacklist the refresh token server-side

Use JWT based authentication with short token lifetimes, token rotation, and HTTPS, and you will have a good authentication foundation for any Django REST API.

For detailed information, you can visit djangorestframework-simplejwt official docs.