ProvenPin Install

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:20 UTC
Status
passed
Last passed
2026-10-15 13:20 UTC
Tests
5 / 5
Language
Python
Runtime
Python 3.13.16
pinned versions5
django@6.1.2djangorestframework@3.18.3djangorestframework-simplejwt@5.5.1pytest-django@4.14.0pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt
.venv/bin/python -m pytest -q
test environment
Python 3.13.16
linux 6.17.0-1022-azure
x64
runner: github-actions 38741369695

The code

Exactly the files that passed. Your agent can pull them with get_recipe("drf-simplejwt-auth") (install), or see them on GitHub.

requirements.txt
django==6.1.2
djangorestframework==3.18.3
djangorestframework-simplejwt==5.5.1
pytest-django==4.14.0
pytest==9.1.1
settings.py
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)}
urls.py
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"),
]
views.py
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"]})
tests/test_auth.py
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.ini
[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.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.