Saltar al contenido principal

Cookies y Sesiones

[... previous sections remain the same ...]

🗄️ Sesiones (Session)

Principio de funcionamiento de las sesiones

1. Inicio de sesión del cliente
├─ Enviar username, password
└─ Servidor autentica

2. Servidor crea sesión
├─ Generar ID de sesión (abc123)
├─ Almacenar en memoria/base de datos
└─ {
sessionId: 'abc123',
userId: 456,
createdAt: '2025-01-26T10:00:00Z'
}

3. Servidor envía ID de sesión como cookie
└─ Set-Cookie: sessionId=abc123

4. Cliente almacena ID de sesión
└─ Almacenar en cookies del navegador

5. En cada solicitud posterior, enviar ID de sesión
├─ Cookie: sessionId=abc123
└─ Servidor consulta sesión para identificar usuario

6. Al cerrar sesión
├─ Eliminar datos de sesión del servidor
└─ Eliminar cookie

Almacenamiento de sesiones

1. Memoria (Memory)
├─ Más rápido
├─ Se pierde al reiniciar servidor
├─ No compartible entre múltiples servidores
└─ Adecuado para entorno de desarrollo

2. Redis
├─ Alto rendimiento
├─ Opciones de persistencia
├─ Compartible entre servidores
└─ Adecuado para entorno de producción

3. Base de datos (MySQL, PostgreSQL)
├─ Garantiza persistencia
├─ Relativamente más lento
├─ Compartible entre servidores
└─ Adecuado para sesiones de larga duración

4. Sistema de archivos
├─ Implementación simple
├─ Rendimiento bajo
├─ Complejo en entornos multiservidor
└─ Aplicaciones pequeñas

Gestión de sesiones con Redis

const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');

const app = express();

// Crear cliente Redis
const redisClient = createClient({
host: 'localhost',
port: 6379
});
redisClient.connect();

// Middleware de sesión
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: 'tu-clave-secreta',
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // HTTPS
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // 24 horas
}
}));

// Inicio de sesión
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = await authenticate(username, password);

if (user) {
// Almacenar información de usuario en sesión
req.session.userId = user.id;
req.session.username = user.username;

res.json({ message: 'Inicio de sesión exitoso' });
} else {
res.status(401).json({ message: 'Autenticación fallida' });
}
});

// Consultar perfil
app.get('/perfil', (req, res) => {
if (!req.session.userId) {
return res.status(401).json({ message: 'Autenticación requerida' });
}

res.json({
userId: req.session.userId,
username: req.session.username
});
});

// Cierre de sesión
app.post('/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.status(500).json({ message: 'Cierre de sesión fallido' });
}
res.clearCookie('connect.sid'); // Eliminar cookie de sesión
res.json({ message: 'Cierre de sesión exitoso' });
});
});

🔐 JWT vs Sesiones

JWT (JSON Web Token)

Estructura JWT:
Header.Payload.Signature

Ejemplo:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VySWQiOjQ1NiwidXNlcm5hbWUiOiJob25nIn0.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Características:
├─ Autócontenido
├─ No requiere almacenamiento en servidor
├─ Sin estado
└─ Gran escalabilidad

Ventajas:
├─ Ahorro de memoria en servidor
├─ Adecuado para entornos multiservidor
├─ Ideal para microservicios
└─ Conveniente para aplicaciones móviles

Desventajas:
├─ Tokens de mayor tamaño
├─ Dificultad para invalidar tokens
├─ No se puede forzar cierre de sesión
└─ Necesidad de cifrar información sensible
const jwt = require('jsonwebtoken');

// Generar JWT
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = await authenticate(username, password);

if (user) {
// Generar token
const token = jwt.sign(
{
userId: user.id,
username: user.username
},
'tu-clave-secreta',
{ expiresIn: '24h' }
);

res.json({ token });
} else {
res.status(401).json({ message: 'Autenticación fallida' });
}
});

