JWT Stateless: Arquitectura, seguridad y límites reales
Source: Dev.to
¿Cómo es posible autenticar de forma segura a millones de usuarios activos sin realizar una sola consulta a la base de datos? La autenticación basada en JSON Web Tokens (Stateless JWT) rompe el esquema tradicional al encapsular toda la identidad del usuario y firmarla criptográficamente. Este enfoque permite que cualquier servidor en un entorno distribuido verifique de manera matemática y en microsegundos la autenticidad del cliente, facilitando una escalabilidad horizontal masiva sin cuellos de botella. Tabla de contenidos El auge de las APIs distribuidas y el costo del estado ¿Qué es la Autenticación con JWT? Anatomía de un JSON Web Token (JWT) El flujo stateless de JWT Almacenamiento seguro del JWT en el cliente Arquitectura de Producción: Access Tokens y Refresh Tokens Para no morir en el intento y consejos que no pediste Conclusiones En el desarrollo de software moderno, las aplicaciones ya no viven en un único servidor monolítico. Las arquitecturas de microservicios, el cómputo serverless (sin servidor) y las redes de distribución global (Edge computing) exigen que las peticiones se procesen de la forma más independiente posible. Mantener una base de datos centralizada de sesiones activa en cada petición de usuario introduce latencia y crea un punto único de fallo. Aquí es donde entra en juego el diseño stateless (sin estado). Imagina que compras un boleto para el cine por internet. En la entrada, en lugar de buscar tu nombre en una lista impresa o consultar una base de datos centralizada, el taquillero simplemente escanea el boleto. El boleto contiene impreso tu número de asiento, la película y la fecha (Payload), y tiene un holograma de seguridad infalsificable estampado por el cine (Firma). Si el holograma es auténtico y la fecha coincide con el día de hoy, el taquillero te deja pasar inmediatamente sin hacer ninguna llamada telefónica ni consultar computadoras. En este escenario, el boleto con holograma es el JWT, el taquillero es el servidor web y tú eres el cliente. Este modelo es Stateless (sin estado) porque el servidor no necesita recordar quién eres ni mantener un registro de tu sesión en su disco o memoria RAM. La prueba de tu identidad reside enteramente en el token que tú mismo llevas y presentas en cada petición. Un JWT se compone de tres partes codificadas en Base64URL separadas por puntos (header.payload.signature): eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Indica el tipo de token (siempre JWT) y el algoritmo de firma criptográfica utilizado para protegerlo: { “alg”: “HS256”, “typ”: “JWT” }
Contiene las declaraciones (claims) sobre el usuario y metadatos del token. Existen tres tipos de claims: registrados (estándares como sub para ID de usuario, exp para expiración), públicos y privados (datos personalizados de tu app): { “sub”: “1234567890”, “name”: “John Doe”, “role”: “admin”, “exp”: 1801564800 }
[!caution] El Payload no es privado, solo está firmado cualquier persona en internet puede decodificarlos en milisegundos. Nunca guardes contraseñas, llaves de API o datos altamente sensibles dentro del payload de un JWT. Es la parte que garantiza que el token no haya sido modificado. Se calcula tomando el Header y el Payload codificados, uniéndolos con un punto, y procesándolos con el algoritmo especificado (ej. HMAC SHA256) usando una clave secreta que solo reside en tu servidor: HMACSHA256( base64UrlEncode(header) + ”.” + base64UrlEncode(payload), clave_secreta_del_servidor )
El flujo básico de autenticación basada en tokens JWT funciona de la siguiente manera: Inicio de sesión: El cliente envía sus credenciales. Generación de Tokens: El servidor valida las credenciales y, si son válidas, genera un Access Token (vida corta, ej. 15 minutos) y un Refresh Token (vida larga, ej. 30 días). El ID del Refresh Token se almacena en el backend para poder revocarlo. Retorno y Almacenamiento:
El Access Token se envía en el cuerpo de la respuesta JSON y el cliente lo almacena en memoria de JavaScript. El Refresh Token se envía en una cookie segura con las banderas HttpOnly, Secure y SameSite=Lax. Validación en cada petición: Para acceder a rutas protegidas, el cliente adjunta manualmente el Access Token en la cabecera Authorization: Bearer . El servidor lo valida matemáticamente (Stateless) usando su clave secreta del backend y procesa la petición de inmediato. Expiración y Refresco (Refresh): Cuando el Access Token expira, la API devuelve un error 401 Unauthorized. El cliente realiza en segundo plano una llamada a /api/refresh. El navegador envía la cookie del Refresh Token automáticamente. El servidor valida la vigencia del token contra la base de datos/Redis y, si es correcto, devuelve un nuevo Access Token para reintentar la petición original. Cierre de sesión: El servidor elimina o invalida el Refresh Token de la base de datos y añade la firma del Access Token a una lista negra temporal en Redis para denegar accesos residuales antes de su expiración física. El siguiente diagrama detalla la secuencia de interacciones entre el cliente y el servidor API al utilizar tokens de acceso firmados de manera matemática:
Decidir dónde guardar el JWT en el navegador es un debate clásico de seguridad: localStorage o sessionStorage
Vectores de ataque: Altamente vulnerable a XSS (Cross-Site Scripting). Si un atacante logra inyectar código JavaScript en tu frontend (a través de una vulnerabilidad en una dependencia de npm, un CDN comprometido o un input mal sanitizado), podrá ejecutar localStorage.getItem(‘token’) y enviárselo a su propio servidor, robando la identidad del usuario por completo. Ventaja: Es sumamente fácil de leer y adjuntar a peticiones asíncronas vía código. HttpOnly, Secure, SameSite=Lax) - Recomendado
Vectores de ataque: Al activar HttpOnly, JavaScript de cliente pierde el acceso al token, bloqueando los robos vía XSS. Sin embargo, al viajar automáticamente en las peticiones, introduce el riesgo de CSRF (Cross-Site Request Forgery). Mitigación: El uso de la bandera SameSite=Lax junto con tokens anti-CSRF mitiga eficazmente este vector. Es la opción recomendada para almacenar tokens en entornos web de producción. Mantener un JWT activo por semanas es un riesgo de seguridad enorme: si el token es robado, el atacante tendrá acceso ilimitado. Para mitigar esto, las aplicaciones de producción implementan un esquema de doble token:
Access Token:
Función: Autenticar las peticiones a la API. Duración: Muy corta (ej. 15 minutos). Almacenamiento: Idealmente en memoria JS (no se escribe en disco) o en cookies seguras de vida corta. Refresh Token:
Función: Solicitar nuevos Access Tokens cuando expiren. Duración: Larga (ej. 7 a 30 días). Almacenamiento: Guardado obligatoriamente en una cookie HttpOnly, Secure y SameSite=Lax en una ruta específica /api/refresh. Validación: Sí requiere una consulta a la base de datos (o Redis) en el servidor para verificar que el Refresh Token no haya sido revocado. Implementar JWT de manera stateless tiene consecuencias en la flexibilidad que debes conocer y gestionar desde el primer día de desarrollo. La naturaleza stateless de JWT tiene un costo: no se puede invalidar un token de forma remota antes de su fecha de vencimiento. Si un usuario cambia su contraseña, cierra sesión o es bloqueado, su Access Token seguirá siendo válido en cualquier servidor hasta que expire. Para solucionar esto sin perder todas las ventajas de rendimiento, se utiliza el patrón de lista negra (Blacklisting): Cuando un usuario cierra sesión, el servidor toma el jti (identificador único del JWT) o su firma y lo almacena temporalmente en Redis con un tiempo de expiración idéntico al tiempo de vida restante del token. Durante la validación de peticiones, el servidor verifica rápidamente en Redis si el token está en la lista negra. Si está, bloquea el acceso. Una vez que pasa la hora de expiración natural del token, este se elimina automáticamente de Redis, manteniendo el tamaño del almacenamiento de la lista negra optimizado. [ ] Claves de Firma Fuertes: Utiliza algoritmos robustos (ej. RS256 usando claves públicas/privadas en lugar de HS256) y rota las claves periódicamente. [ ] No almacenar datos sensibles: Asegúrate de que el payload no contenga información confidencial, ya que cualquiera puede decodificarlo. [ ] Validación de Claims Obligatoria: Valida siempre exp (expiración), iat (emisión), y de ser posible iss (emisor) y aud (audiencia). JWT es una herramienta potente para escalar APIs de microservicios y comunicar sistemas distribuidos sin sobrecargar las bases de datos de sesión. Sin embargo, su implementación en producción requiere extremo cuidado en la duración del token y la protección frente a ataques en el almacenamiento del cliente. Nunca guardes datos sensibles en el payload: Base64 es legible para cualquiera. Mantén los Access Tokens con vida corta: 15 minutos es el estándar de seguridad óptimo. Protege tus Refresh Tokens: Guárdalos bajo cookies seguras HttpOnly e implementa listas de revocación.