Cierra la sesión de un usuario en una app basada en sesiones y el servidor lo olvida en la siguiente petición. Cierra la sesión en una app basada en JWT y, a menos que hayas montado un mecanismo extra, el token que tiene en la mano sigue siendo válido hasta que expira. Esa diferencia es la decisión real detrás de “JWT vs sesión”. El resto se deriva de ahí.

Qué guarda realmente una sesión

Una sesión mantiene el estado del usuario en el servidor y al navegador solo le pasa una clave de búsqueda.

  1. El usuario inicia sesión con una contraseña.
  2. El servidor crea un registro de sesión (ID de usuario, roles, lo que necesite) y lo guarda en memoria, en una base de datos o en Redis, indexado con un ID de sesión aleatorio.
  3. El servidor envía ese ID al navegador como cookie, marcada HttpOnly para que el JavaScript del cliente no pueda leerla.
  4. En cada petición, el navegador devuelve la cookie automáticamente. El servidor busca el ID en el almacén y recupera los datos de la sesión.

La cookie no lleva ninguna información del usuario, solo una cadena opaca que el servidor resuelve en cada petición. Borra el registro en el almacén y la sesión desaparece en la siguiente petición, en todos los dispositivos.

Qué hay dentro de un JWT

Un JSON Web Token se salta la búsqueda metiendo el estado directamente en el token, firmado para que no se pueda manipular.

Un JWT son tres partes codificadas en base64url unidas por puntos: header.payload.signature. El payload es un objeto JSON de claims. La RFC 7519 define una lista corta de claims registrados:

ClaimNombreSignificado
issIssuerQuién emitió el token
subSubjectDe quién trata el token
audAudiencePara quién está pensado el token
expExpirationRechaza el token después de este momento
nbfNot beforeRechaza el token antes de este momento
iatIssued atCuándo se creó el token
jtiJWT IDID único, útil para revocación y control de repetición

El servidor firma el header y el payload con un secret (HMAC) o una clave privada (RSA/ECDSA) y envía el resultado al cliente. Cualquier servidor que tenga la clave correspondiente, o solo la clave pública en la firma asimétrica, puede verificar la firma y confiar en los claims. Sin ida y vuelta a la base de datos, sin almacén de sesiones compartido. En un sistema con varios servicios, cada uno puede verificar un token por su cuenta.

La otra cara: quien tenga el token puede leer el payload. Base64 es una codificación, no un cifrado. Nunca metas una contraseña, un secret o datos personales que no imprimirías en un log dentro del payload de un JWT.

Cómo configurar la autenticación por sesión en Express

Una configuración típica en Node usa express-session como middleware y connect-redis para que las sesiones sobrevivan a un reinicio del servidor y funcionen en varias instancias. Si Redis es nuevo para ti, cómo funciona el caching con Redis cubre lo básico para ponerlo en marcha.

1
npm install express express-session connect-redis redis
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
const express = require("express");
const session = require("express-session");
const { RedisStore } = require("connect-redis");
const { createClient } = require("redis");

const redisClient = createClient({ url: "redis://localhost:6379" });
redisClient.connect().catch(console.error);

const app = express();
app.use(express.json());

app.use(
  session({
    store: new RedisStore({ client: redisClient, prefix: "sess:" }),
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    cookie: {
      httpOnly: true,
      secure: true,
      sameSite: "lax",
      maxAge: 1000 * 60 * 60 * 2, // 2 horas
    },
  })
);

app.post("/login", async (req, res) => {
  const user = await authenticate(req.body.email, req.body.password);
  req.session.userId = user.id;
  res.sendStatus(204);
});

app.get("/me", (req, res) => {
  if (!req.session.userId) return res.sendStatus(401);
  res.json({ userId: req.session.userId });
});

app.post("/logout", (req, res) => {
  req.session.destroy(() => res.sendStatus(204));
});

app.listen(3000);

req.session.destroy() elimina la entrada en Redis. La siguiente petición con esa cookie no encuentra nada y se trata como sesión cerrada. Sin lista negra de tokens, nada que retocar después.

Cómo configurar la autenticación JWT en Node

El mismo flujo con jsonwebtoken, emitiendo un access token de vida corta y un refresh token separado:

1
npm install express jsonwebtoken
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
const crypto = require("crypto");
const express = require("express");
const jwt = require("jsonwebtoken");

const app = express();
app.use(express.json());

