🔐 2.10 · API REST con autenticación JWT: protege tu backend

⏱ 6h 00min ⚡ 120 XP 🏅 Profesional Junior 📖 Backend FullStack
"Una API sin autenticación es una puerta abierta. JWT es la cerradura estándar."

🎯 Objetivo del tema

Al terminar este tema serás capaz de: Al terminar este tema serás capaz de diseñar una API REST profesional con autenticación JWT: registro, login, rutas protegidas, middleware de autenticación, refresh tokens, y buenas prácticas de seguridad. Tu API podrá distinguir entre usuarios autenticados y anónimos, proteger datos sensibles, y escalar a millones de usuarios.
🎬
Video del instructor

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.

1. Qué es autenticación vs autorización

Identidad vs permisos: dos conceptos que confundes una vez y aprendes para siempre.

2. REST: las 4 reglas que tu API debe seguir

Stateless, recursos como URLs, verbos HTTP, y respuestas JSON.

3. JWT: estructura, firma, verificación

header.payload.signature: el token que viaja entre cliente y servidor.

4. Login y registro: bcrypt y jsonwebtoken

Cómo hashear contraseñas y emitir tokens.

5. Middleware de autenticación en Express

req.user: la forma de saber quién hizo la petición.

6. Refresh tokens y logout

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?).

ConceptoPreguntaCuándo se aplicaTecnologí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.
⭐ Autenticado != autorizado: El error #1 de juniors: confundir autenticación con autorización. Un usuario puede estar AUTENTICADO (sé quién es) pero NO AUTORIZADO a hacer algo (no es admin). Un endpoint '/admin' debe verificar AMBAS cosas: (1) que el usuario esté logueado, (2) que su rol sea 'admin'. La primera sin la segunda es un agujero de seguridad gigante.
🎬
Video del instructor

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.

📷 Imagen referencial: Arquitectura REST: cliente hace peticiones HTTP (GET/POST/PUT/DELETE) a URLs del servidor, servidor responde con JSON y códigos de estado HTTP.
Regla RESTQué significaBuen ejemploMal 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 URLsURLs = sustantivos (cosas), no verbos (acciones).GET /usuarios, /usuarios/5GET /getUsuario?id=5
Verbos HTTPLa ACCIÓN va en el verbo HTTP, no en la URL.DELETE /usuarios/5POST /usuarios/borrar/5
JSON como respuestaEl formato estándar.Content-Type: application/jsonXML o texto plano.
⭐ URLs = recursos, verbos HTTP = acciones: Las URLs en REST son como las direcciones de tu casa: el RECURSO que quieres (sustantivo), no la ACCIÓN que quieres hacer (verbo). GET /usuarios/5 es 'dame el recurso usuario 5', no 'obtén el usuario 5'. La acción de 'obtener' ya está implícita en GET. Es lo que hace que las APIs REST sean intuitivas y cacheables.
🎬
Video del instructor

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).

📷 Imagen referencial: Estructura de un JWT: 3 partes separadas por puntos. Header (algoritmo), Payload (datos del usuario), Signature (firma con secret).
// 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

⭐ JWT firmado, no encriptado: JWT NO está encriptado, está FIRMADO. Cualquiera puede decodificar el payload (es base64, no cifrado). Lo que evita la manipulación es la signature: si alguien cambia el payload, la signature no coincide. Por eso NUNCA pongas datos sensibles (contraseñas, datos bancarios) en el payload: cualquiera puede leerlos. Solo información NO sensible: userId, rol, email.
🎬
Video del instructor

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

⭐ bcrypt SIEMPRE: NUNCA guardes contraseñas en texto plano. NUNCA. Ni en la BD, ni en logs, ni en archivos. SIEMPRE bcrypt.hash(password, 12) antes de guardar. El 12 son 'salt rounds': 2^12 iteraciones de hashing, lo que hace que sea lento de fuerza bruta. Para comparar después, bcrypt.compare(password, hash) en vez de comparar strings. Si tu BD se filtra y las contraseñas están en texto plano, es CATASTROFE.
🎬
Video del instructor

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

⭐ 401 vs 403: 401 vs 403: 401 Unauthorized = 'no sé quién eres' (falta autenticación, token inválido). 403 Forbidden = 'sé quién eres pero no tienes permiso' (autenticado pero no autorizado). Es un detalle que muchos juniors confunden. Tu API REST debe distinguir: 401 cuando el token falta/es inválido, 403 cuando el usuario está autenticado pero no tiene el rol necesario.
🎬
Video del instructor

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

⭐ Refresh en httpOnly cookie: El refresh token debe estar en una cookie httpOnly + secure + sameSite=strict para que JavaScript NO pueda acceder a él (protección contra XSS). El access token puede ir en memoria (variable JS) o en sessionStorage. NUNCA en localStorage (vulnerable a XSS). Esta arquitectura de 2 tokens es la que usan Google, Facebook, GitHub, etc.
🎬
Video del instructor

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

  1. Instala: npm install bcrypt jsonwebtoken cookie-parser.
  2. Crea .env: JWT_SECRET=mi_secreto_super_seguro (largo, aleatorio).
  3. Crea middleware/auth.js (verificación del JWT) y middleware/admin.js (solo admins).
  4. Crea routes/auth.js con /register, /login, /refresh, /logout.
  5. Protege las rutas existentes: agrega authMiddleware a GET /perfil, POST /libros, etc.
  6. Agrega adminMiddleware a DELETE /libros/:id (solo admins pueden borrar).
  7. Prueba con curl: registro, login (obtén token), GET /perfil con token, GET /perfil sin token (debe dar 401).
  8. Intenta hackear: token alterado (cambia una letra), token expirado, GET /admin siendo user normal.
  9. 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.'
  10. Aplica 2 mejoras. Vuelve a probar.
  11. 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

Error 1: Guardar contraseñas en texto plano.
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.
Error 2: Poner el JWT_SECRET en el código.
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.
Error 3: Guardar el JWT en localStorage (vulnerable a XSS).
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.
Error 4: No validar el token correctamente (aceptar cualquier string).
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.
Error 5: Devolver 200 cuando el usuario no está autorizado (en vez de 401 o 403).
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

  1. Parte del proyecto 'api-libros-mongo' del Tema 2.9.
  2. Instala: npm install bcrypt jsonwebtoken cookie-parser dotenv (si no lo tienes).
  3. Crea .env con: JWT_SECRET, JWT_REFRESH_SECRET (largos, aleatorios), MONGODB_URI.
  4. Modifica el schema de Usuario: agrega password (select: false para que no se devuelva por defecto) y rol (default: 'user').
  5. Crea middleware/auth.js: lee Authorization: Bearer <token>, verifica con jwt.verify, carga el usuario, req.user = usuario.
  6. Crea middleware/admin.js: verifica req.user.rol === 'admin'.
  7. 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).
  8. Modifica routes/libros.js: protege POST, PUT, DELETE con authMiddleware. DELETE solo con adminMiddleware.
  9. Configura cookies httpOnly para el refresh token: res.cookie('refreshToken', token, { httpOnly: true, secure: true, sameSite: 'strict' }).
  10. 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).
  11. Haz commit: 'feat: autenticación JWT completa con bcrypt, refresh tokens y middlewares'.
  12. 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.

📓 Tu cuaderno: Anota pseudocódigo, diagramas, errores que encontraste y respuestas a "explica sin código". La escritura manual refuerza tu razonamiento. Tu profesor puede pedirte que subas fotos de páginas específicas.