Autenticación Avanzada10 temas

Autenticación Avanzada

JWT (JSON Web Tokens) es el estándar más usado para autenticación en APIs REST. Con djangorestframework-simplejwt se puede implementar un sistema completo con refresh tokens, claims personalizados y revocación de sesiones.

Flujo básico de JWT

Antes de entrar en código, este es el flujo típico de JWT con access y refresh tokens:

Diagrama: flujo básico de JWT

El cliente envía credenciales, recibe dos tokens, usa el access para pedir datos, y cuando expira usa el refresh para obtener uno nuevo sin pedir credenciales de nuevo.

Custom Claims

Un claim es un campo con información dentro del token JWT. SimpleJWT incluye estos por defecto:

Claim Significado
token_type "access" o "refresh" — qué tipo de token es
exp Fecha de expiración (timestamp Unix)
iat Fecha de emisión (timestamp Unix)
jti ID único del token (para blacklist, etc.)
user_id ID del usuario autenticado

Además de estos, el frontend recibe access y refresh (los strings del token), más payload con los claims si lo decodificás.

Pero se pueden agregar claims personalizados para evitar consultar la base de datos en cada request. Por ejemplo, si el rol del usuario no cambia seguido, guardalo en el token:

from rest_framework_simplejwt.serializers import TokenObtainPairSerializer


class CustomTokenSerializer(TokenObtainPairSerializer):
    @classmethod
    def get_token(cls, user):
        token = super().get_token(user)

        # Claims personalizados (se SUMAN a los estándar)
        token["email"] = user.email
        token["role"] = getattr(user, "role", "user")
        token["is_verified"] = user.is_verified

        return token


class CustomTokenView(TokenObtainPairView):
    serializer_class = CustomTokenSerializer

Esto permite leer role o email directamente del token en el frontend o en middlewares, sin hacer queries adicionales. El token se vuelve autosuficiente para ciertas decisiones.

Logout con Blacklist

Los JWT son stateless por diseño: no se puede invalidar un access token hasta que expire. Pero sí se puede blacklistear el refresh token para que el usuario no pueda renovar la sesión:

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework_simplejwt.exceptions import TokenError


class LogoutView(APIView):
    """
    Invalida el refresh token para cerrar la sesión.
    El access token actual expirará naturalmente.
    """
    permission_classes = [IsAuthenticated]

    def post(self, request):
        refresh = request.data.get("refresh")
        if not refresh:
            return Response(
                {"error": "refresh token requerido"},
                status=status.HTTP_400_BAD_REQUEST,
            )
        try:
            token = RefreshToken(refresh)
            token.blacklist()
            return Response(status=status.HTTP_205_RESET_CONTENT)
        except TokenError:
            return Response(
                {"error": "Token inválido o expirado"},
                status=status.HTTP_400_BAD_REQUEST,
            )

Importante: El access token sigue siendo válido hasta su expiración natural (por diseño JWT). La blacklist del refresh token impide renovar la sesión, pero el access actual se mantiene. Si se necesita invalidación inmediata, considerar usar tokens opacos (sesiones de Django) o listas de denegación de access tokens.

Flujo completo con Blacklist

Acá se ve cómo entra la blacklist en el ciclo de vida completo, incluyendo logout:

Diagrama: flujo JWT con blacklist

La diferencia clave: el access token no se invalida al hacer logout. Solo el refresh se blacklistea. Si alguien robó el access token, lo va a poder usar hasta que expire.

URLs

from django.urls import path
from rest_framework_simplejwt.views import TokenRefreshView

urlpatterns = [
    path('auth/token/', CustomTokenView.as_view(), name='token_obtain'),
    path('auth/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
    path('auth/logout/', LogoutView.as_view(), name='logout'),
]

Configuración recomendada

# settings.py
INSTALLED_APPS = [
    ...
    'rest_framework_simplejwt.token_blacklist',
]

SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
    'ROTATE_REFRESH_TOKENS': True,
    'BLACKLIST_AFTER_ROTATION': True,
}

Buenas prácticas

Práctica Por qué
ROTATE_REFRESH_TOKENS: True Cada vez que se renueva, el refresh anterior se invalida
BLACKLIST_AFTER_ROTATION: True Evita que refresh tokens viejos sigan siendo válidos
Access token corto (15-30 min) Minimiza la ventana de exposición si un token es robado
Refresh token más largo (7-30 días) Buena experiencia de usuario sin re-login constante

Recursos

SimpleJWT Docs