Autenticación JWT con djangorestframework-simplejwt

Avanzado
Django
Django
Actualizado: 19/04/2026

Por qué JWT

La autenticación tradicional con sesiones guarda el estado en el servidor (cookie de sesión + fila en la BBDD). Funciona bien para aplicaciones web monolíticas, pero tiene problemas en APIs modernas:

  • No escala horizontalmente sin sticky sessions o sesiones compartidas.
  • Obliga al cliente a manejar cookies, complicado en móviles y SPAs.
  • Difícil atravesar orígenes (CORS complejo).

JWT (JSON Web Token) resuelve esto con un token firmado que el cliente envía en cada petición. El servidor solo verifica la firma, sin tocar BBDD. Stateless, escalable y universal.

DRF incluye TokenAuthentication básico, pero el estándar real es djangorestframework-simplejwt: implementa JWT con access + refresh tokens, rotación y blacklist.

Instalación y configuración

pip install djangorestframework-simplejwt
# settings.py
INSTALLED_APPS = [
    ...
    'rest_framework',
    'rest_framework_simplejwt',
    'rest_framework_simplejwt.token_blacklist',  # necesario para logout
]

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
}

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',),
    'USER_ID_FIELD': 'id',
    'USER_ID_CLAIM': 'user_id',
}

Y la migración para blacklist:

python manage.py migrate

Endpoints de DRF Simple JWT

# urls.py
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'),
]

Flujo:

  1. Cliente envía username + password a /api/token/.
  2. Servidor responde con {access, refresh}.
  3. Cliente envía peticiones con header Authorization: Bearer <access>.
  4. Cuando access caduca, cliente envía refresh a /api/token/refresh/ y recibe un access nuevo (y un refresh nuevo si ROTATE_REFRESH_TOKENS = True).

Ejemplo de login

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

Respuesta:

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

Endpoint protegido

# views.py
from rest_framework.permissions import IsAuthenticated

class PerfilView(APIView):
    permission_classes = [IsAuthenticated]

    def get(self, request):
        return Response({
            'id': request.user.id,
            'username': request.user.username,
            'email': request.user.email,
        })
curl http://localhost:8000/api/perfil/ \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

Refresh token y rotación

Cuando el access caduca (15 min), el cliente hace:

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

Con ROTATE_REFRESH_TOKENS = True, la respuesta incluye un refresh nuevo:

{
  "access": "<new_access>",
  "refresh": "<new_refresh>"
}

Con BLACKLIST_AFTER_ROTATION = True, el refresh viejo queda en blacklist y ya no se puede usar. Esto protege ante robo: si alguien roba el refresh viejo, al rotarse queda inválido.

Claims personalizados

Por defecto el JWT lleva user_id, exp, iat, jti. Puedes añadir más:

# serializers.py
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer

class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
    @classmethod
    def get_token(cls, user):
        token = super().get_token(user)
        # Custom claims
        token['username'] = user.username
        token['email'] = user.email
        token['role'] = user.profile.role
        token['tenant_id'] = user.profile.tenant_id
        return token
# views.py
from rest_framework_simplejwt.views import TokenObtainPairView
from .serializers import MyTokenObtainPairSerializer

class MyTokenObtainPairView(TokenObtainPairView):
    serializer_class = MyTokenObtainPairSerializer

Con esto, el frontend puede decodificar el access (sin verificar firma) y obtener datos del usuario sin hacer otra query:

const payload = JSON.parse(atob(token.split('.')[1]));
console.log(payload.username, payload.role, payload.tenant_id);

Logout: blacklist del refresh token

Como JWT es stateless, un "logout" no puede invalidar el token en el cliente. La única forma es blacklist el refresh token:

# views.py
from rest_framework_simplejwt.tokens import RefreshToken

class LogoutView(APIView):
    permission_classes = [IsAuthenticated]

    def post(self, request):
        try:
            refresh_token = request.data['refresh']
            token = RefreshToken(refresh_token)
            token.blacklist()
            return Response(status=205)
        except Exception as e:
            return Response({'error': str(e)}, status=400)