// Middleware de verificación de JWT
function authenticateToken(req, res, next) {
const encabezadoAuth = req.headers['authorization'];
const token = encabezadoAuth && encabezadoAuth.split(' ')[1]; // Bearer TOKEN

if (!token) {
return res.status(401).json({ message: 'Token requerido' });
}

jwt.verify(token, 'tu-clave-secreta', (err, user) => {
if (err) {
return res.status(403).json({ message: 'Token inválido' });
}
req.user = user;
next();
});
}

// Ruta protegida
app.get('/perfil', authenticateToken, (req, res) => {
res.json({
userId: req.user.userId,
username: req.user.username
});
});

Comparación de sesiones vs JWT

┌──────────────┬─────────────────┬─────────────────┐
│ Aspecto │ Sesiones │ JWT │
├──────────────┼─────────────────┼─────────────────┤
│ Ubicación │ Servidor │ Cliente │
│ Escalabilidad│ Baja (requiere │ Alta (sin │
│ │ compartir) │ estado) │
│ Uso memoria │ Alto │ Bajo │
│ Tamaño token │ Pequeño (solo │ Grande │
│ │ ID) │ (datos incluidos)│
│ Seguridad │ Alta │ Media │
│ Invalidación │ Fácil │ Difícil │
│ Control │ Flexible │ Requiere │
│ │ │ reemisión │
│ Entorno │ Aplicaciones │ API, móvil │
│ adecuado │ web │ │
└──────────────┴─────────────────┴─────────────────┘

¿Cuándo usar?

Sesiones:
├─ Aplicaciones web tradicionales
├─ Requieren seguridad fuerte
├─ Necesidad de invalidación inmediata
└─ Servidor único o con sesión compartida

JWT:
├─ API RESTful
├─ Arquitecturas de microservicios
├─ Aplicaciones móviles
├─ Escalabilidad importante
└─ Requiere sin estado

🛡️ Consideraciones de seguridad

Defensa contra XSS (Cross-Site Scripting)

// ❌ Riesgo: Cookies accesibles por JavaScript
document.cookie = "sessionId=abc123";
// Un atacante podría usar <script>alert(document.cookie)</script> para robar

// ✅ Seguro: Cookies HttpOnly
res.cookie('sessionId', 'abc123', {
httpOnly: true // Bloquear acceso de JavaScript
});

// ✅ Seguro: JWT almacenado en cookie HttpOnly
res.cookie('token', tokenJWT, {
httpOnly: true,
secure: true,
sameSite: 'strict'
});

Defensa contra CSRF (Cross-Site Request Forgery)

// ❌ Riesgo: Cookies enviadas sin SameSite
res.cookie('sessionId', 'abc123');
// Se envían cookies incluso desde otros sitios

// ✅ Seguro: Configurar SameSite
res.cookie('sessionId', 'abc123', {
sameSite: 'strict' // Solo enviar en el mismo sitio
});

// ✅ Seguro: Usar token CSRF
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });

app.post('/transferencia', csrfProtection, (req, res) => {
// Token CSRF verificado
res.json({ message: 'Transferencia exitosa' });
});

Defensa contra ataques de intermediario

// ❌ Riesgo: Enviar cookie por HTTP
res.cookie('sessionId', 'abc123');

// ✅ Seguro: Enviar solo por HTTPS
res.cookie('sessionId', 'abc123', {
secure: true // Solo HTTPS
});

// ✅ Seguro: Configurar encabezado HSTS
app.use((req, res, next) => {
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
next();
});

Defensa contra ataque de fijación de sesión

// Regenerar ID de sesión al iniciar sesión
app.post('/login', (req, res) => {
const { username, password } = req.body;
const user = authenticate(username, password);

if (user) {
// Invalidar ID de sesión actual y generar nuevo
req.session.regenerate((err) => {
if (err) {
return res.status(500).json({ message: 'Inicio de sesión fallido' });
}

req.session.userId = user.id;
res.json({ message: 'Inicio de sesión exitoso' });
});
}
});

💡 Sistema de autenticación completo

const express = require('express');
const session = require('express-session');
const bcrypt = require('bcrypt');
const helmet = require('helmet');

