This tutorial covers usage of djangorestframework-simplejwt — the most popular Django Simple JWT library — to add JWT authentication to your Django REST Framework project. We cover the full flow: login, token refresh, token blacklisting, logout, and custom claims.

While you can use this tutorial for any Django project, it is recommended that you follow our Django Project Tutorial for beginners

Also, we don't cover docker-compose.yml file and some other stuff which you can find in the Django Project Tutorial so we can focus on implementing JWT Authentication.

Source repository: https://github.com/appliku/djangojwt2fa

In this article:

Install djangorestframework-simplejwt

Make sure you have the following lines in requirements.txt:

Django==5.1
djangorestframework==3.15.2
djangorestframework-simplejwt==5.3.1
django-environ==0.11.2
django-cors-headers==4.4.0
psycopg2-binary==2.9.9
gunicorn==22.0.0

Note: there can be other packages as well, these are directly required or recommended packages to have.

Django Settings

Add the following code to settings.py of your project:

# REST FRAMEWORK
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    )
}

Add the token_blacklist app to INSTALLED_APPS (or THIRD_PARTY_APPS if you use a project template):

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

This configures Django REST Framework to use JWTAuthentication backend.

In the project's urls.py (adjacent to settings.py) add the following imports and url_patterns:

from rest_framework_simplejwt.views import (
    TokenObtainPairView,
    TokenRefreshView,
)

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

Why we need these endpoints:

The token_obtain_pair is needed for the "login" process, when the user provides username and password and gets access_token and refresh_token. The token_refresh is needed when the access_token expires and with the refresh_token the user gets a new access_token. With proper settings, the refresh_token is also rotated on each refresh.

Django SimpleJWT Settings Reference

The djangorestframework-simplejwt library provides a SIMPLE_JWT configuration object. Here is a recommended production-ready setup with explanations for each setting:

from datetime import timedelta

SIMPLE_JWT = {
    # How long the access token is valid. Keep this short (5–30 minutes).
    # After expiry, the client must use the refresh token to get a new one.
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30),

    # How long the refresh token is valid. Longer lifetimes mean users stay
    # logged in longer but increase risk if a token is stolen.
    'REFRESH_TOKEN_LIFETIME': timedelta(days=3),

    # Issue a new refresh token every time /api/token/refresh/ is called.
    # This extends the session while the user is active.
    'ROTATE_REFRESH_TOKENS': True,

    # Blacklist the old refresh token after rotation. Requires
    # 'rest_framework_simplejwt.token_blacklist' in INSTALLED_APPS.
    'BLACKLIST_AFTER_ROTATION': True,

    # Use a dedicated secret key for signing JWTs, separate from SECRET_KEY.
    # This allows you to invalidate all tokens without changing SECRET_KEY.
    'SIGNING_KEY': env('SIMPLE_JWT_SIGNING_KEY', default=None) or SECRET_KEY,

    # Hashing algorithm used to sign the token.
    'ALGORITHM': 'HS256',

    # Update user.last_login on every token issue.
    'UPDATE_LAST_LOGIN': False,

    # The prefix in the Authorization header: "Bearer <token>"
    'AUTH_HEADER_TYPES': ('Bearer',),

    # The user model field used as the identity claim in the token payload.
    'USER_ID_FIELD': 'id',
    'USER_ID_CLAIM': 'user_id',

    # The claim name that stores the JWT ID (used for blacklisting).
    'JTI_CLAIM': 'jti',
}

With these settings: access tokens are valid for 30 minutes, refresh tokens for 3 days. Refresh tokens rotate on every use and the old one is immediately blacklisted.

Security tip: Set SIMPLE_JWT_SIGNING_KEY as a separate environment variable in production. If you ever need to invalidate all tokens (e.g. after a security breach), you can rotate just the signing key without affecting SECRET_KEY, which is used by many other Django internals.

Now let's apply run our app and apply migrations.

First let's run docker compose up db -d in order for our db to initialize.

Then run docker compose run web python manage.py migrate to apply migrations.

Expected output of this command should be a lot of "Applying... " lines and no errors.

Let's create a superuser.

Run docker compose run web python manage.py createsuperuser command.

Now we can run the development server of our project:

docker compose up

This will spin up all backing services and our application in web container.

Let's open admin panel of our Django Project in a browser. In order to do that open this URL: http://0.0.0.0:8000/admin/

Once you were able to login into the admin panel, let's make sure that we have the Blacklist app installed. On the main page of the admin panel you should see a block called TOKEN BLACKLIST and two models: Blacklisted tokens and Outstanding tokens.

Django Simple JWT TOKEN BLACKLIST and two models Blacklisted tokens and Outstanding tokens

Testing Django SimpleJWT token_obtain_pair endpoint

Let's use CURL command to issue a token pair.

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

Output for this command should look like this:

{
  "refresh": "eyJ0eXAiOiJKV1Qi...",
  "access": "eyJ0eXAiOiJKV1Qi..."
}

As you can see the response contains refresh with the refresh token and access with the access token.

Now let's try to refresh our tokens. Replace the refresh token with the one you got from the last command.

