JWT Authentication in Django

JWT Authentication in Django

So you’ve built a Django app, and now you want to add secure, stateless authentication for your APIs or frontend clients. You’ve probably heard about JSON Web Tokens (JWT)—a popular, modern approach to handle user sessions without server-side storage. In this guide, I’ll walk you through what JWT is, why it’s useful, and how you can implement it in your Django project step by step.

What is JWT and Why Use It?

JWT is an open standard (RFC 7519) that defines a compact, self-contained way to securely transmit information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with HMAC algorithm) or a public/private key pair (like RSA or ECDSA).

Unlike traditional session-based authentication, where the server stores session data, JWT is stateless. The token itself contains all the necessary information about the user, so your server doesn’t need to keep a session store. This makes scaling easier and works beautifully with distributed systems or microservices.

A JWT consists of three parts: a header, a payload, and a signature, each base64-encoded and separated by dots. Here’s a quick example:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

The payload often includes "claims" about the user (like user ID or roles) and other metadata. Since the token is signed, you can verify that it hasn’t been tampered with.

Key benefits of using JWT in Django:

  • Statelessness: No need to store session data on the server.
  • Decentralized verification: Microservices can independently verify tokens.
  • Flexibility: Tokens can contain custom claims and are easy to use with frontend frameworks.

However, it’s important to note that JWTs have downsides too—such as larger payload sizes and the complexity of token invalidation. But for many use cases, they’re a great fit.

Setting Up JWT in Your Django Project

To get started with JWT authentication in Django, we’ll use the popular djangorestframework-simplejwt package. It’s well-maintained, integrates seamlessly with Django REST Framework (DRF), and provides a solid implementation of JWT standards.

First, make sure you have Django and DRF installed. Then, install the package:

pip install djangorestframework-simplejwt

Add it to your INSTALLED_APPS in settings.py:

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

Next, configure DRF to use JWT authentication by adding this to your settings.py:

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

Now, include the JWT routes in your urls.py:

from django.urls import path
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'),
]

That’s the basic setup! You now have endpoints to obtain and refresh JWTs.

Obtaining and Using JWTs

With the setup above, you can obtain a JWT by sending a POST request to /api/token/ with a username and password. Here’s an example using curl:

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

If the credentials are valid, you’ll receive a response like:

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

The access token is short-lived (e.g., 5 minutes) and must be included in the Authorization header for protected requests:

Authorization: Bearer <your_access_token>

The refresh token is used to obtain a new access token once it expires, by sending a POST to /api/token/refresh/ with the refresh token:

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

This returns a new access token, keeping the user authenticated without requiring them to log in again.

Customizing Token Claims

Often, you’ll want to include additional user information in the token payload, such as user ID, email, or permissions. You can do this by customizing the token class.

Create a serializers.py file in your app and define a custom token serializer:

from rest_framework_simplejwt.serializers import TokenObtainPairSerializer

class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
    @classmethod
    def get_token(cls, user):
        token = super().get_token(user)
        token['username'] = user.username
        token['email'] = user.email
        return token

Then, create a custom view to use this serializer:

from rest_framework_simplejwt.views import TokenObtainPairView
from .serializers import CustomTokenObtainPairSerializer

class CustomTokenObtainPairView(TokenObtainPairView):
    serializer_class = CustomTokenObtainPairSerializer

Update your urls.py to use this custom view:

from .views import CustomTokenObtainPairView

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

Now, when you obtain a token, it will include the extra claims you specified.

Securing Your JWT Implementation

While JWT is secure by design, how you implement it matters. Here are some best practices:

  • Use HTTPS: Always serve your API over HTTPS to prevent token interception.
  • Set reasonable expiration times: Short-lived access tokens reduce the risk of misuse.
  • Store tokens securely: On the client side, avoid storing tokens in local storage (which is vulnerable to XSS). Consider using httpOnly cookies, though this requires CSRF protection.
  • Implement token blacklisting: If you need to revoke tokens before they expire, consider using a blacklist. djangorestframework-simplejwt supports this through its Blacklist app.

To enable token blacklisting, first add 'rest_framework_simplejwt.token_blacklist' to INSTALLED_APPS, then run migrations:

python manage.py migrate

Then, in your settings, configure the token blacklist:

SIMPLE_JWT = {
    'BLACKLIST_AFTER_ROTATION': True,
}

This will automatically add refresh tokens to the blacklist after they are used to obtain a new access token.

Handling JWT in Django Views and Permissions

Once JWT authentication is set up, you can protect your API views easily. In DRF, you can use permission classes to restrict access:

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

class ProtectedView(APIView):
    permission_classes = [IsAuthenticated]

    def get(self, request):
        return Response({"message": "This is a protected view!"})

You can also access the user from the token in your views via request.user.

If you need to customize permissions further—for example, to allow only users with a specific role—you can create custom permission classes:

from rest_framework.permissions import BasePermission

class IsAdmin(BasePermission):
    def has_permission(self, request, view):
        return request.user and request.user.is_staff

class AdminView(APIView):
    permission_classes = [IsAuthenticated, IsAdmin]

    def get(self, request):
        return Response({"message": "Hello, admin!"})

Comparing JWT to Session Authentication

It’s useful to understand when to use JWT and when to stick with traditional sessions. Here’s a quick comparison:

Aspect JWT Authentication Session Authentication
Statefulness Stateless Stateful
Server Storage None (token holds data) Server stores session data
Scalability Excellent for distributed systems Requires sticky sessions or shared storage
Token Size Larger (contains payload) Smaller (session ID only)
Revocation Complex (requires blacklisting) Simple (delete session)
Use Case APIs, microservices Traditional web apps

In summary, JWT is ideal for API-based architectures where you want to avoid server-side state, while session authentication may be simpler for classic web applications with server-rendered views.

Troubleshooting Common JWT Issues

As you work with JWT, you might run into some common issues. Here are a few and how to resolve them:

  • Token not working: Ensure you’re including the token in the Authorization header as Bearer <token>.
  • Token expired: Use the refresh token to get a new access token.
  • Custom claims not appearing: Double-check your token serializer and view customization.
  • Blacklist not working: Verify that the token_blacklist app is in INSTALLED_APPS and migrations are applied.

If you need to debug, you can decode your JWT using online tools like jwt.io (just remember not to use sensitive tokens in public tools) or write a simple Python script:

import jwt
token = "your.jwt.token"
decoded = jwt.decode(token, options={"verify_signature": False})
print(decoded)

This will show you the payload without verifying the signature—useful for debugging, but never use this in production without verification!

Final Thoughts

JWT authentication is a powerful tool in your Django toolkit, especially for building scalable, stateless APIs. With djangorestframework-simplejwt, the setup is straightforward, and customization is flexible enough for most needs.

Remember to always follow security best practices, like using HTTPS and setting appropriate token expiration times. If you need to revoke tokens, leverage the blacklist feature.

I hope this guide helps you implement JWT authentication smoothly in your next project. Feel free to experiment, and happy coding!