Refresh tokens en Node.js con JWT

Seguridad Y Cumplimiento

Usa access y refresh tokens con rotación y revocación para cortar sesiones al instante y detectar reutilización.

Si usas JWT en Node.js, el flujo más seguro no es un solo token: son 2 tokens, rotación y control en servidor.

Yo lo resumiría así: uso un access token corto de 5 a 15 minutos para autorizar peticiones, y un refresh token largo de días o semanas para sacar uno nuevo sin pedir login otra vez. El punto clave no es solo emitirlos, sino poder cortar la sesión al momento, detectar reuso y cerrar el paso si un token se filtra.

Si tú quieres montar este esquema sin liarte, aquí están las piezas que no pueden faltar:

  • Login que emite 2 tokens

    • access token con JWT y caducidad corta

    • refresh token con jti y caducidad larga

  • Reglas de validación del JWT

    • comprobar iss

    • comprobar aud

    • comprobar exp

    • fijar alg y no aceptar cambios raros

  • Dónde guardar cada token

    • access token: memoria o cookie

    • refresh token: cookie HttpOnly + registro en BD o Redis

  • Protección de rutas privadas

    • solo el access token va en Authorization

    • si falla la verificación: 401

  • Control de sesión en servidor

    • guardar jti

    • enlazar tokens con family_id

    • marcar estados como activo, reemplazado o revocado

  • Refresco con rotación

    • cada uso del refresh token genera otro nuevo

    • el anterior queda invalidado

  • Detección de reutilización

    • si aparece un token ya reemplazado, yo trataría eso como posible robo

    • en ese caso, revocaría toda la familia de sesión

  • Logout

    • revocar el registro

    • borrar la cookie

    • esperar a que caduque el access token

  • Endurecimiento básico

    • HTTPS

    • cookie HttpOnly, Secure, SameSite=Strict

    • rate limiting en /refresh

    • secretos separados y fuera del repo

Flujo Seguro de JWT con Refresh Tokens en Node.js

Flujo Seguro de JWT con Refresh Tokens en Node.js

Refresh Token Rotation and Reuse Detection in Node.js JWT Authentication

Node.js

Comparación rápida

Elemento

Access token

Refresh token

Uso

Autorizar API

Sacar otro access token

Duración

Corta

Larga

Envío

Cabecera Authorization

Cookie HttpOnly

Estado en servidor

No suele llevar

Revocación inmediata

No

En pocas palabras: yo no confiaría en JWT de sesión si no va con refresh token, rotación y registro en servidor. Ese es el mínimo para bajar el tiempo de abuso, detectar reuso y dejar el logout casi al instante.

Configurar el proyecto Node.js y emitir ambos tokens

Aquí vas a ver cómo emitir los dos tokens y cómo controlar el acceso a rutas privadas.

Dependencias, configuración y secretos

Para montar este flujo necesitas cinco paquetes: express, jsonwebtoken, cookie-parser, dotenv y bcrypt.

  • express hace de framework web

  • jsonwebtoken firma y verifica tokens

  • cookie-parser deja leer la cookie HttpOnly del refresh token

  • dotenv carga las variables de entorno

  • bcrypt comprueba contraseñas en el login

npm install express jsonwebtoken cookie-parser dotenv bcrypt

Crea un archivo .env en la raíz del proyecto y no lo subas al repositorio:

ACCESS_TOKEN_SECRET=cadena_aleatoria_de_alta_entropia
REFRESH_TOKEN_SECRET=otra_cadena_distinta_y_segura
ACCESS_TOKEN_EXPIRY=15m
REFRESH_TOKEN_EXPIRY=7d

Usa secretos distintos y rótalos desde un gestor de secretos.

Endpoint de login: generar ambos tokens

Si las credenciales son correctas, emites los dos tokens:

const accessToken = jwt.sign(
  { sub: user.id },
  process.env.ACCESS_TOKEN_SECRET,
  { expiresIn: process.env.ACCESS_TOKEN_EXPIRY, issuer: 'mi-app', audience: 'mi-app-client' }
);

