Módulo 4 · Autenticación y seguridad

Lección 18 — Sesiones frente a tokens

JWT y sus riesgos, refresh tokens y qué encaja mejor con cada cliente.

Publicada
En esta lección
  1. Objetivos
  2. 1. Las dos familias
  3. 2. SimpleJWT en TicketFlow
  4. 3. Dónde vive el token (la decisión XSS)
  5. 4. La elección de TicketFlow (y su porqué)
  6. Autoevaluación

Stack: DRF · Proyecto: TicketFlow Estado: Publicada — apertura del módulo de seguridad Prerrequisito: Lección 17 — Webhooks


Objetivos

  1. Comparar sesión-servidor y token-stateless con su impacto real (revocación, escala, CSRF/XSS).
  2. Implementar auth por token con refresh en DRF (SimpleJWT) y decidir dónde vive el token en el frontend.
  3. Elegir la autenticación de TicketFlow con argumentos de entrevista.

1. Las dos familias

Sesión con cookie (Django default): el servidor guarda el estado (tabla session), el cliente lleva solo un ID opaco en cookie HttpOnly. Revocación: borrar la fila — instantánea. Escala: sesiones en BD/Redis compartidos (o sticky sessions, a evitar).

Token stateless (JWT): el servidor firma {user_id, exp, scopes}; no guarda nada. Cada request valida la firma. Revocación: NO trivial (el token vive hasta expirar) → se mitiga con expiraciones cortas + refresh token + blacklist (que ya es estado: admitirlo). Escala: cualquier pod valida sin consulta compartida.

Cookie sesiónJWT access+refresh
Revocación inmediatatrivialsolo con blacklist/rotación
Escala horizontalestado compartidosin estado
CSRFriesgo (mitigar SameSite/token)si va en header, inmune
XSScookie HttpOnly roba pocotoken en JS roba todo
Apps móvilescookies mehnativo

El mito a desmontar en entrevistas: "JWT = sin estado = mejor". El access token corto + refresh revocable + blacklist = estado en el servidor de vuelta. La pregunta real es dónde quieres el estado y qué revocas: comprar entradas con dinero revocable gana la sesión/cookie para la web; tokens para móviles y APIs de terceros.

2. SimpleJWT en TicketFlow

python
REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] = [
    "rest_framework_simplejwt.authentication.JWTAuthentication",
]

SIMPLE_JWT = {
    "ACCESS_TOKEN_LIFETIME": timedelta(minutes=10),   # corto: el daño de un robo expira
    "REFRESH_TOKEN_LIFETIME": timedelta(days=7),
    "ROTATE_REFRESH_TOKENS": True,                    # cada refresh emite refresh nuevo
    "BLACKLIST_AFTER_ROTATION": True,                 # el viejo muere: revocable
}

Endpoints: POST /api/auth/token/ (login), POST /api/auth/token/refresh/. El payload mínimo: user_id, exp; nada de datos sensibles (el JWT es legible en base64, firma ≠ cifrado).

3. Dónde vive el token (la decisión XSS)

  • localStorage: cómodo pero todo JS (incluido un XSS de terceros en tu bundle) lo lee y lo exporta.
  • Cookie HttpOnly + SameSite: el JS no la lee; CSRF se mitiga con SameSite=Lax/Strict (la 01). El access token en cookie HttpOnly corta el robo por XSS.
  • Memoria del SPA + refresh en HttpOnly cookie: el patrón moderno — access en memoria (muere con la pestaña), refresh silencioso vía cookie HttpOnly rotada. Un XSS roba el access de la sesión viva (no el refresh de larga vida).

Regla: refresh token siempre HttpOnly cookie con rotación y blacklist; access corto en memoria o cookie HttpOnly. localStorage solo si el threat model lo acepta explícitamente.

4. La elección de TicketFlow (y su porqué)

Web + móvil sobre la misma API: JWT corto + refresh rotado en cookie HttpOnly para la web; los mismos endpoints sirven al móvil (el móvil guarda el refresh en el keychain del SO, no en localStorage). Sesiones de Django siguen para el admin (/admin/), porque Django las trae reforzadas y es otro canal. Documentar la decisión (ADR-0008 en los ejercicios) vale tanto como implementarla.


Autoevaluación

  1. ¿Por qué "JWT elimina el estado del servidor" es a medias mentira cuando hay refresh + blacklist?
  2. Un XSS entra en tu SPA: ¿qué puede robar con el token en localStorage, en cookie HttpOnly, en memoria + refresh en cookie?
  3. ¿Por qué el access token es de 10 minutos y no de 7 días, y qué equilibra la rotación de refresh?
  4. ¿Por qué el JWT firmado NO es secreto y qué NO debe llevar dentro?
  5. ¿Qué elegirías para /admin/ de Django y por qué no hay que cambiarlo?

Continúa con los ejercicios. Las solutions.md solo tras intentarlo.