const app = express();
app.use(express.json());
app.use(helmet()); // Encabezados de seguridad

// Configuración de sesión
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production', // HTTPS en producción
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000, // 24 horas
sameSite: 'strict'
}
}));

// Registro
app.post('/registro', async (req, res) => {
try {
const { username, password } = req.body;

// Hash de contraseña
const hashedPassword = await bcrypt.hash(password, 10);

// Guardar usuario (base de datos)
await saveUser({ username, password: hashedPassword });

res.status(201).json({ message: 'Registro exitoso' });
} catch (error) {
res.status(500).json({ message: 'Registro fallido' });
}
});

// Inicio de sesión
app.post('/login', async (req, res) => {
try {
const { username, password } = req.body;

// Buscar usuario
const user = await findUser(username);
if (!user) {
return res.status(401).json({ message: 'Usuario no encontrado' });
}

// Verificar contraseña
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
return res.status(401).json({ message: 'Contraseña incorrecta' });
}

// Regenerar sesión (defensa contra ataque de fijación)
req.session.regenerate((err) => {
if (err) {
return res.status(500).json({ message: 'Inicio de sesión fallido' });
}

// Guardar información de usuario en sesión
req.session.userId = user.id;
req.session.username = user.username;

res.json({
message: 'Inicio de sesión exitoso',
user: {
id: user.id,
username: user.username
}
});
});
} catch (error) {
res.status(500).json({ message: 'Inicio de sesión fallido' });
}
});

// Middleware de autenticación
function requireAuth(req, res, next) {
if (!req.session.userId) {
return res.status(401).json({ message: 'Inicio de sesión requerido' });
}
next();
}

// Consultar perfil
app.get('/perfil', requireAuth, async (req, res) => {
try {
const user = await findUserById(req.session.userId);
res.json({
id: user.id,
username: user.username,
email: user.email
});
} catch (error) {
res.status(500).json({ message: 'Consulta de perfil fallida' });
}
});

// Cierre de sesión
app.post('/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.status(500).json({ message: 'Cierre de sesión fallido' });
}
res.clearCookie('connect.sid');
res.json({ message: 'Cierre de sesión exitoso' });
});
});

// Verificar información de sesión
app.get('/sesion', (req, res) => {
if (req.session.userId) {
res.json({
isAuthenticated: true,
userId: req.session.userId,
username: req.session.username
});
} else {
res.json({ isAuthenticated: false });
}
});

🤔 Preguntas frecuentes

Q1. ¿Cuál es la diferencia entre cookies y almacenamiento local?

R:

Cookies
├─ Enviadas automáticamente al servidor
├─ Límite de 4 KB
├─ Tiempo de expiración configurable
├─ Pueden ser HttpOnly (defensa XSS)
└─ Limitadas por dominio/ruta

Almacenamiento local (localStorage)
├─ No enviado automáticamente al servidor
├─ Límite de 5-10 MB
├─ Almacenamiento permanente
├─ Accesible solo por JavaScript (vulnerable a XSS)
└─ Solo en mismo origen

Almacenamiento de sesión (sessionStorage)
├─ Almacenamiento por pestaña
├─ Se elimina al cerrar pestaña
└─ Similar a localStorage

Cuándo usar:
Cookies: Tokens de autenticación
localStorage: Configuraciones de usuario
sessionStorage: Datos de formulario temporal

Q2. ¿Cómo implementar timeout de sesión?

R:

// 1. Timeout fijo (predeterminado de express-session)
app.use(session({
cookie: {
maxAge: 30 * 60 * 1000 // 30 minutos
}
}));

// 2. Timeout deslizante (actualizar al hacer actividad)
app.use((req, res, next) => {
if (req.session.userId) {
// Actualizar tiempo de última actividad
req.session.ultimaActividad = Date.now();
}
next();
});

