JWT sin estado vs sesiones con estado: el criterio que uso para elegir en sistemas de identidad
Estaba revisando la arquitectura de validación de tokens de un backend de identidad cuando me encontré con algo que me incomodó bastante: el sistema emitía JWT con expiración de 24 horas y no había ningún mecanismo de revocación. Si un token se comprometía, el único remedio era esperar que expirara. Veinticuatro horas de ventana para un atacante con credencial válida.
Pregunté por qué. La respuesta fue la de siempre: "JWT es stateless, escala mejor, no necesita base de datos." Como argumento de escalabilidad, no está mal — pero acá lo que estaba en juego no era escala, era revocación. Y ahí la respuesta se cae.
Mi tesis: JWT stateless es una optimización prematura en la mayoría de sistemas de identidad. Si necesitás revocación inmediata o auditoría fina, el estado no es el enemigo — es exactamente lo que necesitás. El debate no es "JWT malo, sesiones buenas": es cuándo el costo de stateless supera el beneficio.
jwt vs sesiones con estado identidad digital criterio: qué significa realmente la elección
La dicotomía se simplifica demasiado seguido. JWT stateless significa que el servidor no necesita consultar ningún store para validar un token: toda la información está en el token mismo, firmada. Eso es genuinamente valioso en ciertos contextos. El problema es cuando ese diseño se aplica sin preguntarse qué pasa cuando algo sale mal.
Con JWT stateless puro tenés dos palancas: el tiempo de expiración (exp) y la firma. Si el secreto o la clave privada no se comprometieron, cualquier token firmado y vigente es válido. Punto. No hay "revocar este token específico" sin agregar estado en algún lado.
Las sesiones con estado son otra cosa: el servidor guarda un registro de la sesión (en memoria, Redis, base de datos) y puede darlo de baja cuando quiera, sin esperar a que nada expire. El trade-off es la dependencia del store: si Redis no responde, la validación falla. Eso es un costo real que no debería minimizarse.
El error no es elegir uno u otro. El error es no preguntarse qué nivel de control necesita el sistema en el que estás trabajando.
Lo que dice el RFC 7009 — y lo que no dice
El RFC 7009 — OAuth 2.0 Token Revocation es el estándar que define cómo un cliente puede solicitar la revocación de un token ante el Authorization Server. Define el endpoint /revoke, los parámetros esperados y el comportamiento del servidor.
Lo que el RFC dice explícitamente:
- El Authorization Server debería revocar los tokens dependientes cuando se revoca un refresh token (sección 4.1).
- La revocación de un access token JWT no elimina el token del mundo: solo registra que fue revocado en el servidor que implementa el endpoint.
- La especificación no define cómo el Resource Server se entera de que un token fue revocado.
Ese último punto es el que más se omite en los tutoriales. El RFC 7009 resuelve la comunicación entre cliente y Authorization Server. No resuelve el problema de que un Resource Server validando JWT de forma completamente stateless no tiene forma de saber que ese token fue revocado, a menos que vaya a consultar al Authorization Server o a un store compartido.
Spring Security documenta esto con claridad en su guía de OAuth2 Resource Server: la validación JWT por defecto es local (verificación de firma + claims como exp, nbf, iss). Para revocación activa necesitás implementar token introspection o un mecanismo propio de blocklist.
// Validación JWT stateless por defecto en Spring Security
// Solo verifica firma, exp, iss — NO consulta ningún store externo
http
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.decoder(NimbusJwtDecoder
.withJwkSetUri("https://auth.ejemplo.com/.well-known/jwks.json")
.build())
)
);// Para revocación real necesitás token introspection activa
// El Resource Server consulta al Authorization Server en cada request
http
.oauth2ResourceServer(oauth2 -> oauth2
.opaqueToken(opaque -> opaque
.introspectionUri("https://auth.ejemplo.com/introspect")
.introspectionClientCredentials("client-id", "client-secret")
)
);La segunda opción tiene latencia por request adicional. Eso es el costo honesto de la revocación real.
Dónde se equivoca la gente: el costo oculto del stateless
El argumento más común a favor de JWT stateless en sistemas de identidad es la escalabilidad: "sin state, sin coordinación entre instancias, horizontal scaling gratis." Es un argumento válido para APIs públicas de lectura con tokens de corta vida. Para un sistema de identidad con usuarios reales, es frecuentemente un espejismo.
El costo oculto número uno: ventanas de compromiso largas.
Si emitís tokens con expiración de 1 hora o más y no tenés revocación, una credencial robada tiene una ventana de ataque proporcional. En un sistema de identidad donde el token da acceso a operaciones sensibles — cambios de perfil, firma de documentos, acceso a datos personales — esa ventana importa.
El costo oculto número dos: auditoría imposible.
Los sistemas de identidad en contextos regulados o con requisitos de compliance necesitan saber qué token se usó, cuándo, desde qué IP, para qué operación. Con JWT stateless puro, esa información no existe en el servidor a menos que la loguees explícitamente en cada Resource Server. Si tenés varios servicios validando el mismo JWT, la auditoría queda fragmentada o directamente ausente.
El caso tipico que uso para explicarlo:
Pensá en un usuario que reporta que le comprometieron la cuenta. Con sesiones con estado en Redis, la respuesta es inmediata:
# Invalidar todas las sesiones activas del usuario — respuesta inmediata
redis-cli DEL "session:usuario:abc123"
# O con un patrón si tenés múltiples sesiones por usuario
redis-cli --scan --pattern "session:usuario:abc123:*" | xargs redis-cli DELCon JWT stateless puro, la respuesta es: "esperamos que expiren." O implementás una blocklist, que es agregar estado — exactamente lo que querías evitar.
Lo que sí funciona bien con JWT stateless:
- Tokens de corta vida (minutos, no horas) con refresh tokens de larga vida y revocación del refresh.
- APIs internas entre servicios donde los tokens no representan sesiones de usuario.
- Contextos donde la latencia de introspección es prohibitiva y el riesgo de compromiso es bajo.
Matriz de decisión: cuándo usar cada enfoque
Antes de elegir, respondé estas preguntas. Son las que yo uso como filtro en cualquier diseño de sistema de identidad:
| Criterio | Stateless JWT | Con estado (sesión / token store) |
|---|---|---|
| ¿Necesitás revocar tokens individuales inmediatamente? | ❌ No sin blocklist | ✅ Sí |
| ¿Tenés requisitos de auditoría por sesión? | ❌ Complejo | ✅ Natural |
| ¿Los tokens representan sesiones de usuario final? | ⚠️ Cuidado con exp largo | ✅ Mejor fit |
| ¿Son tokens máquina-a-máquina de corta vida? | ✅ Ideal | ⚠️ Overhead innecesario |
| ¿Escala horizontal sin coordinación es crítica? | ✅ Ventaja real | ⚠️ Requiere store compartido |
| ¿Tenés latencia disponible por request para introspección? | — | ✅ Necesario |
El checklist de alarma para JWT stateless:
[ ] Expiración mayor a 30 minutos en tokens de acceso de usuarios finales
[ ] Sin mecanismo de revocación documentado
[ ] Operaciones sensibles autorizadas solo por el token (sin segunda validación)
[ ] Auditoría de sesiones requerida por regulación o política interna
[ ] Múltiples Resource Servers sin store compartido para blocklist
Si marcás dos o más, stateless puro probablemente no sea la arquitectura correcta para ese sistema.
El patrón que más uso en práctica: JWT de corta vida (15 minutos) + refresh token opaco con estado en Redis. El access token es stateless para validación rápida en cada request. El refresh token es stateful y revocable. La ventana de compromiso queda acotada a los 15 minutos del access token — tiempo razonable para la mayoría de los escenarios.
// Configuración típica de token en un Authorization Server con Spring Security
// Access token corto, refresh token revocable almacenado en Redis
@Bean
public TokenSettings tokenSettings() {
return TokenSettings.builder()
// Ventana corta para stateless — revocación máxima 15min
.accessTokenTimeToLive(Duration.ofMinutes(15))
// Refresh token de larga vida, revocable en Redis
.refreshTokenTimeToLive(Duration.ofDays(7))
// Reutilización controlada: cada refresh rota el token
.reuseRefreshTokens(false)
.build();
}Este patrón aparece documentado en la especificación OAuth 2.0 (RFC 6749) como práctica recomendada para reducir la ventana de exposición sin sacrificar completamente el beneficio del stateless.
Errores comunes y gotchas que aparecen tarde
"El JWT tiene toda la info necesaria, no necesito más."
Esta frase se convierte en problema cuando esa "info necesaria" cambia antes de que el token expire. Roles del usuario actualizados, cuenta suspendida, cambio de organización — con JWT stateless, la info en el token puede estar desactualizada durante toda su ventana de vida.
Confundir stateless con simple.
Implementar JWT stateless correctamente en un sistema de identidad requiere rotación de claves, JWKS endpoint, validación de claims, manejo de clock skew y gestión de refresh tokens. No es menos código que una sesión bien implementada; es código diferente con distintos puntos de falla.
Blocklist sin TTL.
Si agregás una blocklist para revocación, asegurate de que los registros tengan TTL igual al tiempo de expiración del token. Una blocklist que crece indefinidamente es un memory leak lento. Redis con EXPIRE o similares resuelve esto con una línea:
# Agregar token a blocklist con TTL igual al tiempo restante de expiración
# Asumiendo que calculás los segundos restantes antes de agregar
redis-cli SET "blocklist:jti:${TOKEN_JTI}" "revoked" EX ${SEGUNDOS_HASTA_EXP}Ignorar el jti (JWT ID).
El claim jti definido en RFC 7519 es el identificador único del token. Es lo que necesitás para una blocklist eficiente. Si no lo estás emitiendo, revocar tokens individuales se vuelve mucho más complejo — tendrías que revocar por sub (usuario), que es más agresivo y puede afectar otras sesiones legítimas.
FAQ sobre JWT vs sesiones con estado en sistemas de identidad
¿JWT stateless es inseguro por naturaleza?
No. JWT stateless es inseguro cuando se usa en contextos donde el control de sesión activo es un requisito no negociable. El mecanismo en sí, firmado correctamente con algoritmos asimétricos (RS256, ES256), es sólido. El problema es la semántica de "este token es válido hasta que expire" en sistemas donde necesitás decir "este token ya no es válido" antes de ese momento.
¿Puedo tener lo mejor de ambos mundos?
Sí, con el patrón híbrido: access token JWT stateless de corta vida + refresh token opaco con estado. El costo es la complejidad adicional del flujo de refresh. Vale la pena en la mayoría de sistemas de identidad con usuarios finales.
¿Token introspection no resuelve todo?
Resuelve la revocación, sí. El costo es una llamada al Authorization Server en cada request de validación — latencia adicional que puede ser significativa dependiendo del volumen. Para microservicios internos de alta frecuencia, el costo puede no justificarse. Para endpoints de usuario final con menor frecuencia, suele ser aceptable.
¿Qué pasa con las cookies de sesión tradicionales vs JWT?
Son mecanismos distintos en capas distintas. JWT es un formato de token; las cookies son un mecanismo de transporte. Podés transportar JWT en una cookie httpOnly+Secure y obtener protección contra XSS mientras usás el formato JWT. El debate "JWT vs cookies" suele mezclar estas capas y confundir más de lo que aclara.
¿Spring Security soporta ambos enfoques?
Sí. Para stateless JWT usás el Resource Server con JWT decoder. Para introspección activa usás el soporte de opaque tokens con el endpoint de introspección. Para sesiones tradicionales, el soporte de HttpSession con Redis o JDBC está bien documentado en Spring Session.
¿La arquitectura que describís en el post sobre decisiones de arquitectura de identidad resuelve esto de raíz?
Las decisiones de arquitectura de identidad y la elección de JWT vs estado son ortogonales pero relacionadas. Una buena arquitectura de identidad debería forzar esta pregunta antes de emitir el primer token, no después de que el sistema esté en producción. Ese post cubre el "qué construir"; este cubre el "cómo validar lo que emitís".
Conclusión: el estado no es el enemigo, la ambigüedad sí
La industria tuvo un momento de fascinación con "stateless everywhere" que llevó a muchos sistemas de identidad a optimizar para el caso de escala horizontal antes de tener ningún problema de escala real. El resultado frecuente: sistemas que no pueden revocar tokens, no pueden auditar sesiones y no tienen respuesta operativa cuando algo se compromete.
Lo incómodo es que JWT stateless tiene ventajas genuinas. No las estoy descartando. Estoy diciendo que en sistemas de identidad — donde la pregunta "¿quién es este usuario y sigue siendo válido?" tiene consecuencias reales — el costo de la rigidez stateless aparece antes de lo que los tutoriales prometen.
Mi recomendación práctica: empezá con el patrón híbrido (access token corto + refresh token opaco revocable). Si el overhead del store es un problema real medido, investigá si podés reducir el TTL del access token antes de eliminar el estado del refresh. Stateless puro es una optimización para más adelante, no el punto de partida.
La pregunta incómoda que te dejo: si hoy alguien te reporta una cuenta comprometida, ¿cuánto tarda tu sistema en cortarle el acceso? Si la respuesta es "hasta que expire el token", ya sabés qué revisar primero — empezá por el RFC 7009 y fijate qué te falta implementar para revocación real. No es teoría: es el contrato que el ecosistema OAuth espera que implementes.
Lecturas relacionadas:
- Arquitectura backend de identidad digital: las decisiones que los tutoriales omiten
- Firma digital: formato, certificado y política de validación
- El benchmark que me hizo cambiar de opinión sobre Jakarta EE en 2026
Fuentes originales:
- OAuth 2.0 Token Revocation RFC 7009: https://datatracker.ietf.org/doc/html/rfc7009
- Spring Security OAuth2 Resource Server: https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/index.html
Artículos Relacionados
Cline en producción: el agente de código autónomo para VS Code que uso con restricciones deliberadas
Cline puede crear archivos, ejecutar comandos y abrir el browser de forma autónoma desde VS Code. Eso suena a productividad. También huele a riesgo si no sabés qué permisos le das antes de empezar. Mi tesis: el modelo mental importa más que la herramienta.
17 ago 2026 · 9′ · Tutoriales · TypeScript · LLM
Firma digital CAdES vs XAdES en Java: las diferencias que importan cuando tu CA te pide una y vos tenés la otra
CAdES y XAdES no son intercambiables aunque ambos sean "firma avanzada". La elección depende del tipo de documento, del perfil de confianza y de lo que la CA espera validar. Guía técnica con DSS y Java, con los errores que ya me hicieron perder tiempo antes de aprender a preguntar el formato primero.
12 ago 2026 · 9′ · Tutoriales · seguridad · certificados
Virtual Threads no te salva de un synchronized mal puesto
Virtual Threads resuelve el costo de crear miles de threads en la JVM. No resuelve el bloqueo cuando el código tiene synchronized o llamadas nativas bloqueantes. La JEP 444 lo dice, pero casi nadie lo lee hasta el final.
11 ago 2026 · 8′ · Tutoriales · concurrencia · spring-boot
Comentarios (0)
¿Qué pensás de esto?
Dejá tu comentario en 10 segundos.
Usamos tu login solo para mostrar tu nombre y avatar. Nada de spam.
Todavía nadie comentó. Sé el primero — tu opinión vale oro cuando somos pocos.