const refreshToken = jwt.sign(
  { sub: user.id, jti: crypto.randomUUID() },
  process.env.REFRESH_TOKEN_SECRET,
  { expiresIn: process.env.REFRESH_TOKEN_EXPIRY, issuer: 'mi-app', audience: 'mi-app-client' }
);

El jti único dentro del refresh token hace falta para la rotación y para detectar reutilización. Sin ese dato, no puedes separar un token ya gastado de uno que sigue siendo válido.

Piensa en el refresh token como una sesión renovable, no como una credencial para entrar a la API.

Ese refresh token se guarda como sesión del servidor y se envía en una cookie HttpOnly, Secure y SameSite=Strict. Mientras tanto, el access token vuelve en el cuerpo JSON para que el cliente lo mande en las cabeceras de autorización.

res.cookie('refreshToken', refreshToken, {
  httpOnly: true,
  secure: true,
  sameSite: 'Strict',
  maxAge: 7 * 24 * 60 * 60 * 1000
});
res.json({ accessToken });

Y ojo: emitir el refresh token no llega por sí solo. Luego tienes que poder invalidarlo desde el servidor. Eso se trata en la siguiente sección.

Con los tokens ya emitidos, toca blindar las rutas privadas con el access token.

Proteger rutas privadas con middleware de access token

Primero emites ambos tokens. Después, la API confía solo en el access token, que dura poco tiempo. Solo ese token viaja en Authorization; el refresh token se queda fuera de las rutas privadas.

El middleware de autenticación hace tres cosas muy claras: lee la cabecera Authorization, verifica el token y devuelve 401 si algo falla.

function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];
  if (!token) return res.sendStatus(401);

  jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, {
    issuer: 'mi-app',
    audience: 'mi-app-client'
  }, (err, payload) => {
    if (err) return res.sendStatus(401);
    req.user = payload;
    next();
  });
}

Si el token no pasa la verificación, responde con 401 y corta la petición. Valida siempre iss, aud, exp y alg; saltarte esas comprobaciones deja la verificación del token más floja.

Conviene mantener este middleware separado de la lógica de refresco. Así cada pieza hace su trabajo, y el código resulta más fácil de auditar y mantener.

El refresh token no autoriza peticiones: solo se usará para renovar la sesión en el endpoint de refresh.

Almacenar refresh tokens de forma segura y controlar su ciclo de vida

Después de emitir el refresh token, toca decidir dos cosas: dónde se guarda y qué registro te va a permitir revocarlo o rotarlo sin demora. El refresh token ya viaja en una cookie HttpOnly, pero eso solo cubre una parte del problema. Ahora necesitas definir cómo lo vas a persistir en el servidor y qué datos vas a guardar para invalidarlo en el acto cuando haga falta. Ese control es la base de la rotación, la detección de reutilización y el logout.

Opciones de almacenamiento: cookie HttpOnly, local storage o registros de sesión en servidor

La forma en que guardas el refresh token en el cliente cambia por completo su exposición a XSS y tu capacidad para cerrar sesiones de forma fiable.

Opción de almacenamiento

Exposición a XSS

Consideraciones CSRF

Complejidad

Caso de uso recomendado

Cookie HttpOnly

Baja (inaccesible desde JavaScript)

Mitigada con SameSite=Strict o Lax

Media

Aplicaciones web con cookies

Local Storage

Alta (accesible desde JavaScript)

Baja si el token se envía en cabeceras

Baja

No recomendado para refresh tokens

Sesión en servidor (Redis o base de datos)

Ninguna

Depende del canal de entrega; si el refresh viaja en cookie, aplica CSRF

Alta

Apps de alta seguridad con revocación inmediata

La opción recomendada es cookie HttpOnly en el cliente + registro en servidor. Es la combinación que mejor encaja cuando quieres control de sesiones de verdad. En cambio, local storage deja el token expuesto a XSS, así que para este flujo queda fuera.