function issueTokens(user) {
  const accessToken = jwt.sign(
    { sub: user.id, role: user.role },
    process.env.JWT_ACCESS_SECRET,
    { expiresIn: "15m", algorithm: "HS256" }
  );
  const refreshToken = jwt.sign(
    { sub: user.id, jti: crypto.randomUUID() },
    process.env.JWT_REFRESH_SECRET,
    { expiresIn: "7d", algorithm: "HS256" }
  );
  return { accessToken, refreshToken };
}

function requireAuth(req, res, next) {
  const header = req.headers.authorization || "";
  const token = header.startsWith("Bearer ") ? header.slice(7) : null;
  if (!token) return res.sendStatus(401);

  try {
    req.user = jwt.verify(token, process.env.JWT_ACCESS_SECRET, {
      algorithms: ["HS256"],
    });
    next();
  } catch (err) {
    return res.sendStatus(401);
  }
}

app.post("/login", async (req, res) => {
  const user = await authenticate(req.body.email, req.body.password);
  res.json(issueTokens(user));
});

app.get("/me", requireAuth, (req, res) => {
  res.json({ userId: req.user.sub, role: req.user.role });
});

app.listen(3000);

jwt.verify comprueba la firma y los claims exp/nbf en una sola llamada y lanza una excepción si alguno falla. Esa es toda la comprobación de autenticación, sin ningún almacén de por medio.

La opción algorithms: ["HS256"] importa. Sin ella, un token firmado con un algoritmo distinto del que esperabas podría pasar igualmente, y así es como varias librerías JWT acabaron con vulnerabilidades de “alg confusion”. Fija en el código la lista de algoritmos aceptados al verificar y rechaza siempre alg: "none".

El refresh token es lo que hace vivible un access token de 15 minutos. El cliente lo conserva y, cuando el access token expira, lo cambia por un par nuevo. Guarda el jti del refresh token en el servidor para poder revocar esa cadena concreta sin tocar los access tokens que ya están en circulación.

Por qué los JWT son difíciles de revocar

Una sesión es un registro en un almacén: lo borras y el usuario queda desconectado en todas partes. Un JWT es un claim autocontenido que cualquier verificador acepta hasta que exp diga lo contrario. Cerrar la sesión, banear a un usuario o bajarle el rol no cambia nada en un token ya emitido, porque el servidor que verifica no tiene ningún registro que borrar.

Las mitigaciones habituales:

  • Vida corta del access token. 15-30 minutos es el rango más usado, así un token robado o viejo tiene una ventana pequeña.
  • Rotación de refresh tokens. Cada refresh emite un refresh token nuevo e invalida el anterior; reutilizar uno viejo revoca toda la cadena. OAuth 2.1 exige esto (o tokens sender-constrained) para clientes públicos.
  • Una lista negra bien acotada. Para matar un token concreto antes de que expire, indexa la lista negra por jti más iss, no por la cadena del token. La OWASP JWT Cheat Sheet explica por qué poner ahí la cadena entera no es fiable. Una lista negra comprobada en cada petición también vuelve a meter una consulta a la base de datos en cada petición, justo el coste que los JWT debían quitar de en medio.

Si tu app termina haciendo una búsqueda en cada verificación de todos modos, una sesión habría sido el diseño más simple.

Dónde guardar un JWT en el navegador

En una app de navegador, pon el token en una cookie HttpOnly y Secure, no en localStorage. Cualquier script que corra en la página puede leer localStorage, así que un solo agujero XSS entrega todos los tokens que haya dentro. El coste: en cuanto el token viaja en una cookie, el navegador lo envía automáticamente, y vuelves a tener que defenderte del CSRF con SameSite y tokens CSRF, igual que con las sesiones. La comparación completa está en LocalStorage, SessionStorage y Cookies: Cuál Usar.

Cuándo usar sesiones y cuándo usar JWT

SituaciónUsa
Una sola app renderizada en el servidor, un solo backendSesiones: más simples, revocación inmediata
Logout instantáneo o cambios de permisos instantáneosSesiones, o JWT con una lista negra por jti
Varios servicios backend que verifican al usuario por su cuentaJWT: sin almacén de sesiones compartido al que recurrir
App móvil o clientes de API de tercerosJWT: los bearer tokens encajan con una API HTTP sin estado

Ninguno de los dos es más seguro en abstracto. Una sesión en Redis y un JWT de 15 minutos con refresh tokens rotados aguantan igual de bien. La diferencia es arquitectónica: cuántos servicios necesitan confiar en el claim y qué tan rápido necesitas poder retirarlo.

Si tu app tiene un solo backend, elige sesiones por defecto: la revocación viene gratis. Cambia a JWT el día que un segundo servicio necesite saber quién es el usuario sin llamar de vuelta al servicio que lo autenticó.

Artículos relacionados