Protege tu API REST con JWT: login seguro paso a paso
Introducción
Cuando desarrollamos una aplicación web, muchas veces necesitamos proteger ciertas partes de nuestra API. Por ejemplo, una zona de usuario, un panel de administración o una ruta que devuelve información privada.El problema es que una API REST no recuerda por sí sola quién está haciendo cada petición. Cada solicitud es independiente, por lo que necesitamos algún sistema para identificar al usuario después de iniciar sesión.En este post vamos a resolver ese problema usando JWT, una forma muy común de autenticar usuarios en aplicaciones modernas. Construiremos una demo sencilla con Node.js, Express y jsonwebtoken, donde tendremos una ruta pública, una ruta de login y una ruta protegida que solo funcionará si enviamos un token válido.
¿Qué problema vamos a resolver?
Imaginemos que tenemos una API REST con varias rutas. Algunas rutas pueden ser públicas, como una página de inicio o un listado general, pero otras deberían ser privadas, como el perfil de un usuario.
El problema principal sería el siguiente:
¿Cómo podemos saber si el usuario que está haciendo una petición está autenticado?
Para resolverlo, vamos a usar un token JWT. El funcionamiento será sencillo:
- El usuario inicia sesión enviando usuario y contraseña.
- El backend comprueba si los datos son correctos.
- Si son correctos, genera un token JWT.
- El cliente guarda ese token.
- En las siguientes peticiones, el cliente envía el token.
- El backend valida el token antes de permitir el acceso a rutas privadas.
Requisitos previos
Antes de empezar, necesitamos tener instalado:
- Node.js
- npm
- Visual Studio Code
- Un navegador web
También es recomendable tener conocimientos básicos de JavaScript y de cómo funcionan las peticiones HTTP.
Paso 1: Crear el proyecto
Lo primero será crear una carpeta para el proyecto e inicializar Node.js:
mkdir jwt-api-visual-demo
cd jwt-api-visual-demo
npm init -y
Esto creará el archivo package.json, donde se guardará la configuración del proyecto.
Paso 2: Instalar las dependencias
Para este ejemplo vamos a utilizar tres paquetes principales:
npm install express jsonwebtoken dotenv
Estos paquetes sirven para lo siguiente:
express: permite crear el servidor y las rutas de la API.jsonwebtoken: permite crear y validar tokens JWT.dotenv: permite trabajar con variables de entorno.
También podemos instalar nodemon para reiniciar el servidor automáticamente cada vez que hagamos un cambio:
npm install -D nodemon
Después, en el archivo package.json, añadimos este script:
"scripts": {
"dev": "nodemon server.js"
}
Así podremos arrancar el proyecto con:
npm run dev
Paso 3: Crear la clave secreta
Creamos un archivo llamado .env en la raíz del proyecto:
JWT_SECRET=mi_clave_secreta_super_segura
Esta clave será utilizada para firmar y verificar los tokens JWT.
En un proyecto real, este archivo no debería subirse nunca a GitHub. Para evitarlo, se recomienda añadir .env al archivo .gitignore.
Paso 4: Crear el servidor Express
Creamos un archivo llamado server.js. Este archivo será el encargado de levantar el servidor, servir la página visual y definir las rutas de la API.
const express = require('express');
const jwt = require('jsonwebtoken');
require('dotenv').config();
const app = express();
const PORT = 3000;
app.use(express.json());
app.use(express.static('public'));
const userDemo = {
id: 1,
username: 'daniel',
password: '1234'
};
app.get('/api/public', (req, res) => {
res.json({
message: 'Esta es una ruta pública. No necesitas token para acceder.'
});
});
app.post('/api/login', (req, res) => {
const { username, password } = req.body;
if (username !== userDemo.username || password !== userDemo.password) {
return res.status(401).json({
message: 'Usuario o contraseña incorrectos'
});
}
const token = jwt.sign(
{
id: userDemo.id,
username: userDemo.username
},
process.env.JWT_SECRET,
{
expiresIn: '1h'
}
);
res.json({
message: 'Login correcto',
token: token
});
});
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({
message: 'Acceso denegado. No se ha enviado ningún token.'
});
}
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({
message: 'Token inválido o expirado'
});
}
req.user = user;
next();
});
}
app.get('/api/profile', authenticateToken, (req, res) => {
res.json({
message: 'Has accedido a una ruta protegida',
user: req.user
});
});
app.listen(PORT, () => {
console.log(`Servidor iniciado en http://localhost:${PORT}`);
});
Paso 5: Crear la interfaz visual
Para que la demo sea más fácil de entender, he creado una pequeña página web con cuatro botones:
- Probar ruta pública
- Hacer login y generar token
- Perfil sin token
- Perfil con token
De esta forma no necesitamos usar Postman ni Thunder Client durante el vídeo. Toda la prueba se puede hacer desde el navegador.
La interfaz permite ver claramente el flujo de autenticación:
- Primero se prueba una ruta pública.
- Después se hace login y se genera un token JWT.
- Luego se intenta entrar a una ruta protegida sin token.
- Por último, se accede a la misma ruta enviando el token generado.
Paso 6: Probar la ruta pública
Al pulsar el botón “Probar ruta pública”, la aplicación llama a la ruta:
GET /api/public
La respuesta de la API será parecida a esta:
{
"message": "Esta es una ruta pública. No necesitas token para acceder."
}
Esto demuestra que hay rutas que pueden funcionar sin autenticación.
Paso 7: Hacer login y generar el token
Después pulsamos el botón “Hacer login y generar token”.
La aplicación envía al backend un usuario y contraseña de prueba:
{
"username": "daniel",
"password": "1234"
}
Si los datos son correctos, el backend genera un token JWT y lo devuelve al cliente.
Ese token será el que usaremos después para acceder a la ruta protegida.
Paso 8: Probar la ruta protegida sin token
Ahora pulsamos el botón “Perfil sin token”.
La aplicación intenta acceder a:
GET /api/profile
Pero no envía ningún token. Por eso, la API responde con un error:
{
"message": "Acceso denegado. No se ha enviado ningún token."
}
Este paso es importante porque demuestra que la ruta está realmente protegida.
Paso 9: Probar la ruta protegida con token
Por último, pulsamos el botón “Perfil con token”.
En este caso, la aplicación sí envía el token generado en el login dentro del header Authorization:
Authorization: Bearer TOKEN
El backend recibe el token, lo valida y permite el acceso a la ruta protegida.
La respuesta será parecida a esta:
{
"message": "Has accedido a una ruta protegida",
"user": {
"id": 1,
"username": "daniel"
}
}
Problemas encontrados
Uno de los problemas más habituales al trabajar con JWT es enviar mal el token. El formato correcto debe ser:
Authorization: Bearer TOKEN
Si se olvida la palabra Bearer o se copia mal el token, la API no podrá validarlo.
Otro problema común es configurar mal la variable JWT_SECRET. Si el token se firma con una clave y se intenta validar con otra distinta, la verificación fallará.
También puede ocurrir que el token haya expirado. En este ejemplo hemos configurado una duración de una hora, por lo que después de ese tiempo sería necesario iniciar sesión de nuevo.
Recomendaciones
Aunque esta demo es sencilla, en una aplicación real deberíamos tener en cuenta algunas buenas prácticas:
- No guardar contraseñas en texto plano.
- Cifrar las contraseñas con una librería como
bcrypt. - No subir el archivo
.enva GitHub. - Usar HTTPS en producción.
- No guardar información sensible dentro del token.
- Añadir roles de usuario si la aplicación tiene permisos diferentes.
Conclusión
JWT es una solución muy utilizada para proteger APIs REST modernas. Su funcionamiento consiste en generar un token cuando el usuario inicia sesión y pedir ese token cada vez que se quiera acceder a una ruta privada.
En este tutorial hemos visto una demo visual con Node.js y Express. Primero hemos probado una ruta pública, después hemos generado un token mediante login y finalmente hemos comprobado cómo una ruta protegida solo permite el acceso cuando recibe un token válido.
Aunque el ejemplo es básico, la idea principal es la misma que se utiliza en aplicaciones reales. La diferencia es que en producción habría que añadir más medidas de seguridad, como cifrado de contraseñas, HTTPS, roles de usuario y una gestión más avanzada de tokens.
Autor: Daniel Talavera Clemente
Máster: Desarrollo Full Stack + Arquitecturas Cloud
Centro: Tajamar Tech
Año académico: 2025-2026
Código / recursos utilizados : github