Diseñar un registro de sesión para el refresh token

Cada sesión de refresh token debería tener su propio registro en base de datos o en Redis. Aquí hay una regla simple: no guardes el JWT completo. Guarda solo su jti y, si aplica, un hash.

La pieza más importante de ese registro es family_id. Piensa en él como el hilo que une toda la cadena de rotación. Si detectas que alguien ha reutilizado un token que ya fue reemplazado, family_id te permite revocar toda la familia de golpe. A partir de ahí, el resto de campos te da contexto y control sobre el ciclo de vida del token:

Campo

Propósito

Beneficio para seguridad y respuesta ante incidentes

token_id

Identificador único del token

Permite revocar un token concreto sin afectar al resto

user_id

Referencia a la cuenta del usuario

Habilita el cierre de sesión en todos los dispositivos

family_id

Seguimiento de la cadena de rotación

Detecta reutilización; si se usa un token ya reemplazado, se revoca toda la familia

status

Estado del ciclo de vida (activo, revocado, reemplazado)

Identifica de inmediato si un token ya no es válido

expires_at

Caducidad absoluta

Impide que el token se use indefinidamente

last_used_at

Última vez que se usó

Permite purgar sesiones inactivas automáticamente

ip_address

Metadato de red

Detecta anomalías geográficas o ataques de credential stuffing

user_agent

Metadato del dispositivo

Ayuda al usuario a identificar sus sesiones y detectar accesos no autorizados

Con este registro, el servidor puede rotar tokens, detectar usos duplicados y cerrar sesiones al instante.

Implementar refresco, rotación, detección de reutilización y cierre de sesión

Con jti, family_id y el estado de sesión ya definidos, toca montar el refresco, la rotación, la detección de reutilización y el logout.

Endpoint de refresco con rotación

/auth/refresh sigue este orden:

  1. Recibe el refresh token desde la cookie HttpOnly.

  2. Verifica el JWT con jwt.verify().

  3. Consulta el registro persistente y confirma que el jti está en estado activo; si no lo está, rechaza el refresco.

  4. Emite un nuevo access token y un nuevo refresh token.

  5. Marca el jti anterior como reemplazado. Al dejarlo así, ya no puede volver a aceptarse.

Con Redis, el TTL borra los registros caducados por sí solo.

Una vez que la rotación está activa, el siguiente paso es tratar cualquier segundo uso como una señal clara de compromiso.

Detectar reutilización del refresh token tras una filtración

Si alguien intenta usar un jti que ya fue marcado como reemplazado, el servidor sabe que algo falla. Esa reutilización señala un posible compromiso: el servidor debe revocar toda la family_id y cortar la sesión.

Escenario

Sin rotación

Con rotación

Token filtrado

El atacante puede usarlo hasta que caduque

El primer uso legítimo lo invalida; un segundo uso ya no funciona

Detección de replay

Difícil

Automática al detectar un jti ya reemplazado

La rotación no borra el riesgo de filtración, pero sí acorta mucho la ventana de exposición. Un refresh token filtrado solo sirve hasta que el cliente legítimo lo rota. A partir de ese momento, el atacante se queda fuera.

Cuando pasa eso, el servidor debe revocar toda la familia y cortar la sesión.

Cierre de sesión y revocación de sesión

El logout tiene tres pasos:

  1. Revocar el registro: elimina el registro persistente o márcalo como revocado.

  2. Borrar la cookie: borra la cookie con Set-Cookie y Max-Age=0.

  3. Esperar la caducidad del access token: el access token sigue vigente hasta expirar; por eso, el logout efectivo es casi inmediato.

Acción de logout

Implementación técnica

Resultado

Invalidar refresh token

Eliminar el registro persistente o marcarlo como revocado

Impide generar nuevos access tokens

Limpiar cookie del cliente

Set-Cookie con Max-Age=0

Elimina el token del almacenamiento HttpOnly del navegador

