Gestión de sesiones y revocación: el talón de Aquiles de los tokens autocontenidos
La revocación inmediata de un JWT firmado es un problema que la criptografía no resuelve por sí sola. Gestionar sesiones en sistemas distribuidos exige combinar tiempos de vida cortos, rotación de refresh tokens, introspección y mecanismos de logout coordinados. Entender los trade-offs entre estado y autocontención define la postura real de seguridad de una aplicación.
Este artículo forma parte de la serie La evolución de la seguridad en aplicaciones modernas. Hay una pregunta incómoda que aparece tarde o temprano en cualquier revisión de seguridad: si detectamos que una cuenta está comprometida, ¿cuánto tiempo tardamos en cortar todas sus sesiones activas? La respuesta honesta, en muchos sistemas que usan JWT, es “hasta que expire el token”. Y eso no siempre es aceptable.
Por qué revocar es difícil
El atractivo de los tokens autocontenidos es también su problema. Un JWT firmado lleva todo lo necesario para que cualquier servicio lo valide sin consultar a nadie: claims, firma, expiración. Esto elimina el estado compartido, reduce la latencia y permite escalar horizontalmente sin replicar sesiones. El precio es que la misma propiedad que lo hace eficiente lo hace difícil de invalidar. Si el token es válido criptográficamente y no ha expirado, cualquier resource server lo aceptará.
Las sesiones tradicionales basadas en cookies con ID de sesión opaco no tenían este problema. El servidor consulta su store, decide que la sesión está revocada y deniega. Pero esa arquitectura no escala tan bien y acopla al cliente a un backend específico.
La ingeniería real consiste en admitir que no hay una respuesta única y construir un sistema donde la ventana de validez residual sea tolerable para el modelo de amenazas.
Access tokens de vida corta
La primera defensa es reducir la ventana. RFC 6749 (The OAuth 2.0 Authorization Framework) no obliga a ningún tiempo de vida, pero RFC 6819 (OAuth 2.0 Threat Model and Security Considerations) recomienda access tokens de minutos, no horas. En la práctica, valores entre 5 y 15 minutos son el estándar de facto para aplicaciones web modernas.
Un access token de 10 minutos significa que, en el peor caso, un atacante con un token robado tiene 10 minutos de ventana antes de que el token expire por sí solo y tenga que refrescarlo. Si el refresh token ha sido revocado, la cadena se corta ahí.
Esto traslada la carga a los refresh tokens, que pasan a ser el objetivo de alto valor. Un refresh token robado es mucho más peligroso que un access token porque permite emitir nuevos access tokens indefinidamente.
Refresh token rotation y detección de replay
RFC 6819 sección 5.2.2.3 describe el patrón: cada vez que un cliente usa un refresh token para obtener un access token nuevo, el AS emite también un refresh token nuevo e invalida el anterior. Esto se llama refresh token rotation.
La propiedad interesante es que permite detectar reuso. Si el AS recibe un refresh token que ya había sido invalidado, sabe que algo va mal: o bien el cliente legítimo y el atacante tienen ambos una copia, o bien el token fue robado después de la última rotación. La respuesta correcta es invalidar toda la familia de refresh tokens descendientes de esa sesión y forzar re-autenticación.
RFC 8252 (OAuth 2.0 for Native Apps) recomienda explícitamente rotation para clientes públicos que no pueden guardar secretos. En SPAs y apps móviles es, en la práctica, obligatorio.
Revocación explícita
RFC 7009 (OAuth 2.0 Token Revocation) define el endpoint /revoke que permite a un cliente invalidar un token explícitamente. Es lo que debería llamarse en un logout del lado del cliente. En la práctica, muchos clientes lo olvidan y dejan refresh tokens válidos flotando incluso después de que el usuario cierre sesión.
Para que la revocación tenga efecto en access tokens autocontenidos, el resource server tiene que consultar al AS. Ahí entra RFC 7662 (OAuth 2.0 Token Introspection), que define el endpoint /introspect: el RS envía el token y recibe los claims más un flag active. Si el token ha sido revocado, active es falso.
El problema es que la introspección anula la ventaja del JWT autocontenido. Si cada request va a introspectar, vuelves al modelo stateful con un hop de red adicional. Hay dos salidas habituales. La primera es usar tokens opacos en lugar de JWT para servicios donde revocación inmediata importa más que latencia. La segunda es mantener JWT pero consultar introspección solo para operaciones sensibles (cambios de contraseña, pagos, administración), aceptando la ventana residual en lecturas rutinarias.
Denylist corta
Un compromiso pragmático es mantener una denylist de tokens revocados con TTL igual a la vida máxima del access token. Como los access tokens son cortos (10 minutos), la denylist también lo es, y cabe en Redis sin problema.
Cada RS consulta la denylist antes de aceptar un JWT. Si el token está listado, rechaza. Si no, confía en la firma. Esto añade un lookup por request, pero contra un store en memoria el coste es marginal comparado con la seguridad que aporta.
Una denylist de access tokens solo funciona si puedes identificar cada token unívocamente. Usa el claim
jti(JWT ID) y asegúrate de que tu AS lo emite siempre.
Session management en OIDC
A nivel de sesión del IdP (Identity Provider), OIDC añade tres mecanismos complementarios. OpenID Connect Session Management 1.0 define el check_session_iframe, un iframe oculto que consulta periódicamente al IdP si la sesión sigue activa. OpenID Connect Front-Channel Logout 1.0 notifica a las RPs (Relying Parties) mediante redirecciones o iframes cuando el usuario hace logout en el IdP. OpenID Connect Back-Channel Logout 1.0 hace lo mismo vía llamadas HTTP directas del IdP a las RPs, usando un JWT logout_token.
El back-channel es el más robusto porque no depende del navegador del usuario (que puede estar cerrado cuando ocurre la revocación administrativa). El front-channel es más simple pero frágil. En una arquitectura con muchas RPs, conviene soportar back-channel y mantener un mapa de sesiones activas por usuario para poder propagar el logout.
Device binding como alternativa
Una línea de defensa distinta es hacer que el token no sea bearer puro. Si el token está vinculado a una clave que solo posee el cliente legítimo, robarlo no basta para usarlo.
RFC 9449 (OAuth 2.0 Demonstrating Proof of Possession - DPoP) añade una prueba firmada por una clave del cliente en cada request. El AS emite un access token con el thumbprint de esa clave, y el RS verifica que cada request incluye una firma DPoP que coincide. Si un atacante roba el token pero no la clave privada, el token es inútil.
mTLS-bound tokens, definidos en RFC 8705 (OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens), usan el certificado de cliente TLS como vínculo. Ambos mecanismos reducen la urgencia de la revocación: un token robado sin la credencial de proof-of-possession no se puede usar, lo que permite tiempos de vida más largos con menos riesgo.
Trade-offs y cuándo inclinarse a cada lado
La pregunta no es “autocontenido o stateful”, es “cuánta ventana residual tolera mi modelo de amenazas”. Si el peor escenario aceptable de un token comprometido son 10 minutos, JWT con rotation y vidas cortas basta. Si la respuesta es “cero minutos” (por ejemplo, banca online, gestión sanitaria, cuentas comprometidas conocidas), necesitas introspección o tokens opacos en los flujos críticos.
Los factores que inclinan hacia revocación inmediata son varios. Cumplimiento normativo es el más duro: el derecho al borrado del RGPD (Reglamento General de Protección de Datos) y ciertos requisitos de HIPAA exigen que, cuando se revoca el consentimiento o se bloquea una cuenta, el acceso cese de forma efectiva y demostrable. Un argumento de “el token expira en 15 minutos” no convence a un DPO.
También inclinan hacia stateful los sectores con threat intelligence activa: detectar un token filtrado y cortarlo en segundos es un requisito operativo, no un nice-to-have.
Si tu organización opera en sectores regulados y tu AS no soporta revocación efectiva, estás asumiendo un riesgo de cumplimiento aunque el diseño técnico sea correcto según OAuth.
Cuándo no complicarse
Para APIs internas entre microservicios del mismo dominio de confianza, con mTLS y redes privadas, la revocación inmediata rara vez es crítica. El modelo de amenazas es diferente: el atacante que consigue un token interno probablemente ya tiene acceso a la red y otros vectores más peligrosos. Ahí, JWT autocontenidos con vidas cortas son la opción pragmática.
Para aplicaciones de usuario final con datos sensibles, inversión en mecanismos de revocación paga. No siempre hace falta introspección en todas las requests: una combinación de rotation, denylist corta, back-channel logout y DPoP cubre la mayoría de los escenarios con coste operativo manejable.
Encaje arquitectónico
Una postura realista combina varias capas. Access tokens JWT de 10 minutos con claim jti. Refresh tokens con rotation y detección de replay. Denylist de jti revocados en Redis con TTL de 10 minutos. Back-channel logout propagado a todas las RPs. DPoP o mTLS-bound tokens para flujos de alto valor. Introspección solo en operaciones administrativas y de gestión de cuenta.
Este stack no es trivial de implementar, pero es lo que distingue una gestión de sesiones madura de una configuración por defecto.
Artículos relacionados en esta serie
Referencias
- RFC 6749 (octubre 2012). The OAuth 2.0 Authorization Framework.
- RFC 6819 (enero 2013). OAuth 2.0 Threat Model and Security Considerations.
- RFC 7009 (agosto 2013). OAuth 2.0 Token Revocation.
- RFC 7662 (octubre 2015). OAuth 2.0 Token Introspection.
- RFC 8252 (octubre 2017). OAuth 2.0 for Native Apps.
- RFC 8705 (febrero 2020). OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens.
- RFC 9449 (septiembre 2023). OAuth 2.0 Demonstrating Proof of Possession (DPoP).
- OpenID Foundation (2014-2022). OpenID Connect Session Management, Front-Channel Logout y Back-Channel Logout 1.0.