// 3. Timeout absoluto (tiempo fijo tras inicio de sesión)
app.use((req, res, next) => {
if (req.session.userId && req.session.horaInicio) {
const transcurrido = Date.now() - req.session.horaInicio;
const duracionMaxima = 8 * 60 * 60 * 1000; // 8 horas

if (transcurrido > duracionMaxima) {
req.session.destroy();
return res.status(401).json({ message: 'Sesión expirada' });
}
}
next();
});

// 4. Timeout de inactividad
app.use((req, res, next) => {
if (req.session.userId && req.session.ultimaActividad) {
const inactividad = Date.now() - req.session.ultimaActividad;
const maxInactividad = 30 * 60 * 1000; // 30 minutos

if (inactividad > maxInactividad) {
req.session.destroy();
return res.status(401).json({ message: 'Sesión expirada (inactividad)' });
}

// Actualizar tiempo de actividad
req.session.ultimaActividad = Date.now();
}
next();
});

Q3. ¿Cómo implementar la función "Recordarme"?

R:

app.post('/login', async (req, res) => {
const { username, password, recordarme } = req.body;
const user = await authenticate(username, password);

if (user) {
req.session.userId = user.id;

if (recordarme) {
// Ampliar vigencia de cookie si se marca "Recordarme"
req.session.cookie.maxAge = 30 * 24 * 60 * 60 * 1000; // 30 días
} else {
// Por defecto: eliminar al cerrar navegador
req.session.cookie.maxAge = null;
}

res.json({ message: 'Inicio de sesión exitoso' });
}
});

// O usando token de recordatorio separado
app.post('/login', async (req, res) => {
const { username, password, recordarme } = req.body;
const user = await authenticate(username, password);

if (user) {
// Cookie de sesión (corta duración)
req.session.userId = user.id;

if (recordarme) {
// Generar token de recordatorio
const tokenRecordatorio = generarTokenSeguro();

// Guardar token en base de datos
await guardarTokenRecordatorio(user.id, tokenRecordatorio);

// Enviar como cookie
res.cookie('tokenRecordatorio', tokenRecordatorio, {
httpOnly: true,
secure: true,
maxAge: 30 * 24 * 60 * 60 * 1000 // 30 días
});
}

res.json({ message: 'Inicio de sesión exitoso' });
}
});

// Verificación de token de recordatorio
app.use(async (req, res, next) => {
// Sin sesión pero con token de recordatorio
if (!req.session.userId && req.cookies.tokenRecordatorio) {
const user = await buscarUsuarioPorTokenRecordatorio(req.cookies.tokenRecordatorio);

if (user) {
// Regenerar sesión
req.session.userId = user.id;

// Seguridad: generar nuevo token
const nuevoToken = generarTokenSeguro();
await actualizarTokenRecordatorio(user.id, nuevoToken);
res.cookie('tokenRecordatorio', nuevoToken, {
httpOnly: true,
secure: true,
maxAge: 30 * 24 * 60 * 60 * 1000
});
} else {
// Eliminar token no válido
res.clearCookie('tokenRecordatorio');
}
}

next();
});

Q4. ¿Cómo gestionar inicios de sesión en múltiples dispositivos?

R:

// Tabla de sesiones
/*
sesiones
- id
- userId
- sessionId
- infoDispositivo (User-Agent)
- dirección IP
- createdAt
- ultimaActividad
*/

// Inicio de sesión
app.post('/login', async (req, res) => {
const user = await authenticate(req.body.username, req.body.password);

if (user) {
req.session.regenerate(async (err) => {
req.session.userId = user.id;

// Guardar información de sesión
await guardarInfoSesion({
userId: user.id,
sessionId: req.sessionID,
infoDispositivo: req.headers['user-agent'],
dirección IP: req.ip,
createdAt: new Date()
});

res.json({ message: 'Inicio de sesión exitoso' });
});
}
});

// Listar sesiones activas
app.get('/sesiones', requireAuth, async (req, res) => {
const sesiones = await obtenerSesionesActivas(req.session.userId);

res.json({
actual: req.sessionID,
sesiones: sesiones.map(s => ({
id: s.sessionId,
dispositivo: parsearUserAgent(s.infoDispositivo),
ubicación: s.dirección IP,
ultimaActividad: s.ultimaActividad,
esActual: s.sessionId === req.sessionID
}))
});
});