Caducidad del access token

Esperar a que el JWT de corta duración expire

El acceso queda completamente revocado

Revocar familia completa

Eliminar todos los registros asociados al mismo family_id

Cierra la sesión en todos los dispositivos simultáneamente

Si quieres un logout global, revoca todos los registros que compartan el mismo family_id.

Con el flujo ya cerrado, faltan las medidas que recortan el daño si un token acaba filtrándose.

Medidas de endurecimiento y checklist final de implementación

Con la rotación y la revocación ya definidas, estas medidas ayudan a limitar el daño si un token se escapa del sistema. Van de la mano con la rotación por jti y la revocación por family_id.

Controles que reducen el impacto de un token filtrado

La seguridad de verdad no sale de una sola pieza. Sale de varias capas trabajando juntas.

HTTPS obligatorio es el punto de partida. Usa siempre cookies HttpOnly, Secure y SameSite=Strict para recortar el riesgo de robo por XSS y CSRF. Si hoy guardas tokens en localStorage, conviene pasarlos a cookies HttpOnly.

Mantén el access token en 15 minutos o menos. Además, aplica rate limiting en /refresh para frenar el abuso del endpoint de rotación con express-rate-limit.

Valida iss, aud, exp y alg; un fallo en alg puede abrir la puerta a un bypass de autenticación. Actualiza dependencias y guarda los secretos fuera del repositorio.

Tabla resumen de mitigaciones

Resumen de los controles que más bajan el riesgo en producción.

Técnica

Riesgo que reduce

Implementación en Node.js

Coste operativo

Cookies HttpOnly

Robo de token por XSS

res.cookie('rt', val, { httpOnly: true })

Bajo

SameSite=Strict

CSRF

Atributo en la misma llamada a res.cookie()

Bajo

TTL corto (15 min)

Ventana de abuso tras filtración

expiresIn: '15m' en jsonwebtoken

Medio

Rotación de refresh token

Robo con uso diferido

Invalida el jti anterior en cada refresco

Medio

Rate limiting en /refresh

Fuerza bruta / DoS

express-rate-limit sobre la ruta

Bajo

Rotación de claves (JWKS)

Compromiso de clave de firma

JSON Web Key Sets con renovación periódica

Alto

Conclusión: el flujo mínimo seguro para producción

En producción, combina access tokens cortos, refresh tokens en cookies HttpOnly, sesión en servidor con jti y family_id, rotación en cada refresco y revocación inmediata si detectas reutilización o logout.

FAQs

¿Cuándo conviene usar Redis en lugar de una base de datos?

Conviene usar Redis cuando necesitas latencia muy baja y acceso rápido a datos efímeros o compartidos entre varios servidores, como sesiones o el estado de tokens. También viene bien para recortar consultas repetidas a SQL y para aplicar caducidad nativa sin montar lógica extra.

Si no necesitas acceder a esos datos en cada petición y lo que buscas es persistencia o relaciones complejas, suele encajar mejor una base de datos más clásica.

¿Cómo se gestiona la rotación de claves JWT sin romper sesiones?

La rotación de claves JWT se gestiona con cambios periódicos sin cortar la sesión del usuario. En la práctica, el servidor debe aceptar varias claves públicas al mismo tiempo durante ese periodo de cambio.

Así, los tokens firmados con la clave antigua siguen siendo válidos hasta que caducan, mientras que los nuevos se emiten con la clave ya actualizada. Además, conviene mantener una validación estricta tanto de la firma como de los claims.

¿Qué cambia si tengo varios dispositivos por usuario?

Debes gestionar los refresh tokens por separado en cada sesión abierta. En lugar de invalidar todos los accesos cuando el usuario cierra sesión o revoca un token, cada dispositivo debe tener su propio registro en la base de datos o en Redis.

Así, el usuario puede seguir con la sesión activa en un dispositivo y revocar el acceso solo en otro. Eso da un control más fino y una experiencia consistente.

Publicaciones de blog relacionadas