🎯 Objetivo del tema
El administrador aún no ha insertado un video para esta sección.
🗺️ Mapa del tema
Hasta ahora, tu API es PÚBLICA: cualquiera puede hacer GET, POST, DELETE sin ninguna restricción. En el mundo real, necesitas controlar QUIÉN puede hacer QUÉ: solo el dueño de un post puede editarlo, solo usuarios logueados pueden comentar, solo admins pueden borrar usuarios. JWT (JSON Web Tokens) es el estándar moderno para esto: compacto, sin estado, escalable, y portable entre lenguajes.
Identidad vs permisos: dos conceptos que confundes una vez y aprendes para siempre.
Stateless, recursos como URLs, verbos HTTP, y respuestas JSON.
header.payload.signature: el token que viaja entre cliente y servidor.
Cómo hashear contraseñas y emitir tokens.
req.user: la forma de saber quién hizo la petición.
Cómo extender sesiones sin guardar estado en el servidor.
2.10.1 Autenticación vs autorización
Dos conceptos que se confunden constantemente: autenticación es 'quién eres', autorización es 'qué puedes hacer'. Son pasos distintos y tu API los maneja por separado. Primero autenticas (login), después autorizas (¿este usuario puede hacer esto?).
| Concepto | Pregunta | Cuándo se aplica | Tecnología típica |
|---|---|---|---|
| Autenticación | ¿QUIÉN eres? | Al login (POST /auth/login). | JWT, sesiones, OAuth. |
| Autorización | ¿QUÉ puedes hacer? | En cada petición protegida. | Roles, permisos, scopes. |
| Identificación | ¿A QUIÉN representas? | Ya autenticado, en cada request. | Token con userId. |
El administrador aún no ha insertado un video para esta sección.
2.10.2 REST: las 4 reglas que tu API debe seguir
REST (Representational State Transfer) es un ESTILO de arquitectura para APIs web, no un estándar rígido. Las buenas APIs REST tienen 4 características: son stateless, usan URLs para representar recursos, verbos HTTP para acciones, y JSON como formato de respuesta. Si tu API sigue estas reglas, es RESTful.
| Regla REST | Qué significa | Buen ejemplo | Mal ejemplo |
|---|---|---|---|
| Stateless (sin estado) | Cada petición es independiente; el servidor NO guarda sesión. | Bearer token en cada request. | Sesión en el servidor con cookie de sesión. |
| Recursos como URLs | URLs = sustantivos (cosas), no verbos (acciones). | GET /usuarios, /usuarios/5 | GET /getUsuario?id=5 |
| Verbos HTTP | La ACCIÓN va en el verbo HTTP, no en la URL. | DELETE /usuarios/5 | POST /usuarios/borrar/5 |
| JSON como respuesta | El formato estándar. | Content-Type: application/json | XML o texto plano. |
El administrador aún no ha insertado un video para esta sección.
2.10.3 JWT: estructura, firma, verificación
Un JWT (JSON Web Token) es un string con 3 partes separadas por puntos: header.payload.signature. El header dice qué algoritmo se usó. El payload contiene los datos (claims). La signature verifica que el token no fue alterado. Es COMPACTO (cabe en una cookie o un header), SIN ESTADO (el servidor no guarda nada) y PORTABLE (cualquier servidor puede verificarlo).
// Un JWT de ejemplo:
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjQyLCJyb2wiOiJhZG1pbiIsImlhdCI6MTcwMDAwMDAwMCwiZXhwIjoxNzAwMDAzNjAwfQ.7v8X3qGvBhC0dE4sWqXjKp2z0gN3p9wY5sF4nL6kM2c
// \_HEADER_/ \_/___________________PAYLOAD___________________________/ \_SIGNATURE_/
// Decodificado:
// Header: { alg: 'HS256', typ: 'JWT' }
// Payload: { userId: 42, rol: 'admin', iat: 1700000000, exp: 1700003600 }
// Signature: HMACSHA256(base64UrlEncode(header) + '.' + base64UrlEncode(payload), secret)
// La signature se calcula con un SECRET que solo el servidor conoce.
// Si alguien cambia el payload, la signature no coincide, y el token es INVÁLIDO.Estructura de un JWT
El administrador aún no ha insertado un video para esta sección.
2.10.4 Login y registro: bcrypt y jsonwebtoken
El flujo de autenticación: el usuario se REGISTRA (email + contraseña), el servidor hashea la contraseña y guarda el hash. En el LOGIN, el usuario envía sus credenciales, el servidor las compara con el hash, y si coinciden emite un JWT. El cliente guarda el JWT y lo envía en cada petición protegida.
// auth/register.js: registrar un usuario
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import { Usuario } from '../models/Usuario.js';
export const registrar = asyncHandler(async (req, res) => {
const { email, password, nombre } = req.body;
// 1. Validar (en Mongoose con validators: true)
if (!email || !password) {
return res.status(400).json({ error: 'Email y password requeridos' });
}
// 2. Verificar que no exista
const existe = await Usuario.findOne({ email });
if (existe) return res.status(409).json({ error: 'Email ya registrado' });
// 3. HASHEAR la contraseña (NUNCA guardar en texto plano)
const passwordHash = await bcrypt.hash(password, 12); // 12 = salt rounds
// 4. Crear el usuario
const usuario = await Usuario.create({ email, password: passwordHash, nombre });
// 5. Emitir un JWT
const token = jwt.sign(
{ userId: usuario._id, rol: usuario.rol },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
res.status(201).json({ usuario: { id: usuario._id, email, nombre }, token });
});
// auth/login.js: iniciar sesión
export const login = asyncHandler(async (req, res) => {
const { email, password } = req.body;
const usuario = await Usuario.findOne({ email });
if (!usuario) return res.status(401).json({ error: 'Credenciales inválidas' });
// Comparar la contraseña con el hash
const ok = await bcrypt.compare(password, usuario.password);
if (!ok) return res.status(401).json({ error: 'Credenciales inválidas' });
// Emitir JWT (igual que en register)
const token = jwt.sign(
{ userId: usuario._id, rol: usuario.rol },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
res.json({ usuario: { id: usuario._id, email, nombre }, token });
});Registro y Login con bcrypt y JWT
El administrador aún no ha insertado un video para esta sección.
2.10.5 Middleware de autenticación en Express
Un middleware de autenticación verifica el JWT en cada petición protegida. Si el token es válido, añade req.user con los datos del usuario para que las rutas puedan usarlos. Si no, devuelve 401 Unauthorized. Es la forma estándar de proteger rutas en Express.
// middleware/auth.js: middleware de autenticación
import jwt from 'jsonwebtoken';
import { Usuario } from '../models/Usuario.js';
export const authMiddleware = asyncHandler(async (req, res, next) => {
// 1. Leer el header Authorization: Bearer <token>
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Token no proporcionado' });
}
const token = authHeader.split(' ')[1];
try {
// 2. Verificar el token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// 3. Cargar el usuario (opcional, para tener datos frescos)
const usuario = await Usuario.findById(decoded.userId).select('-password');
if (!usuario) return res.status(401).json({ error: 'Usuario no existe' });
// 4. Añadir req.user para las rutas
req.user = usuario;
next();
} catch (err) {
return res.status(401).json({ error: 'Token inválido o expirado' });
}
});
// middleware/admin.js: solo admins
export const adminMiddleware = (req, res, next) => {
if (req.user?.rol !== 'admin') {
return res.status(403).json({ error: 'Requiere rol admin' });
}
next();
};
// Uso en rutas:
import { authMiddleware, adminMiddleware } from '../middleware/auth.js';
router.get('/perfil', authMiddleware, (req, res) => {
res.json(req.user); // req.user viene del middleware
});
router.delete('/usuarios/:id', authMiddleware, adminMiddleware, async (req, res) => {
// Solo usuarios autenticados Y con rol admin pueden borrar
await Usuario.findByIdAndDelete(req.params.id);
res.status(204).send();
});Middleware de autenticación y de admin
El administrador aún no ha insertado un video para esta sección.
2.10.6 Refresh tokens y logout
Un JWT con expiración corta (1h) es seguro, pero incomodo: el usuario tiene que hacer login cada hora. La solución profesional: access token corto (1h) + refresh token largo (7-30 días). El access token se usa para peticiones; cuando expira, el refresh token (que está en una cookie httpOnly) permite obtener uno nuevo sin que el usuario haga login de nuevo.
// auth/refresh.js: obtener un nuevo access token
import jwt from 'jsonwebtoken';
export const refresh = asyncHandler(async (req, res) => {
// El refresh token viene en una cookie httpOnly (no accesible por JS)
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) {
return res.status(401).json({ error: 'No refresh token' });
}
try {
const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
const newAccessToken = jwt.sign(
{ userId: decoded.userId, rol: decoded.rol },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
res.json({ token: newAccessToken });
} catch (err) {
res.status(401).json({ error: 'Refresh token inválido' });
}
});
// auth/logout.js: cerrar sesión (borrar el refresh token)
export const logout = (req, res) => {
res.clearCookie('refreshToken');
res.json({ mensaje: 'Sesión cerrada' });
};Refresh token y logout
El administrador aún no ha insertado un video para esta sección.
📚 Contenido ampliado
Material adicional, referencias externas verificadas y ejemplos extendidos.
🤖 AI Mission
La seguridad de una API se rompe probándola, no leyendo sobre ella.
Misión: Agrega autenticación JWT a tu API de Express + Mongoose del Tema 2.9. Implementa registro, login, rutas protegidas, y middleware de admin. Después, intenta 'hackear' tu propia API: intenta acceder a rutas protegidas sin token, con token expirado, con token alterado, intenta borrar siendo user normal, etc. Pídele a la IA que audite tu seguridad. Anota qué vulnerabilidades encontraste.
Pasos sugeridos
- Instala: npm install bcrypt jsonwebtoken cookie-parser.
- Crea .env: JWT_SECRET=mi_secreto_super_seguro (largo, aleatorio).
- Crea middleware/auth.js (verificación del JWT) y middleware/admin.js (solo admins).
- Crea routes/auth.js con /register, /login, /refresh, /logout.
- Protege las rutas existentes: agrega authMiddleware a GET /perfil, POST /libros, etc.
- Agrega adminMiddleware a DELETE /libros/:id (solo admins pueden borrar).
- Prueba con curl: registro, login (obtén token), GET /perfil con token, GET /perfil sin token (debe dar 401).
- Intenta hackear: token alterado (cambia una letra), token expirado, GET /admin siendo user normal.
- Pídele a la IA: 'Tengo esta API con JWT. Audita la seguridad: token storage, expiración, validación. Dame 2 mejoras concretas con código.'
- Aplica 2 mejoras. Vuelve a probar.
- Anota en tu cuaderno: 3 vulnerabilidades que encontraste y cómo las arreglaste.
📓 Entregable: Capturas de: registro, login, acceso con token, intento de acceso sin token (401), intento como user normal a ruta admin (403), código de los middlewares, y media página de cuaderno con tu auditoría de seguridad.
🚫 Errores típicos de razonamiento
Por qué: Si tu BD se filtra y las contraseñas están en texto plano, TODOS los usuarios están comprometidos. SIEMPRE hashea con bcrypt.hash(password, 12) antes de guardar. Para comparar, bcrypt.compare(password, hash). Es la regla #1 de seguridad en cualquier sistema con login.
Por qué: Si tu código se filtra o se sube a Git, todos los tokens quedan comprometidos. El SECRET debe estar en variables de entorno (.env) y NUNCA en el código. Y debe ser LARGO y aleatorio (no 'mi_secret'). Usa al menos 32 caracteres generados con openssl rand -hex 32.
Por qué: localStorage es accesible por cualquier JavaScript en la página, incluyendo código malicioso de terceros. Si un atacante inyecta un script (XSS), puede robar todos los tokens. Guarda el access token en memoria (variable JS) y el refresh token en una cookie httpOnly (no accesible por JS). Es el patrón de Google, Facebook, etc.
Por qué: Si solo verificas que el header Authorization existe pero no validas el JWT con jwt.verify(), cualquier string sería 'válido'. Un atacante podría simplemente enviar cualquier basura y pasar la autenticación. SIEMPRE usa jwt.verify(token, secret) que valida la signature y la expiración. Si falla, lanza una excepción que tu middleware atrapa con try/catch.
Por qué: Si tu API devuelve 200 OK con un error en el body, el cliente no puede distinguir entre éxito y error mirando solo el código HTTP (que es lo que muchas herramientas hacen). Usa los códigos correctos: 401 para no autenticado, 403 para autenticado pero sin permiso, 404 para no encontrado, 409 para conflicto, 500 para error del servidor. Tu API debe comunicar el resultado en el código HTTP, no solo en el body.
🧪 Laboratorio práctico
La seguridad se rompe probando tu propio código, no leyendo sobre ella.
Laboratorio: API REST con autenticación JWT completa
Objetivo: Agregar autenticación JWT completa a tu API del Tema 2.9: registro, login, rutas protegidas, middleware de admin, refresh tokens. Practicarás: bcrypt, jsonwebtoken, middleware, cookies httpOnly, y los códigos HTTP correctos.
Pasos
- Parte del proyecto 'api-libros-mongo' del Tema 2.9.
- Instala: npm install bcrypt jsonwebtoken cookie-parser dotenv (si no lo tienes).
- Crea .env con: JWT_SECRET, JWT_REFRESH_SECRET (largos, aleatorios), MONGODB_URI.
- Modifica el schema de Usuario: agrega password (select: false para que no se devuelva por defecto) y rol (default: 'user').
- Crea middleware/auth.js: lee Authorization: Bearer <token>, verifica con jwt.verify, carga el usuario, req.user = usuario.
- Crea middleware/admin.js: verifica req.user.rol === 'admin'.
- Crea routes/auth.js con: POST /register (hashea password, crea usuario, emite tokens), POST /login (compara, emite tokens), POST /refresh (usa refresh token), POST /logout (clear cookie).
- Modifica routes/libros.js: protege POST, PUT, DELETE con authMiddleware. DELETE solo con adminMiddleware.
- Configura cookies httpOnly para el refresh token: res.cookie('refreshToken', token, { httpOnly: true, secure: true, sameSite: 'strict' }).
- Prueba con curl: registro, login, GET /perfil con token, GET /libros sin token (200, público), DELETE /libros/:id siendo user normal (403), siendo admin (204).
- Haz commit: 'feat: autenticación JWT completa con bcrypt, refresh tokens y middlewares'.
- Sube a Railway. Comparte URL pública.
📓 Entregable: URL pública con auth funcionando, capturas de los 4 flujos (registro, login, ruta protegida, intento fallido), código de los middlewares, y commit en Git.