Django REST Framework JWT auth with simplejwt, tested offline: working versions (tested 2026-10-15)
Obtain and refresh JWT pairs with simplejwt, add custom claims, protect a DRF view, and test expired-token rejection on sqlite in-memory with pytest-django.
passed5 passed · 0 failed2026-10-15 13:28 UTC- Status
- passed
- Last passed
- 2026-10-15 13:28 UTC
- Tests
- 5 / 5
- Language
- Python
- Runtime
- Python 3.13.16
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38742087302
The code
Exactly the files that passed. Your agent can pull them with get_recipe("drf-simplejwt-auth") (install), or see them on GitHub.
django==6.1.2 djangorestframework==3.18.3 djangorestframework-simplejwt==5.5.1 pytest-django==4.14.0 pytest==9.1.1
from datetime import timedelta
SECRET_KEY = "test-only-secret-key-not-for-production-use-0123456789abcdefghijklmnop"
DEBUG = False
ALLOWED_HOSTS = ["testserver"]
INSTALLED_APPS = [
"django.contrib.auth",
"django.contrib.contenttypes",
"rest_framework",
"rest_framework_simplejwt",
]
DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}}
ROOT_URLCONF = "urls"
USE_TZ = True
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": ["rest_framework_simplejwt.authentication.JWTAuthentication"],
}
SIMPLE_JWT = {"ACCESS_TOKEN_LIFETIME": timedelta(minutes=5)}
from django.urls import path
from rest_framework_simplejwt.views import TokenRefreshView
from views import MyTokenObtainPairView, me
urlpatterns = [
path("api/token/", MyTokenObtainPairView.as_view(), name="token_obtain"),
path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
path("api/me/", me, name="me"),
]
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework_simplejwt.views import TokenObtainPairView
class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
@classmethod
def get_token(cls, user):
# Claims added here land on the refresh token; access tokens derived
# from it (including via /refresh/) inherit them.
token = super().get_token(user)
token["role"] = "admin" if user.is_staff else "member"
return token
class MyTokenObtainPairView(TokenObtainPairView):
serializer_class = MyTokenObtainPairSerializer
@api_view(["GET"])
@permission_classes([IsAuthenticated])
def me(request):
return Response({"username": request.user.username, "role": request.auth["role"]})
from datetime import timedelta
import pytest
from django.contrib.auth import get_user_model
from rest_framework.test import APIClient
from rest_framework_simplejwt.tokens import AccessToken
PASSWORD = "pw-123456"
@pytest.fixture
def user(db):
return get_user_model().objects.create_user("ann", password=PASSWORD, is_staff=True)
@pytest.fixture
def api():
return APIClient()
def bearer(token):
return {"HTTP_AUTHORIZATION": f"Bearer {token}"}
def obtain(api):
r = api.post("/api/token/", {"username": "ann", "password": PASSWORD}, format="json")
assert r.status_code == 200
return r.data
def test_obtain_pair_has_custom_claim(api, user):
access = AccessToken(obtain(api)["access"])
assert access["role"] == "admin"
def test_bad_password_rejected(api, user):
r = api.post("/api/token/", {"username": "ann", "password": "nope"}, format="json")
assert r.status_code == 401
def test_protected_view_requires_token(api, user):
assert api.get("/api/me/").status_code == 401
r = api.get("/api/me/", **bearer(obtain(api)["access"]))
assert r.status_code == 200
assert r.data == {"username": "ann", "role": "admin"}
def test_expired_access_token_rejected(api, user):
# ACCESS_TOKEN_LIFETIME is read when the token class is imported, so
# override_settings has no effect here. Set the exp claim directly instead.
token = AccessToken.for_user(user)
token.set_exp(lifetime=timedelta(seconds=-1))
assert api.get("/api/me/", **bearer(str(token))).status_code == 401
def test_refresh_keeps_custom_claims(api, user):
refresh = obtain(api)["refresh"]
r = api.post("/api/token/refresh/", {"refresh": refresh}, format="json")
assert r.status_code == 200
assert AccessToken(r.data["access"])["role"] == "admin"
[pytest] pythonpath = . DJANGO_SETTINGS_MODULE = settings
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
Django 6 + Django REST Framework model API with pytest-django
A DRF ModelViewSet with a serializer, router and token-free permissions, tested with pytest-django and APIClient on in-memory SQLite.
DRF ModelViewSet with django-filter range, exact and ordering filters
A ModelViewSet with a django-filter FilterSet for price ranges, exact name and ordering, plus page-number pagination, configured in code and tested offline with APIClient on in-memory SQLite.
Django models with factory_boy and pytest-django db fixtures
Django models built with factory_boy DjangoModelFactory (SubFactory, get_or_create, traits) and tested offline on in-memory SQLite with pytest-django database access rules and on_commit capture.
Django Ninja API with Schema models and pagination on sqlite
A typed Django Ninja CRUD API with Schema input and output models, page-number pagination and sqlite, tested offline with the ninja TestClient and pytest-django.
All Django recipesAll Django REST Framework recipesAll djangorestframework-simplejwt recipesAll pytest-django recipes