curl \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"refresh": "eyJ0eXAiOiJKV1Qi..."}' \
  http://localhost:8000/api/token/refresh/

The output should again be an object with both access and refresh keys (since ROTATE_REFRESH_TOKENS is True).

Now run the same command again with the old refresh token — it should no longer work:

{"detail":"Token is blacklisted","code":"token_not_valid"}

It means that the blacklist app is installed and settings are in place to rotate and blacklist old refresh tokens.

You can go to the Django Admin and verify that the refresh token is blacklisted.

Django Admin Blacklisted refresh token JWT Authentication

Custom Claims in Django Simple JWT

By default, djangorestframework-simplejwt only embeds the user's id in the token payload. You can add extra data — called custom claims — such as the user's email, username, or role. This is useful when your frontend needs this information without making a separate API call.

To add custom claims, subclass TokenObtainPairSerializer and override get_token:

# usermodel/serializers.py
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer

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

        # Add custom claims to the token payload
        token['username'] = user.username
        token['email'] = user.email
        token['is_staff'] = user.is_staff

        return token

Then create a custom view that uses this serializer:

# usermodel/views.py
from rest_framework_simplejwt.views import TokenObtainPairView
from .serializers import MyTokenObtainPairSerializer

class MyTokenObtainPairView(TokenObtainPairView):
    serializer_class = MyTokenObtainPairSerializer

Update urls.py to use your custom view:

from usermodel.views import MyTokenObtainPairView
from rest_framework_simplejwt.views import TokenRefreshView

urlpatterns = [
    path('api/token/', MyTokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]

Now when you call /api/token/, the returned JWT will include your custom fields in the payload. You can decode any JWT at jwt.io to verify the payload.

Important: Do not put sensitive data (passwords, payment info) in JWT claims. The token payload is base64-encoded, not encrypted — anyone with the token can decode and read it.

Django JWT Authentication Logout

Up until this point we've put all the code in place to perform the login scenario.

Now let's implement logout functionality.

With JWT authentication, the access_token is issued for a short period and is always valid until it expires — it cannot be individually revoked. The only revocable token is the refresh_token.

What happens on logout: 1. The client forgets both tokens (e.g. removes them from localStorage) 2. But if the refresh_token was stolen, another client can keep using it indefinitely

To prevent this, we create a logout API endpoint that blacklists the current refresh_token — or all refresh tokens issued to the user (for "logout all devices").

Create Django Logout JWT APIView

Open usermodel/views.py and add the following code:

from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework_simplejwt.token_blacklist.models import OutstandingToken, BlacklistedToken
from rest_framework_simplejwt.tokens import RefreshToken


class APILogoutView(APIView):
    permission_classes = (IsAuthenticated,)

    def post(self, request, *args, **kwargs):
        if self.request.data.get('all'):
            token: OutstandingToken
            for token in OutstandingToken.objects.filter(user=request.user):
                _, _ = BlacklistedToken.objects.get_or_create(token=token)
            return Response({"status": "OK, goodbye, all refresh tokens blacklisted"})
        refresh_token = self.request.data.get('refresh_token')
        token = RefreshToken(token=refresh_token)
        token.blacklist()
        return Response({"status": "OK, goodbye"})

In your root urls.py add:

from django.contrib import admin
from django.urls import path, include
from rest_framework_simplejwt.views import TokenRefreshView
from usermodel.views import MyTokenObtainPairView, APILogoutView

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/token/', MyTokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
    path('api/logout/', APILogoutView.as_view(), name='logout_token'),
]

Let's test it. First obtain tokens as before:

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

Now call logout with your access token in the Authorization header:

curl \
  -X POST \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <your_access_token>" \
  -d '{"refresh_token": "<your_refresh_token>"}' \
  http://localhost:8000/api/logout/

Response: {"status":"OK, goodbye"}

Logout all devices (blacklist all refresh tokens)

Send all instead of a specific refresh token to blacklist every refresh token for the current user:

curl \
  -X POST \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <your_access_token>" \
  -d '{"all": "1"}' \
  http://localhost:8000/api/logout/

Response: {"status":"OK, goodbye, all refresh tokens blacklisted"}

Since OutstandingToken has a user foreign key, we can find all tokens issued for a user and blacklist them in one call.

Pro tip: Call this same logic in your password reset flow — after a user resets their password, blacklist all outstanding tokens to force re-authentication on all devices.

Summary

Here is what we covered:

  • Install djangorestframework-simplejwt and configure DRF to use JWT authentication
  • Settings referenceACCESS_TOKEN_LIFETIME, REFRESH_TOKEN_LIFETIME, ROTATE_REFRESH_TOKENS, BLACKLIST_AFTER_ROTATION, and the separate SIGNING_KEY
  • Refresh token flow — how access_token and refresh_token work together
  • Token blacklisting — using the built-in token_blacklist app to invalidate tokens
  • Custom claims — embedding extra user data in the JWT payload using TokenObtainPairSerializer
  • Logout — blacklisting a single refresh token or all tokens for the current user (logout all devices)