Hay una pregunta que aparece en casi todos los proyectos y que suele responderse con una preferencia en lugar de con un criterio: ¿sesiones o tokens? La respuesta habitual es tokens, porque suena moderno y porque es lo que más aparece en los tutoriales recientes.
El problema de esa elección por defecto es que trae consigo una consecuencia que casi nunca se menciona hasta que hace falta: cerrar sesión deja de funcionar como la gente espera.
Tres cosas distintas que se mezclan
Antes de comparar conviene separar los términos, porque no están al mismo nivel y compararlos entre sí produce confusión.
Sesiones y JWT son dos formas de recordar que alguien ya inició sesión. Son alternativas reales entre sí.
OAuth y OIDC son otra cosa: un protocolo para que un tercero confirme la identidad de alguien. Es lo que hay detrás de los botones de entrar con Google. No compite con las anteriores, porque una vez que el tercero confirma quién eres, tu aplicación sigue necesitando recordarlo, y ahí vuelve la elección entre sesión o token.
Esa aclaración ahorra bastante ruido. La decisión técnica real es la primera; la segunda es una decisión de producto sobre si quieres gestionar contraseñas o delegarlo.
Dónde vive el estado, que es lo que decide todo
La diferencia entre los dos mecanismos se reduce a una sola cosa, y de ella salen todas las demás.
Con sesiones, el navegador guarda solo un identificador y el servidor guarda el resto. Cada petición implica consultar ese almacén, y por eso el servidor siempre sabe el estado actual de cada sesión.
Con JWT, el token lleva los datos dentro y va firmado. El servidor verifica la firma y confía en el contenido, sin consultar nada. Eso lo hace muy rápido y elimina el almacén, a cambio de que el servidor deje de tener la última palabra.
La consecuencia práctica es la que aparece en el diagrama: un token emitido es válido hasta que caduca, aunque entretanto el usuario cierre sesión, le cambien los permisos o se le expulse. No hay forma de desdecirse sin añadir precisamente el almacén que se quería evitar.
El problema del cierre de sesión
Conviene detenerse aquí porque es el punto donde más proyectos descubren tarde que eligieron mal.
Cuando alguien pulsa cerrar sesión con tokens, lo que ocurre es que el navegador borra el suyo. Si ese token fue copiado, interceptado o quedó guardado en otro sitio, sigue funcionando exactamente igual hasta la fecha de caducidad.
Lo mismo aplica a situaciones más incómodas: un empleado que deja la empresa, una cuenta comprometida que hay que bloquear, un usuario al que se le retira un permiso. En todos esos casos la expectativa es que el acceso termine ya, y con tokens puros termina cuando el token caduca.
La forma estándar de mitigarlo es emitir tokens de vida muy corta, de minutos, acompañados de un token de renovación de vida larga que sí se guarda en el servidor y sí se puede anular. Es una solución que funciona bien y que, conviene notarlo, reintroduce el almacén de estado que justificaba usar JWT.
Cuándo cada uno es la elección correcta
Con eso claro, la decisión deja de ser una cuestión de modernidad.
Las sesiones son la respuesta adecuada para una aplicación web normal, con su propio backend, servida desde un dominio. Son más simples, permiten revocar al instante y el costo de consultar el almacén es despreciable frente a lo que ya cuesta atender la petición.
Los JWT encajan cuando hay varios servicios que deben validar la identidad sin compartir un almacén común, cuando el emisor y el consumidor son sistemas distintos, o en comunicación entre servicios donde no hay ningún navegador involucrado.
Y hay un caso intermedio muy frecuente: una aplicación con interfaz separada del servidor, en el mismo dominio o en un subdominio. Ahí las sesiones funcionan perfectamente, y la creencia de que una interfaz moderna obliga a usar tokens es simplemente falsa.
Dónde se guarda, que importa más que el formato
Una decisión que se toma casi sin pensar y que tiene más impacto en la seguridad real que la elección anterior: dónde queda guardada la credencial en el navegador.
Si se guarda en el almacenamiento local, cualquier script que se ejecute en la página puede leerla. Eso convierte cualquier inyección de código en un robo de sesión completo, y es la razón por la que esta práctica, muy extendida, está desaconsejada.
La alternativa es una cookie con los atributos correctos, que el navegador envía sola y que el código de la página no puede leer. La contrapartida es que exige protegerse del envío automático desde otros sitios, que es un problema conocido y con solución conocida.
HttpOnly impide que el código de la página lea la cookie. Secure impide que viaje sin cifrar. SameSite limita que se envíe en peticiones que nacen en otro sitio.
Los tres se configuran en una línea y cierran tres categorías de ataque distintas.
Qué se delega al usar un proveedor externo
Ofrecer entrar con una cuenta de Google o similar resuelve de golpe varias cosas incómodas: el almacenamiento de contraseñas, la recuperación cuando se olvidan, la verificación del correo y, en muchos casos, el segundo factor.
Para un proyecto pequeño eso es una ventaja considerable, porque la gestión de contraseñas es una de esas piezas donde un error tiene consecuencias graves y donde no hay ninguna gloria en hacerlo uno mismo.
Lo que conviene tener presente es que delegar la identidad no delega la autorización. El proveedor confirma quién es la persona; qué puede hacer dentro de tu sistema lo sigues decidiendo tú, y ese es el terreno donde se producen la mayoría de los accesos indebidos.
También conviene pensar qué ocurre si alguien pierde el acceso a esa cuenta externa, porque entonces pierde el acceso a la tuya. Ofrecer más de una vía de entrada evita dejar a usuarios fuera por un motivo que no controlas.
El segundo factor, y por qué ya no es opcional
Una contraseña, por buena que sea, tiene un problema que no se puede arreglar con reglas de complejidad: si alguien la consigue, ya está dentro. Y conseguirla no suele requerir adivinarla.
La vía más común es la reutilización. Una persona usa la misma contraseña en varios sitios, uno de ellos sufre una filtración, y esas credenciales se prueban automáticamente en cientos de servicios más. El atacante no rompió nada de tu sistema: entró por la puerta con la llave correcta.
El segundo factor corta ese camino, porque saber la contraseña deja de ser suficiente. Y conviene saber que no todos valen lo mismo: el código enviado por mensaje de texto es el más débil de los habituales, porque el número de teléfono se puede desviar. Una aplicación de códigos temporales es claramente mejor y no cuesta más implementarla.
Para un proyecto pequeño, la decisión razonable es ofrecerlo desde el principio como opción y exigirlo para las cuentas con privilegios amplios. Si además usas un proveedor externo de identidad, es muy probable que ya venga resuelto sin trabajo adicional.
Y hay un detalle que se olvida con frecuencia: los códigos de recuperación. Si alguien pierde el teléfono y no hay otra vía, la única salida es un proceso manual de soporte, que es precisamente donde los atacantes suelen tener más éxito.
Proteger el inicio de sesión, no solo la contraseña
Hay un conjunto de medidas alrededor del formulario de entrada que importan tanto como el mecanismo elegido, y que suelen quedarse fuera de la implementación inicial.
- Limitar los intentos. Sin un tope, probar miles de combinaciones es cuestión de dejar un script corriendo. El límite debe contar por cuenta y no solo por dirección de origen, porque el origen se cambia con facilidad.
- No revelar si el correo existe. Un mensaje que distingue entre usuario inexistente y contraseña incorrecta regala una lista de cuentas válidas. La respuesta debe ser la misma en ambos casos, y también en la recuperación de contraseña.
- Renovar el identificador de sesión al entrar. Si se mantiene el mismo que tenía el visitante antes de identificarse, existe un ataque conocido donde alguien fija ese valor de antemano y hereda la sesión.
- Avisar de lo importante. Un correo cuando se cambia la contraseña o cuando entra un dispositivo nuevo convierte al propio usuario en el sistema de detección, que suele ser el más rápido.
Ninguna de estas medidas es complicada, y su ausencia explica buena parte de los accesos indebidos que no requirieron ninguna técnica sofisticada.
Lo que no hay que escribir uno mismo
Hay una parte de este terreno donde la recomendación es directa: no lo implementes, usa algo probado.
El almacenamiento de contraseñas es el ejemplo claro. Debe usarse una función de derivación diseñada para eso, que sea deliberadamente lenta y que incorpore un valor aleatorio por usuario. Escribir eso a mano, o usar un resumen rápido de los que sirven para verificar archivos, produce un sistema que parece funcionar y que cede en cuanto la base de datos se filtra.
Lo mismo aplica al flujo de recuperación de contraseña, que es la puerta trasera de cualquier sistema de autenticación y donde los errores son sutiles: enlaces que no caducan, que se pueden reutilizar, o que revelan si un correo está registrado.
Y aplica a la implementación del protocolo con proveedores externos, que tiene bastantes detalles cuya omisión abre agujeros reales.
Qué se le escapa a una IA con esto
Pedir un sistema de autenticación produce casi siempre una implementación con tokens, guardados en el almacenamiento local, sin caducidad corta y sin ninguna forma de revocar. Funciona a la primera y tiene los tres problemas de este artículo a la vez.
No es un error del modelo: es que ese es el patrón más repetido en el material del que aprendió, y nadie mencionó que hiciera falta cerrar sesión de verdad ni proteger la credencial de scripts.
Las frases que cambian la respuesta son concretas. Decir que se necesita poder revocar el acceso al instante descarta el token puro. Decir que la credencial debe quedar fuera del alcance del código de la página lleva a la cookie con sus atributos. Y decir que es una aplicación web normal con su backend suele llevar de vuelta a las sesiones, que era la respuesta simple desde el principio.
Conviene además pedir explícitamente qué ocurre al cerrar sesión, al cambiar la contraseña y al retirar un permiso. Si esas tres respuestas no son inmediatas, el diseño tiene un hueco que se va a notar el día que haya que expulsar a alguien.