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 tokencon JWT y caducidad cortarefresh tokenconjtiy caducidad larga
Reglas de validación del JWT
comprobar
isscomprobar
audcomprobar
expfijar
algy no aceptar cambios raros
Dónde guardar cada token
access token: memoria o cookierefresh token: cookieHttpOnly+ registro en BD o Redis
Protección de rutas privadas
solo el
access tokenva enAuthorizationsi falla la verificación:
401
Control de sesión en servidor
guardar
jtienlazar tokens con
family_idmarcar estados como
activo,reemplazadoorevocado
Refresco con rotación
cada uso del
refresh tokengenera otro nuevoel 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=Strictrate limiting en
/refreshsecretos separados y fuera del repo

Flujo Seguro de JWT con Refresh Tokens en Node.js
Refresh Token Rotation and Reuse Detection in Node.js JWT Authentication

Comparación rápida
Elemento | Access token | Refresh token |
|---|---|---|
Uso | Autorizar API | Sacar otro access token |
Duración | Corta | Larga |
Envío | Cabecera | Cookie |
Estado en servidor | No suele llevar | Sí |
Revocación inmediata | No | Sí |
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.
expresshace de framework webjsonwebtokenfirma y verifica tokenscookie-parserdeja leer la cookieHttpOnlydel refresh tokendotenvcarga las variables de entornobcryptcomprueba contraseñas en el login
Crea un archivo .env en la raíz del proyecto y no lo subas al repositorio:
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:
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.
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.
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 | 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 |
|---|---|---|
| Identificador único del token | Permite revocar un token concreto sin afectar al resto |
| Referencia a la cuenta del usuario | Habilita el cierre de sesión en todos los dispositivos |
| Seguimiento de la cadena de rotación | Detecta reutilización; si se usa un token ya reemplazado, se revoca toda la familia |
| Estado del ciclo de vida (activo, revocado, reemplazado) | Identifica de inmediato si un token ya no es válido |
| Caducidad absoluta | Impide que el token se use indefinidamente |
| Última vez que se usó | Permite purgar sesiones inactivas automáticamente |
| Metadato de red | Detecta anomalías geográficas o ataques de credential stuffing |
| 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:
Recibe el refresh token desde la cookie
HttpOnly.Verifica el JWT con
jwt.verify().Consulta el registro persistente y confirma que el
jtiestá en estado activo; si no lo está, rechaza el refresco.Emite un nuevo access token y un nuevo refresh token.
Marca el
jtianterior 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 |
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:
Revocar el registro: elimina el registro persistente o márcalo como revocado.
Borrar la cookie: borra la cookie con
Set-CookieyMax-Age=0.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 |
| Elimina el token del almacenamiento |
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 | 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 |
| Bajo |
SameSite=Strict | CSRF | Atributo en la misma llamada a | Bajo |
TTL corto (15 min) | Ventana de abuso tras filtración |
| Medio |
Rotación de refresh token | Robo con uso diferido | Invalida el | Medio |
Rate limiting en /refresh | Fuerza bruta / DoS |
| 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.