Kodokon kodokon.com

Autenticación: hashing, sesiones frente a JWT

Hashea las contraseñas con una función costosa, elige entre sesiones y JWT con pleno conocimiento, y protege tus rutas con un middleware.

10 min · 3 preguntas

Abrir esta lección en Kodokon

Almacenar una contraseña significa prepararse para una futura filtración: diseña el hashing para que sobreviva al robo de la base de datos. Un hash rápido como SHA-256 se puede forzar por fuerza bruta a miles de millones de intentos por segundo en una GPU. Necesitas una función deliberadamente costosa (scrypt, provista por node:crypto, o bcrypt/argon2) y un salt aleatorio por usuario, que vuelve inútiles las tablas precalculadas. El salt no es un secreto: se almacena en texto plano junto al hash.

JAVASCRIPT
import {
  randomBytes,
  scryptSync,
  timingSafeEqual,
} from 'node:crypto';

export function hashPassword(password) {
  const salt = randomBytes(16).toString('hex');
  const hash = scryptSync(password, salt, 64)
    .toString('hex');
  return `${salt}:${hash}`;
}

export function verifyPassword(password, stored) {
  const [salt, hash] = stored.split(':');
  const candidate = scryptSync(password, salt, 64);
  const expected = Buffer.from(hash, 'hex');
  return timingSafeEqual(candidate, expected);
}
Salt aleatorio, derivación costosa, comparación en tiempo constante.

Dos modelos para mantener al usuario con la sesión iniciada. La sesión: un identificador opaco en una cookie httpOnly, con el estado del lado del servidor. Revocación inmediata, cookie invisible para JavaScript, pero un almacén compartido (Redis) se vuelve necesario en cuanto añades una segunda instancia. El JWT: el estado firmado incrustado en el token, verificable sin ningún almacén (ideal entre servicios) pero irrevocable antes de expirar. La contrapartida honesta: sesiones para una aplicación web clásica, JWT de vida corta para APIs consumidas por terceros o para arquitecturas distribuidas.

JAVASCRIPT
import jwt from 'jsonwebtoken';

const SECRET = process.env.JWT_SECRET;
if (!SECRET) {
  throw new Error('JWT_SECRET must be set');
}

export function signToken(user) {
  return jwt.sign(
    { sub: user.id, email: user.email },
    SECRET,
    { expiresIn: '15m' },
  );
}
Secreto obligatorio al arrancar, tokens con una vida útil corta.

El middleware de protección factoriza el control de acceso: extrae el token de la cabecera Authorization: Bearer, lo verifica, adjunta la identidad decodificada a req.user, o corta por lo sano con un 401 dirigido al middleware de errores de la lección anterior. Aplícalo a nivel del router (router.use(requireAuth)) para proteger todo un grupo de rutas en lugar de ruta por ruta.

JAVASCRIPT
import jwt from 'jsonwebtoken';
import { HttpError } from './http-error.js';

const SECRET = process.env.JWT_SECRET;

export function requireAuth(req, res, next) {
  const header = req.headers.authorization ?? '';
  const [scheme, token] = header.split(' ');
  if (scheme !== 'Bearer' || !token) {
    return next(new HttpError(401, 'Missing token'));
  }
  try {
    req.user = jwt.verify(token, SECRET);
    next();
  } catch {
    next(new HttpError(401, 'Invalid or expired token'));
  }
}
Verificar, adjuntar req.user, o 401: nada más.

Prueba de conocimientos

Comprueba que has retenido los puntos clave de esta lección.

  1. ¿Por qué SHA-256 por sí solo no es adecuado para almacenar contraseñas?
    • Porque el algoritmo está criptográficamente roto
    • Porque es demasiado rápido: una GPU prueba miles de millones de candidatos por segundo; necesitas una función lenta y con salt
    • Porque produce colisiones en contraseñas cortas
    • Porque Node no lo implementa de forma nativa
  2. ¿Qué mecanismo permite revocar el acceso de inmediato, antes de cualquier expiración?
    • El JWT, porque incrusta su fecha de expiración
    • La sesión del lado del servidor, porque basta con borrar su entrada del almacén
    • Ambos, por igual
  3. ¿Para qué sirve timingSafeEqual en la verificación de una contraseña?
    • Para acelerar la comparación de los dos hashes
    • Para comparar en tiempo constante, de modo que la duración de la respuesta no revele cuántos bytes coinciden
    • Para convertir los hashes hexadecimales en Buffers