// Cerrar sesión específica
app.delete('/sesiones/:sessionId', requireAuth, async (req, res) => {
const { sessionId } = req.params;

// Solo eliminar sesión propia
const sesion = await obtenerSesion(sessionId);
if (sesion.userId !== req.session.userId) {
return res.status(403).json({ message: 'Sin autorización' });
}

// Eliminar sesión de Redis
await clienteRedis.del(`sess:${sessionId}`);

// Eliminar registro de base de datos
await eliminarSesion(sessionId);

res.json({ message: 'Sesión cerrada' });
});

// Cerrar todas las sesiones (cerrar sesión en otros dispositivos)
app.post('/logout-all', requireAuth, async (req, res) => {
const sesiones = await obtenerSesionesActivas(req.session.userId);

// Eliminar todas las sesiones
for (const sesion of sesiones) {
if (sesion.sessionId !== req.sessionID) {
await clienteRedis.del(`sess:${sesion.sessionId}`);
await eliminarSesion(sesion.sessionId);
}
}

res.json({ message: 'Cerrado en todos los dispositivos' });
});

Q5. ¿Se pueden autenticar sin cookies?

R:

¡Sí! Usar encabezado Authorization

1. Token Bearer (JWT, etc.)
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Ventajas:
├─ Sin restricciones de cookies
├─ Ideal para apps móviles
├─ Sin problemas CORS
└─ Flexible en almacenamiento

Desventajas:
├─ Vulnerable a XSS si se usa localStorage
├─ Requiere incluir token en cada solicitud
└─ No envío automático

2. Clave API
Authorization: ApiKey abc123xyz
X-API-Key: abc123xyz

Ventajas:
├─ Implementación simple
├─ Adecuado para comunicación entre servicios
└─ No requiere cookies

Desventajas:
├─ Vulnerable a exposición de clave
├─ Más para autenticación de aplicación que de usuario
└─ Difícil renovación

Recomendaciones:
- Web: Cookies HttpOnly + JWT
- Móvil: Encabezado Authorization + JWT
- Entre servicios: Clave API

🎓 Prácticas

1. Pruebas de cookies

// Establecer cookie
document.cookie = "theme=dark; max-age=3600; path=/";

// Leer cookies
console.log(document.cookie);

// Parsear cookies
function obtenerTodasCookies() {
return document.cookie.split('; ').reduce((acc, cookie) => {
const [nombre, valor] = cookie.split('=');
acc[nombre] = valor;
return acc;
}, {});
}

console.log(obtenerTodasCookies());

// Eliminar cookie
document.cookie = "theme=; max-age=0; path=/";

2. Probar sesión con httpstat.us

// Probar flujo de sesión
async function probarSesion() {
// Iniciar sesión (crear sesión)
const resInicioSesion = await fetch('https://api.example.com/login', {
method: 'POST',
credentials: 'include', // Incluir cookies
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: 'prueba',
password: 'contraseña'
})
});

console.log('Inicio de sesión:', resInicioSesion.status);

// Consultar perfil (envío automático de cookie de sesión)
const resPerfil = await fetch('https://api.example.com/perfil', {
credentials: 'include'
});

const perfil = await resPerfil.json();
console.log('Perfil:', perfil);

// Cerrar sesión
const resCierreSesion = await fetch('https://api.example.com/logout', {
method: 'POST',
credentials: 'include'
});

console.log('Cierre de sesión:', resCierreSesion.status);
}

probarSesion();

🔗 Documentos relacionados

🎬 Conclusión

¡Cookies y sesiones son tecnologías fundamentales para superar la naturaleza sin estado de HTTP y para implementar autenticación de usuarios! Es crucial utilizarlas considerando la seguridad.

¡Ha completado la serie HTTP! Ahora comprende los conceptos fundamentales del protocolo HTTP. ¡Aplíquelos en sus proyectos prácticos!