- JWT permite autenticación stateless en APIs Node.js, reduciendo la necesidad de sesiones en servidor y mejorando la escalabilidad.
- La combinación de contraseñas hash con bcrypt, middleware de verificación JWT y HTTPS refuerza la seguridad de los endpoints.
- El uso conjunto de access tokens de corta duración y refresh tokens ofrece sesiones largas con capacidad de revocación centralizada.
- Una buena gestión de claves, roles, expiraciones y auditoría convierte a JWT en una pieza clave de arquitecturas modernas basadas en microservicios.
Si trabajas con APIs en JavaScript, tarde o temprano vas a tener que pelearte con la autenticación. Proteger endpoints en una API de Node.js usando JWT se ha convertido casi en el estándar de facto: es ligero, escalable y encaja perfecto con arquitecturas modernas basadas en microservicios o SPAs.
En este artículo vamos a juntar todas las piezas y ver de forma práctica y profunda cómo implementar autenticación con JSON Web Tokens en una API REST hecha con Node.js y Express, empezando por los conceptos clave y terminando con patrones más avanzados como los refresh tokens, la protección de rutas con middleware y varias recomendaciones de seguridad que conviene tener muy presentes.
Qué es JWT y por qué es tan usado en APIs con Node.js
JSON Web Token (JWT) es un estándar abierto que define un formato compacto para transmitir información entre dos partes como un objeto JSON firmado digitalmente. Esa firma permite verificar que el mensaje no ha sido modificado y que procede de quien dice ser, sin necesidad de mantener estado en el servidor.
Un token JWT está formado por tres secciones separadas por puntos: header.payload.signature, normalmente codificadas en Base64, lo que da lugar a cadenas como estas (muy típicas cuando empiezas a trastear con jwt.io): una parte de cabecera, otra con los datos del usuario y una tercera con la firma que certifica la integridad del conjunto.
La primera parte, el Header, indica principalmente el tipo de token y el algoritmo de firma. Lo habitual en APIs de Node.js es encontrarse con algo como alg: HS256 y typ: JWT, es decir, un token JWT firmado con HMAC usando SHA-256. Esta cabecera se codifica en Base64 y se convierte en el primer segmento del token.
La segunda sección es el Payload, donde se incluye la información o «claims». Aquí se suele guardar el identificador del usuario, su nombre y, en muchos casos, roles y permisos básicos, además de campos estándar como exp (fecha de caducidad), iat (fecha de emisión) o sub (sujeto del token). Es crucial entender que todo lo que metas aquí, aunque vaya en Base64, no está cifrado, solo codificado.
La tercera parte es la Signature o firma. Se construye aplicando un algoritmo de tipo HMAC, por ejemplo HMAC-SHA256, sobre la concatenación de la cabecera y el payload codificados en Base64, junto con un secreto que solo conoce el servidor. Esta firma es la que permite comprobar que nadie ha manipulado el token por el camino.
El flujo típico en una API con Node.js es muy directo: el usuario se autentica con sus credenciales, el servidor verifica que son correctas y responde con un JWT firmado. A partir de ese momento, todas las peticiones a endpoints protegidos deben incluir el token (por ejemplo en la cabecera Authorization: Bearer <token>), y el backend lo validará en cada solicitud sin consultar una sesión en memoria o en base de datos.
Autenticación basada en token vs sesiones tradicionales
En los sistemas clásicos basados en sesión, el servidor almacena información de cada usuario autenticado (en memoria, Redis, MongoDB, etc.) y la relaciona con una cookie. Cada petición consulta ese estado para saber quién es el usuario. Es un enfoque que funciona, pero que introduce problemas de sobrecarga, escalabilidad y sincronización entre instancias.
Con JWT y la llamada autenticación sin estado (stateless), la cosa cambia: el servidor ya no guarda la sesión, sino que toda la información necesaria para identificar al usuario va dentro del token. Mientras la firma sea válida y el token no haya caducado, cualquier instancia del servicio puede atender la petición sin compartir sesiones.
Este diseño trae varias ventajas claras: mejor escalabilidad horizontal (no tienes que replicar sesiones), capacidad para que distintas aplicaciones (web, móvil, escritorio) consuman el mismo API y reducción de ciertas vulnerabilidades ligadas al manejo de sesión, como los ataques CSRF clásicos basados en cookies.
Dicho esto, usar JWT no es una varita mágica que arregle todo. Sigues necesitando HTTPS, una buena gestión de claves y controles de acceso sólidos. Además, hay que decidir bien qué metes en el payload: demasiada información o datos sensibles pueden volverse en tu contra si alguien intercepta o roba el token.
En entornos más elaborados, sobre todo cuando se introduce OAuth2 o integraciones con terceros, es habitual complementar el JWT con refresh tokens para renovar el acceso sin pedir credenciales cada dos por tres, manteniendo sesiones largas pero seguras.
En entornos más elaborados, sobre todo cuando se introduce OAuth2 o integraciones con terceros, es habitual complementar el JWT con refresh tokens para renovar el acceso sin pedir credenciales cada dos por tres, o usar proveedores de identidad como Firebase, manteniendo sesiones largas pero seguras.
Configuración inicial de un proyecto Node.js con Express y JWT
Para ver todo esto en acción vamos a sentar las bases de un proyecto sencillo con Node.js y Express. La idea es montar una pequeña API REST que permita registro, login y acceso a rutas protegidas mediante JWT, empezando incluso por un ejemplo simple con un array en memoria y, luego, enlazándolo con bases de datos como MongoDB.
En un directorio vacío, lo habitual es inicializar el proyecto y añadir las dependencias básicas. Vas a necesitar, como mínimo, Express para el servidor HTTP, jsonwebtoken para crear y verificar los tokens, bcryptjs para encriptar contraseñas y, muy recomendable, dotenv para gestionar variables de entorno como la clave secreta del token o la cadena de conexión a la base de datos.
Con eso ya tienes el esqueleto para crear un fichero principal, por ejemplo index.js o app.js, en el que levantar un servidor Express sencillo, habilitar parsing de JSON en el cuerpo de las peticiones y preparar las primeras rutas de prueba. Es un paso similar al típico «hola mundo», pero pensado para que pronto pueda manejar usuarios y tokens.
Es buena idea desde el principio definir la estructura mínima del proyecto: una carpeta routes para separar endpoints de autenticación y de negocio, otra carpeta models para los esquemas de usuarios si vas a usar Mongo/Mongoose, y quizá una carpeta middlewares donde colocar la lógica de validación de JWT y otros filtros.
En paralelo, conviene crear un fichero .env para almacenar la clave secreta de JWT, el puerto del servidor, la URI de la base de datos y otros parámetros. Gracias a dotenv, no tendrás que exponer estos datos sensibles en tu repositorio, lo que es básico cuando luego subes el código a GitHub o despliegas en un PaaS como Heroku.
Estructura básica del servidor Express y simulación de usuarios
Una vez montado el esqueleto, se suele empezar por algo muy simple: un array en memoria que simule una base de datos de usuarios. Aunque no es apto para producción, es perfecto para entender el flujo de registro, login y emisión de tokens sin complicarse de entrada con un sistema de persistencia real.
El primer endpoint clave es el de registro. Al recibir un nombre de usuario y una contraseña, el backend debe comprobar que ese usuario no exista ya y, sobre todo, encriptar la contraseña con bcryptjs antes de guardarla en el array o en la base de datos. Nunca, bajo ningún concepto, debería almacenarse una contraseña en texto plano. Para usuarios y administradores es recomendable usar herramientas de gestión de contraseñas seguras.
El hashing con bcrypt implica generar un «salt» y aplicar varias rondas de cálculo, lo que hace que romper la contraseña original sea muy costoso computacionalmente, incluso si la base de datos cae en manos equivocadas. De este modo, cuando más tarde el usuario intente iniciar sesión, se comparará la contraseña enviada con el hash almacenado usando bcrypt, sin revelar nunca el original.
El segundo endpoint fundamental es el de login. Cuando el cliente manda sus credenciales, el servidor busca al usuario en el array o en la colección correspondiente, y si lo encuentra, compara la contraseña enviada con el hash guardado. Si todo cuadra, se procede a generar el JWT usando la librería jsonwebtoken, creando un payload que suele incluir al menos el identificador de usuario y algunos datos básicos.
En este punto se suele firmar el token con una clave secreta definida en las variables de entorno y se le añade un tiempo de expiración, por ejemplo una hora. El servidor responde con un JSON que normalmente incluye el token y, en ocasiones, también información del usuario que puede ser útil para el frontend (nombre, email, rol, etc.).
Por último, toca proteger rutas. Para ello se crea un middleware de autenticación con JWT que lea el token de la cabecera (o de otra fuente como una cookie o el header x-auth-token), verifique la firma usando jsonwebtoken y, si es correcto, adjunte la información del usuario al objeto req antes de pasar a la siguiente función de la cadena.
Protección de rutas con middleware JWT en Express
La pieza que marca la diferencia en una API segura es el middleware reutilizable que valida tokens. La idea es muy simple: antes de llegar al controlador de una ruta privada, se ejecuta una función que inspecciona la petición, comprueba si hay token, lo verifica y solo entonces permite el acceso.
En la práctica, este middleware suele leer la cabecera Authorization buscando un patrón tipo Bearer <token>, o quizá un header personalizado como x-auth-token. Si no encuentra nada, responde con un 401 o 403 indicando que la petición no está autorizada o carece de credenciales válidas.
Si sí hay token, el siguiente paso es llamar a jwt.verify con el token y el secreto. Si la verificación falla por token mal formado, caducado o manipulado, se responde con otro 401/403. Si pasa, se obtiene el payload original y se puede guardar en req.user para que los controladores posteriores sepan quién es el usuario autenticado.
Con esto en la mano, cualquier ruta que quieras proteger se configura simplemente añadiendo el middleware en su definición. Por ejemplo, una ruta /dashboard o /clients solo devolverá información cuando el token sea válido. Si el token falta o es incorrecto, el servidor se niega a servir los datos, lo que es justo lo que se busca en un entorno seguro.
Esta misma estrategia se puede extender para la autorización, no solo la autenticación. Es decir, a partir de req.user puedes comprobar roles o permisos específicos antes de ejecutar la lógica de la ruta, dejando fuera a usuarios sin privilegios suficientes incluso si tienen un token legítimo.
En proyectos de cierta complejidad es habitual utilizar librerías como Passport y su estrategia JWT. Passport proporciona un middleware muy flexible con cientos de estrategias para distintos métodos de login (Google, Facebook, SAML, etc.), y en el caso de JWT permite centralizar la configuración de dónde llega el token y cómo se valida, simplificando la protección de múltiples endpoints con una sola línea de configuración por ruta.
Buenas prácticas de seguridad con JWT en Node.js
Implementar JWT es relativamente sencillo, pero hacerlo bien requiere seguir varias buenas prácticas de seguridad. La primera, y no negociable, es usar HTTPS en todo el tráfico. Base64 no cifra nada, así que si viajas sin TLS cualquiera podría interceptar el token y usarlo en tu contra.
También es vital controlar la caducidad de los tokens. Los access tokens deberían tener un tiempo de vida relativamente corto (por ejemplo, minutos u horas) para minimizar el daño en caso de robo. Dejar tokens válidos durante días o semanas sin rotación multiplica los riesgos en cualquier escenario de compromiso.
Donde almacenas el JWT en el cliente importa, y mucho. Una opción suele ser guardarlo en cookies con bandera httpOnly y, si es posible, secure, reduciendo la exposición frente a ataques XSS. Otra es mantenerlo en memoria o en almacenamiento local, pero en ese caso debes ser especialmente cuidadoso con el código del lado del cliente y cualquier librería que añadas.
En el lado del servidor, las claves con las que firmas los tokens deben protegerse adecuadamente mediante gestores de secretos (KMS, Vault, servicios gestionados en AWS o Azure, etc.). No es buena idea dejar las claves en texto plano en repositorios o en ficheros subidos al servidor sin ningún tipo de protección.
Finalmente, es recomendable incluir solo la información estrictamente necesaria en el payload. Datos extremadamente sensibles (como números de tarjeta o información médica) no deberían viajar dentro del JWT, ni siquiera cifrados, salvo que existan requisitos muy específicos y medidas adicionales como JWE bien configurado.
Refresh tokens: sesiones largas sin perder seguridad
Usar JWT de corta duración mejora mucho la seguridad, pero puede resultar una lata para el usuario si tiene que hacer login constantemente. Ahí entran los refresh tokens, que permiten renovar access tokens sin reenviar las credenciales originales y sin mantener sesiones tradicionales en el servidor.
El esquema es bastante directo: al autenticarse correctamente, el servidor devuelve dos tokens: un access token JWT con caducidad corta y un refresh token con vida más larga. El cliente usa el primero para llamar al API y, cuando expira, llama a un endpoint específico (por ejemplo /token) enviando el refresh token para obtener un nuevo access token.
En el backend, el refresh token se suele almacenar en alguna forma de persistencia (base de datos, Redis, etc.) junto con la información del usuario, fechas de creación y caducidad, e incluso un estado que indique si está activo o ha sido revocado. No es recomendable que el refresh token sea completamente autocontenido, porque entonces no podrías invalidarlo de forma centralizada.
Cuando llega una petición de renovación, el servidor comprueba que el refresh token existe, que está asociado al usuario correcto, que no está en una lista negra y que no ha expirado. Si todo está bien, genera un nuevo JWT con los datos del usuario y lo devuelve. De esta forma, el usuario puede seguir trabajando sin volver a introducir su contraseña.
Un punto importante es que debería existir un mecanismo para revocar o deshabilitar refresh tokens. Por ejemplo, si un usuario pierde un dispositivo o se sospecha que ha habido filtración, un administrador (o el propio usuario, según el diseño) debería poder invalidar el refresh token asociado a ese dispositivo para cortar el acceso sin necesidad de forzar el cierre de sesión de todos los demás.
Este enfoque es especialmente útil cuando una misma identidad se usa desde varios dispositivos. Si uno de ellos se ve comprometido, puedes anular solo su refresh token, mientras el resto siguen funcionando con normalidad. Combinado con access tokens de vida corta, la ventana de tiempo en la que un atacante puede abusar de un token robado se reduce considerablemente.
Diseño avanzado con JWT: roles, microservicios y gestión de claves
Más allá del ejemplo sencillo de login y rutas protegidas, en proyectos reales surgen retos adicionales. Una de las primeras extensiones es incluir roles y permisos en el payload del token, de manera que el backend pueda decidir si un usuario puede acceder a un recurso concreto o ejecutar una acción determinada sin tener que consultar la base de datos en cada petición.
Esto es especialmente potente en arquitecturas de microservicios, donde distintos servicios pueden verificar el mismo JWT sin necesidad de compartir sesiones. En este contexto, se puede optar por firmas simétricas (secreto compartido) o asimétricas (par de claves pública/privada), publicando la clave pública a través de JWK para que otros servicios verifiquen la firma sin conocer el secreto de emisión.
La gestión de claves se vuelve crítica cuando hay varios emisores, servicios de terceros o integración con proveedores de identidad (IdP). Aquí entran en juego estándares como OpenID Connect y servicios de descubrimiento de claves públicas, que permiten rotar las claves sin interrumpir el funcionamiento de las aplicaciones cliente.
También es habitual diseñar estrategias de rotación de tokens y versiones de token por usuario. Por ejemplo, manteniendo en la base de datos del usuario un campo «versión» que se incluye en el payload del JWT; al cambiar esta versión (por un logout global, un cambio de contraseña o un incidente de seguridad), todos los tokens anteriores pasan a considerarse inválidos aunque no hayan caducado.
En entornos profesionales, la arquitectura de autenticación con JWT suele ir acompañada de auditoría, límites de tasa en endpoints de login y renovación, monitorización de intentos fallidos y alertas ante patrones anómalos. Todo esto complementa el uso de JWT, pero no lo reemplaza, y ayuda a construir un sistema realmente robusto.
Por último, cuando el backend forma parte de una plataforma más amplia (por ejemplo, con frontends SPA, apps móviles, paneles de analítica tipo Power BI o integración con agentes de IA), es clave diseñar la autenticación desde el principio con estos escenarios en mente, asegurando que los tokens se puedan usar de forma controlada por diferentes componentes sin abrir puertas de más.
La autenticación con JWT en una API de Node.js, combinando access tokens de vida corta, refresh tokens seguros, middleware de protección de rutas y una buena política de claves y auditoría, permite construir sistemas que son a la vez escalables, eficientes y razonablemente cómodos para el usuario. Si eliges bien qué datos incluir en el payload, aplicas HTTPS, proteges tus secretos y piensas desde el principio en cómo revocar y renovar tokens, tendrás una base sólida sobre la que seguir añadiendo funcionalidades como permisos avanzados, integración con terceros o despliegues en la nube sin sorpresas desagradables.
Redactor apasionado del mundo de los bytes y la tecnología en general. Me encanta compartir mis conocimientos a través de la escritura, y eso es lo que haré en este blog, mostrarte todo lo más interesante sobre gadgets, software, hardware, tendencias tecnológicas, y más. Mi objetivo es ayudarte a navegar por el mundo digital de forma sencilla y entretenida.