Desde ese momento, ese refresh no puede obtener nuevos access tokens. El access actual sigue siendo válido hasta que expira (5-15 min), por eso se recomiendan ACCESS_TOKEN_LIFETIME cortos.

Claims de autorización: roles y permisos

Puedes aprovechar los claims para evitar queries en cada petición:

# permissions.py
from rest_framework.permissions import BasePermission

class IsAdmin(BasePermission):
    def has_permission(self, request, view):
        # Lee del JWT, no de la BBDD
        return request.auth and request.auth.get('role') == 'admin'

Cuidado: si el rol de un usuario cambia en la BBDD, su JWT actual sigue diciendo el rol viejo hasta que caduque. Para cambios críticos (ban, demotion) combina con una blacklist checked en cada petición.

Uso con Axios en SPA

// axios interceptor
import axios from 'axios';

const api = axios.create({ baseURL: '/api/' });

api.interceptors.request.use(config => {
  const token = localStorage.getItem('access');
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});

api.interceptors.response.use(null, async error => {
  if (error.response?.status === 401 && !error.config._retry) {
    error.config._retry = true;
    const refresh = localStorage.getItem('refresh');
    const { data } = await axios.post('/api/token/refresh/', { refresh });
    localStorage.setItem('access', data.access);
    localStorage.setItem('refresh', data.refresh);
    error.config.headers.Authorization = `Bearer ${data.access}`;
    return api(error.config);
  }
  return Promise.reject(error);
});

Este patrón refresca automáticamente cuando el access expira.

Testing con APIClient

from rest_framework.test import APITestCase
from rest_framework_simplejwt.tokens import RefreshToken

class PedidoAPITest(APITestCase):
    def setUp(self):
        self.user = User.objects.create_user(username='alan', password='secreto')
        refresh = RefreshToken.for_user(self.user)
        self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {refresh.access_token}')

    def test_list_pedidos(self):
        response = self.client.get('/api/pedidos/')
        self.assertEqual(response.status_code, 200)

Errores comunes

  • SIGNING_KEY = SECRET_KEY inseguro: en producción usa una clave dedicada rotable. Si usas SECRET_KEY de Django y la rotas, invalidas TODOS los JWT existentes.
  • ACCESS_TOKEN_LIFETIME demasiado largo: 1 hora es el máximo razonable. Nunca días.
  • No activar blacklist: sin ella, el logout no existe realmente.
  • Guardar JWT en localStorage con XSS vulnerable: si la SPA tiene XSS, el atacante roba el token. Considera httpOnly cookies con SameSite=Strict.
  • Exponer claims sensibles: el payload JWT es base64, cualquiera puede leerlo. No pongas contraseñas, números de tarjeta ni secretos.
  • ALGORITHM HS256 con clave débil: usa RS256 con claves RSA si hay múltiples servicios verificando el token (microservicios).

Producción

  • ALGORITHM = 'RS256' con par de claves pública/privada para microservicios.
  • Access ~5-15 min, refresh ~7 días.
  • Rotación y blacklist activados.
  • Claims mínimos necesarios: user_id, roles, tenant_id. Nada más.
  • HTTPS siempre: JWT en plano es robable en HTTP.
  • Rate limiting estricto en /token/ y /token/refresh/ para evitar ataques de enumeración y brute-force.
Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Django es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Django

Explora más contenido relacionado con Django y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Instalar y configurar simplejwt en settings.py con SIMPLE_JWT. Exponer los endpoints /token/, /token/refresh/ y /token/verify/. Añadir claims personalizados al payload (rol, tenant_id). Activar blacklist para logout y rotacion de refresh tokens. Integrar con el login social o con middleware propio. Testear el flujo completo con APIClient.

Cursos que incluyen esta lección

Esta lección forma parte de los siguientes cursos estructurados con rutas de aprendizaje