Inicio / Artículos / Estrategia de tokens de actualización para sistemas de autenticación en Node.js

Estrategia de tokens de actualización para sistemas de autenticación en Node.js

Aprenda cómo diseñar, rotar, revocar y almacenar de forma segura los tokens de actualización en Node.js para que el robo de tokens y el cierre de sesión funcionen tal como se espera.

2681 palabras

Cuando un proyecto de Node.js implementa por primera vez un flujo de inicio de sesión, puede parecer que el trabajo está prácticamente terminado en poco tiempo.

El patrón parece lo suficientemente simple: el usuario envía su correo electrónico y contraseña, el servidor verifica las credenciales en la base de datos, y si coinciden, se firma un JWT que se devuelve al cliente. Ese token se incluye en cada solicitud posterior. A primera vista, esto parece un sistema de autenticación completo.

Pero una inspección más detallada revela una larga lista de preguntas a las que este flujo sencillo nunca responde:

  • ¿Qué sucede una vez que ese token expira?
  • ¿Se espera que el usuario inicie sesión de nuevo desde cero cada vez?
  • Si un token es robado, ¿por cuánto tiempo puede usarlo el atacante?
  • ¿Cómo funciona en la práctica el proceso de cerrar la sesión de un usuario?
  • ¿Existe alguna forma de revocar un token que ya haya sido emitido?
  • ¿Qué sucede si un token robado se reutiliza incluso después de que supuestamente se eliminó?
  • ¿Dónde debería almacenarse este token en el cliente?
  • Ninguna de estas preguntas se resuelve con la respuesta “firma un JWT y vévalo más tarde”. Por lo general, en ese punto es necesario ir más allá de los tutoriales de inicio de sesión copiados y pegados para entender realmente cómo funcionan los tokens de actualización.

    Por qué los tokens de acceso no duran mucho

    Un token de acceso de corta duración no es un valor por defecto que alguien olvidó cambiar; es una elección de diseño deliberada.

    Imagínese qué sucede si un token de acceso permanece válido durante 30 días y termina en manos equivocadas: el atacante ahora tiene acceso al cuenta de ese usuario durante todo un mes. Eso es un compromiso inaceptable. Por esta razón, los tokens de acceso suelen tener una validez de minutos en lugar de semanas; una vida útil corta reduce el alcance del daño en caso de que se filtre el token.

    No obstante, esa elección de diseño plantea su propio problema. Si un token vence cada 15 minutos, ¿el usuario tiene que volver a introducir su contraseña cada 15 minutos? Claramente eso no es viable. Los tokens de actualización existen específicamente para resolver ese problema.

    ¿Y qué función cumple realmente un token de actualización?

    A primera vista, un token de actualización parece ser solo otro token, pero su papel en el sistema es completamente diferente al de un token de acceso.

    El flujo general funciona de la siguiente manera:

    1. El usuario inicia sesión.
    2. El servidor devuelve tanto un token de acceso como un token de actualización.
    3. El token de acceso se utiliza para realizar solicitudes a la API.
    4. Con el tiempo, el token de acceso vence.
    5. El cliente envía el token de actualización a un endpoint especializado en actualizaciones.
    6. Si el servidor lo valida con éxito, emite un token de acceso completamente nuevo.

    El concepto que vale la pena internalizar aquí es que un token de actualización nunca debe usarse para llamar directamente a los puntos finales de tu API. Su única función es obtener un nuevo token de acceso. Una vez que se mantienen mentalmente separadas estas dos responsabilidades, el resto del diseño comienza a encajar perfectamente.

    Tokens de acceso vs. tokens de actualización, lado a lado

    Un token de acceso se utiliza para acceder a APIs protegidas, mientras que un token de actualización solo sirve para obtener un nuevo token de acceso. Un token de acceso tiene una vida útil corta; un token de actualización, en cambio, dura más tiempo. Un token de acceso se envía en casi cada solicitud, mientras que un token de actualización solo se utiliza ocasionalmente. Si se filtra un token de acceso, eso es problemático, pero si se filtra un token de actualización es aún peor, ya que puede usarse para seguir generando nuevos tokens de acceso. Un token de acceso se verifica en cada llamada a la API, mientras que un token de actualización solo se verifica mediante lógica especializada de actualización o sesión.

    Una división comúnmente mencionada es algo como 15 minutos de vida útil para el token de acceso y 7 días para el token de actualización, pero no hay nada mágico en esas cifras. Los valores adecuados dependen por completo de lo que pueda tolerar su aplicación y sus usuarios.

    Por qué los tokens de actualización merecen más respeto de lo que sugiere una primera mirada

    Un token de actualización puede mantener viva una sesión mientras siga siendo válido. Si un atacante consigue uno, no solo obtiene una ventana única de acceso: puede seguir generando tokens de acceso nuevos hasta que ese token de actualización expire o sea revocado.

    Esa realidad cambia la forma en que se debe tratar el token. No es solo otro dato de la aplicación; se comporta más como una llave física de la casa. Tratarlo de esa manera implica abordar realmente los siguientes aspectos:

    • vencimiento
    • revocación
    • rotación
  • dónde y cómo se almacena
  • cómo se transporta
  • detección de reutilización
  • qué es lo que realmente debe hacer el cierre de sesión
  • la gestión de sesiones en su conjunto
  • Por lo general, es aquí donde una configuración de JWT aparentemente sencilla comienza a mostrar sus puntos débiles.

    Rotación: evitar que un token permanezca para siempre

    Un concepto que transforma la forma en que se diseña este sistema es la rotación de tokens de actualización. En lugar de permitir que un único token de actualización se reutilice indefinidamente, el servidor emite uno nuevo cada vez que el actual se utiliza con éxito, y el antiguo se retira de inmediato.

    En lugar de tener una única credencial de larga duración en circulación para siempre, se obtiene una cadena de tokens, donde cada uno reemplaza al anterior:

    Se utiliza el Token A, lo que provoca la emisión del Token B mientras A es retirado. Luego se utiliza el Token B, lo que genera la emisión del Token C mientras B es retirado. Este patrón continúa indefinidamente, siendo válido únicamente el token más reciente de la cadena.

    Por qué esto realmente ayuda

    Considere un escenario en el que un atacante logra obtener una copia del Token A mientras el usuario legítimo aún lo posee.

    Si el usuario real lo utiliza primero, el Token A queda marcado como utilizado y, en efecto, inutilizable, y se emite el Token B en su lugar. Ahora, cuando el atacante intenta utilizar ese mismo Token A más tarde, el servidor lo reconoce como un token que ya ha sido consumido, lo cual es una clara señal de alerta. Dependiendo de cuán estrictamente esté configurado el sistema, esa detección puede provocar la revocación de toda la cadena de tokens vinculados a esa sesión, y no solo del único que fue comprometido.

    Esto te coloca en una posición mucho más fuerte que en un escenario donde quien obtenga el token primero gana efectivamente el control de forma permanente.

    Revocación, porque cerrar la sesión debería tener realmente algún significado

    A menudo se describe a los JWT como sin estado, y desde un punto de vista técnico eso es cierto. Pero cualquier sistema del mundo real necesita al menos algo de estado, y cerrar la sesión es el lugar más claro donde se manifiesta esa necesidad.

    Si un usuario hace clic en cerrar sesión y lo único que ocurre es que el token se elimina del frontend, el propio token sigue siendo perfectamente válido hasta que expire por sí solo. El servidor no tiene conocimiento de que el usuario se haya “cerrado la sesión” en ningún sentido significativo, por lo que seguirá aceptando ese mismo token si se presenta nuevamente antes de su vencimiento.

    Los tokens de actualización ofrecen un mecanismo real para rastrear y finalizar sesiones desde el lado del servidor. Antes de un evento de cierre de sesión, el token de actualización de la sesión se encuentra en estado activo. Una vez que ocurre el cierre de sesión, ese token debe marcarse como revocado, y cualquier intento posterior de usarlo para actualizar la sesión debería fallar completamente. Eso es lo que realmente significa un cierre de sesión, en lugar de uno que sea meramente simbólico.

    El cierre de sesión debe realizarse en el backend, no solo en el frontend

    La versión simplificada del cierre de sesión que a menudo implementan los principiantes consiste simplemente en eliminar el token del almacenamiento local o de dondequiera que lo guarde el cliente, y luego continuar.

    Un flujo de cierre de sesión más completo y preciso suele seguir una secuencia similar a esta:

    1. El cliente envía una solicitud POST /auth/logout
    2. El servidor identifica a qué sesión corresponde esa solicitud
  • El servidor revoca el token de actualización o la sesión asociada a él
  • Solo entonces el cliente elimina su propio estado local
  • El punto clave que merece destacarse aquí es que el cierre de sesión es fundamentalmente un evento de seguridad del lado del servidor. Si su implementación de cierre de sesión solo afecta lo que está almacenado en el cliente, en realidad no ha cerrado la sesión del usuario; simplemente ha hecho que su interfaz olvide que ese usuario existía.

    Decidir dónde se debe almacenar realmente este token

    Las opciones de almacenamiento tienen más importancia de lo que parecen a primera vista. Para aplicaciones basadas en navegadores, un enfoque común es colocar el token de actualización dentro de una cookie configurada con algunos atributos específicos:

    • HttpOnly, que impide que el JavaScript del lado del cliente lo lea directamente
    • Secure, que restringe su transmisión únicamente a través de HTTPS
  • SameSite, que reduce la exposición a ciertas categorías de ataques entre sitios
  • No obstante, ninguno de estos ajustes hace que una cookie sea automáticamente invulnerable. Todavía es necesario tener en cuenta las protecciones contra CSRF, cómo se definen el dominio y la ruta, cómo se gestiona la expiración de las sesiones y cómo el cierre de sesión interactúa con todos estos elementos. No existe un patrón de almacenamiento único que se pueda copiar de un tutorial y confiar en él sin adaptarlo a la arquitectura de su propio sistema.

    ¿Qué ocurre si el token de actualización se filtra de todos modos?

    Este escenario específico es el que hizo evidente la importancia de la rotación.

    Si tanto el usuario legítimo como un atacante terminan por poseer el mismo token de actualización, y su sistema permite que dicho token sea reutilizado sin límites, realmente no existe forma de distinguir entre las dos partes. Desde la perspectiva del servidor, ambas solicitudes parecen igualmente legítimas.

    Con la rotación activada, el primer uso de ese token lo retira inmediatamente. Por lo tanto, si el mismo token se presenta una segunda vez, eso constituye un comportamiento anormal que funciona como señal. Un sistema bien diseñado puede considerar ese uso repetido como una alerta de riesgo y reaccionar en consecuencia, ya sea revocando la sesión, marcando la cuenta o aplicando cualquier política que se ajuste a su nivel de tolerancia al riesgo.

    Esta es, en realidad, la razón fundamental por la cual los tokens de actualización deben considerarse un desafío en el diseño de seguridad, y no algo que se resuelva simplemente generando otro JWT.

    También los tokens de actualización deben vencerse

    Es fácil pasar por alto este aspecto, pero los tokens de actualización tampoco deberían ser permanentes. Sin una fecha de vencimiento, un token de actualización robado se convierte efectivamente en una puerta trasera que nunca se cierra.

    Un punto de partida común es algo como 15 minutos para los tokens de acceso y 7 días para los tokens de actualización, aunque las cifras exactas que elijas deben reflejar tu propia tolerancia al riesgo y no las que aparezcan en el primer tutorial que encuentres.

    Vale la pena hacer una distinción clara en la mente entre la vida útil de los tokens y la vida útil de las sesiones, ya que no se trata del mismo concepto. Una sesión puede permanecer activa durante mucho tiempo gracias a ciclos repetidos de rotación, aunque cada token individual solo dure un breve período.

    Errores que conviene evitar o tener en cuenta

    • Tokens de acceso que permanecen activos por demasiado tiempo, lo cual aumenta los riesgos en caso de que se expongan
    • Tokens de actualización sin fecha de vencimiento, lo que permite un acceso permanente
    • Omitir por completo la rotación de tokens, lo que dificulta enormemente detectar robos
    • No contar con mecanismos de revocación, lo que impide finalizar una sesión antes de su vencimiento natural
    • Tratar el cierre de sesión como algo que solo ocurre en la interfaz frontal
    • Ser descuidados con los datos confidenciales, permitiendo que los tokens terminen en registros, URLs o almacenamiento del lado del cliente donde no deberían estar
    • Ignorar la detección de reutilización, ya que rotar tokens sin verificar si se han reutilizado en realidad no brinda mucha protección

    Prueba real del flujo de actualización

    La autenticación merece la misma cobertura de pruebas que cualquier otro camino crítico en tu aplicación. Aquí hay una secuencia básica que vale la pena probar:

    1. Inicie sesión y confirme que recibe tanto un token de acceso como un token de actualización
    2. Llame a una ruta protegida con un token de acceso válido y confirme que recibe un 200 OK
    3. Llame a una ruta protegida con un token de acceso vencido y confirme que recibe un 401
    4. Llame al endpoint de actualización y confirme que recibe un nuevo token de acceso (además de un nuevo token de actualización, si está habilitada la rotación)
    5. Intente reutilizar el antiguo token de actualización que ya fue rotado, y confirme que se rechaza
    6. Cierre la sesión y luego intente actualizarla usando esa sesión ya inactiva, y confirme que también se rechaza

    Encontrar problemas en esta etapa es mucho más económico que descubrirlos después de que la aplicación esté en funcionamiento.

    La imagen completa

    Login
      ↓
    Access Token + Refresh Token issued
      ↓
    API requests using Access Token
      ↓
    Access Token expires
      ↓
    Refresh Token sent to refresh endpoint
      ↓
    Server validates session
      ↓
    Refresh Token rotated
      ↓
    New Access Token issued
      ↓
    API requests continue
    

    Si la validación falla en cualquier momento de esa secuencia de actualización —el token está vencido, revocado o simplemente inválido— la respuesta es un 401, y el usuario debe iniciar sesión de nuevo desde cero. Esa separación entre “permiso de corta duración para llamar a la API” y “permiso de mayor duración para permanecer conectado” es, en realidad, la idea central detrás de toda esta configuración, resumida en una sola oración.

    Lista de verificación para preproducción

    Diseño del token

    • ¿Los tokens de acceso realmente vencen rápidamente?
    • ¿Los tokens de actualización tienen su propio plazo de vencimiento?
    • ¿Están los tokens de actualización restringidos únicamente al endpoint de actualización, en lugar de poder usarse con rutas arbitrarias?
    • ¿Los payloads de los tokens evitan contener más información de la necesaria?

    Seguridad

    • ¿Es obligatorio HTTPS en todos los casos?
  • ¿Se almacenan los tokens de actualización en un lugar seguro?
  • ¿Se mantienen los secretos separados del código fuente?
  • ¿Se eliminan los tokens de los registros de actividad?
  • ¿Se aplica la protección CSRF donde sea relevante?
  • Gestión de sesiones

    • ¿Se puede revocar un token de actualización según sea necesario?
    • ¿La salida de sesión se realiza en el servidor, no solo en el cliente?
    • ¿Está realmente implementada la rotación de tokens?
    • ¿Se puede detectar cuando un token se reutiliza?

    Cobertura de pruebas

    • Un token de actualización válido funciona correctamente
    • Un token de actualización vencido falla
    • Un token de actualización revocado falla
    • Un token de actualización mal formado o inválido falla
    • Un token que ya fue rotado falla
    • La salida de sesión correcta termina la sesión adecuada

    En resumen

    La autenticación no se trata solo de confirmar la identidad de alguien en el momento en que inicia sesión. Se trata de decidir —y luego hacer cumplir— cuánto tiempo debe mantenerse esa confianza posteriormente.

    Los JWT simplifican la parte de “verificar quién eres”. Lo que no ofrecen gratuitamente es la gestión de sesiones, la revocación, un cierre de sesión adecuado, protección contra tokens robados, detección de reutilización, almacenamiento seguro o plazos razonables de vencimiento. Cada uno de estos aspectos es una elección deliberada que debes tomar tú mismo. Cuanto más grande y utilizada se vuelva tu aplicación, más importantes son esas decisiones.

    Qué viene a continuación

    Ahora que los tokens de acceso y de actualización finalmente tienen sentido, el siguiente área que merece ser explorada es la gestión de sesiones a mayor escala:

    • ¿Cómo se debe manejar a un usuario que permanece conectado en cinco dispositivos diferentes?
  • ¿Deberían los usuarios poder ver —y finalizar— sus propias sesiones activas?
  • ¿Es posible cerrar la sesión de alguien en un dispositivo sin terminar todas las sesiones que tenga?
  • ¿Cómo se ve una sesión “sospechosa” y cómo se marca?
  • ¿Cuál es la forma correcta de almacenar las familias de tokens de actualización?
  • ¿En qué momento un modelo de sesiones basado en base de datos tiene más sentido que mantenerse completamente sin estado con JWTs?
  • Escribir la ruta de inicio de sesión en sí podría requerir diez líneas de código. Construir un sistema de autenticación en el que realmente se pueda confiar exige mucho más esfuerzo que eso.

    Lecturas relacionadas

  • Por qué decodificar los bloques del buffer como texto daña las subidas de ficheros — Explica cómo tratar los datos binarios del buffer como texto UTF-8 corrompe silenciosamente los archivos subidos y muestra el manejo correcto a nivel de bytes para evitarlo.
  • Sesiones vs. JWTs: Elegir el modelo de autenticación adecuado en Node.js — Aprende cómo difieren realmente las sesiones y los JWTs en la autenticación de Node.js, dónde falla cada uno, y cómo elegir entre ellos sin arrepentimientos posteriormente.
  • Implementar tokens de acceso y refresh juntos en Node.js — Aprende cómo combinar tokens de acceso de corta duración con tokens de refresh rotativos en Node.js para equilibrar la seguridad y sesiones de usuario fluidas.