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:
- Cliente envía username + password a
/api/token/. - Servidor responde con
{access, refresh}. - Cliente envía peticiones con header
Authorization: Bearer <access>. - Cuando access caduca, cliente envía refresh a
/api/token/refresh/y recibe un access nuevo (y un refresh nuevo siROTATE_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_KEYde 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
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