FullStackJS Camp
Módulo 6·practica·2h
Objetivos de aprendizaje
  • Comprender por qué HTTP es stateless y para qué sirven las sesiones
  • Configurar express-session con opciones de seguridad
  • Implementar login y logout con req.session
  • Proteger rutas con un middleware de autenticación
  • Diferenciar sesiones de cookies y de JWT

Sesiones y Autenticación con Express

HTTP es stateless

El protocolo HTTP no guarda estado entre requests. Cada solicitud es independiente y el servidor no recuerda quién hizo la solicitud anterior.

code
Request 1: GET /productos          → Servidor no sabe quién eres
Request 2: POST /login {usuario}   → Servidor autentica, pero en la
Request 3: GET /admin              → próxima request ya no lo recuerda

Las sesiones resuelven este problema guardando datos en el servidor y enviando al cliente solo un ID de sesión (en una cookie):

code
Cliente                              Servidor
  │                                     │
  │──── POST /login {user, pass} ──────▶│
  │                                     │ Crea sesión: sess_abc123
  │◀─── Set-Cookie: sessID=abc123 ──────│ req.session.usuario = "Ana"
  │                                     │
  │──── GET /admin (Cookie: abc123) ───▶│
  │                                     │ Busca sesión abc123 → OK
  │◀─── 200 OK (Panel admin) ──────────│

Instalación y configuración

bash
npm install express-session
javascript
// server.js
import express from "express";
import session from "express-session";

const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.use(
  session({
    secret: process.env.SESSION_SECRET ?? "cambia-esto-en-produccion",
    resave: false,           // No re-guardar si no hubo cambios
    saveUninitialized: false, // No crear sesión si no hay datos
    cookie: {
      maxAge: 1000 * 60 * 60, // 1 hora en milisegundos
      httpOnly: true,          // La cookie no es accesible desde JS del cliente
      secure: process.env.NODE_ENV === "production", // Solo HTTPS en producción
      sameSite: "lax",         // Protección CSRF básica
    },
  })
);

Usuarios mock para la demo

javascript
// En producción, esto vendría de la base de datos
// y las contraseñas estarían hasheadas con bcrypt
const USUARIOS = [
  { id: 1, nombre: "Ana García", email: "ana@demo.com", password: "1234", rol: "admin" },
  { id: 2, nombre: "Pedro López", email: "pedro@demo.com", password: "1234", rol: "user" },
];

export { USUARIOS };

Middleware de autenticación

javascript
// middlewares/auth.js

// Verifica que el usuario esté autenticado
export function requireAuth(req, res, next) {
  if (!req.session.usuario) {
    return res.status(401).json({
      ok: false,
      error: "No autenticado. Inicia sesión en POST /auth/login",
    });
  }
  next();
}

// Verifica un rol específico
export function requireRole(rol) {
  return (req, res, next) => {
    if (!req.session.usuario) {
      return res.status(401).json({ ok: false, error: "No autenticado" });
    }
    if (req.session.usuario.rol !== rol) {
      return res.status(403).json({
        ok: false,
        error: `Acceso denegado. Se requiere rol: ${rol}`,
      });
    }
    next();
  };
}

Rutas de autenticación

javascript
// routes/auth.js
import { Router } from "express";
import { requireAuth } from "../middlewares/auth.js";
import { USUARIOS } from "../data/usuarios.js"; // fuente de datos única

const router = Router();

// POST /auth/login
router.post("/login", (req, res) => {
  const { email, password } = req.body;

  if (!email || !password) {
    return res.status(400).json({ ok: false, error: "Email y password requeridos" });
  }

  const usuario = USUARIOS.find(
    (u) => u.email === email && u.password === password
    // ⚠ Demo: en producción usar bcrypt.compare(password, u.passwordHash)
  );

  if (!usuario) {
    return res.status(401).json({ ok: false, error: "Credenciales incorrectas" });
  }

  // Buena práctica: regenerar ID de sesión al autenticar
  // Previene ataques de session fixation (OWASP A07)
  req.session.regenerate((err) => {
    if (err) return res.status(500).json({ ok: false, error: "Error de sesión" });

    // Guardar en sesión (NUNCA incluir la contraseña)
    req.session.usuario = {
      id: usuario.id,
      nombre: usuario.nombre,
      email: usuario.email,
      rol: usuario.rol,
    };

    res.json({
      ok: true,
      mensaje: `Bienvenido, ${usuario.nombre}`,
      usuario: req.session.usuario,
    });
  });
});

// POST /auth/logout
router.post("/logout", requireAuth, (req, res) => {
  const nombre = req.session.usuario.nombre;

  req.session.destroy((err) => {
    if (err) {
      return res.status(500).json({ ok: false, error: "Error al cerrar sesión" });
    }
    res.clearCookie("connect.sid"); // Elimina la cookie del cliente
    res.json({ ok: true, mensaje: `Hasta luego, ${nombre}` });
  });
});

// GET /auth/me — Obtener usuario actual
router.get("/me", requireAuth, (req, res) => {
  res.json({ ok: true, datos: req.session.usuario });
});

export default router;

Rutas protegidas

javascript
// server.js
import express from "express";
import session from "express-session";
import authRouter from "./routes/auth.js";
import { requireAuth, requireRole } from "./middlewares/auth.js";

const app = express();
app.use(express.json());
app.use(session({ /* config */ }));

// Rutas públicas
app.use("/auth", authRouter);

// Rutas protegidas
app.get("/api/perfil", requireAuth, (req, res) => {
  res.json({ ok: true, datos: req.session.usuario });
});

// Solo admins
app.get("/api/admin/usuarios", requireAuth, requireRole("admin"), (req, res) => {
  res.json({ ok: true, datos: [{ id: 1, nombre: "Ana" }] });
});

app.listen(3000);

Probando el flujo completo

bash
# 1. Login
curl -X POST http://localhost:3000/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"ana@demo.com","password":"1234"}' \
  -c cookies.txt          # Guarda las cookies

# 2. Acceder a ruta protegida (enviando la cookie)
curl http://localhost:3000/api/perfil -b cookies.txt

# 3. Acceder como admin
curl http://localhost:3000/api/admin/usuarios -b cookies.txt

# 4. Logout
curl -X POST http://localhost:3000/auth/logout -b cookies.txt

# 5. Intentar acceder después del logout → 401
curl http://localhost:3000/api/perfil -b cookies.txt

Sesiones vs JWT

AspectoSesionesJWT
AlmacenamientoServidor (BD/Redis)Cliente (localStorage/cookie)
EscalabilidadRequiere sesión compartidaStateless, escala fácil
RevocaciónInmediata (borrar sesión)Compleja (lista negra)
TamañoSolo ID en cookieToken completo en cada request
Uso idealApps tradicionales, monolitosAPIs, microservicios, SPAs