DevPath · Aprende a programar ESPTEN

Autenticación y seguridad

Autenticación: contraseñas y tokens

Autenticar es responder "¿quién eres?"

La autenticación comprueba la identidad de quien hace la petición. Empieza, casi siempre, por un par usuario/contraseña, y termina en un token que el cliente envía en cada petición posterior.

Nunca guardes contraseñas en texto plano

Si tu base de datos guarda "hunter2" y alguien la roba, ya tiene todas las contraseñas de tus usuarios (y, como la gente las reutiliza, también las de su correo y su banco). La regla es absoluta: jamás se guarda la contraseña, se guarda su hash.

Un hash es el resultado de una función de un solo sentido: es fácil calcular hash("hunter2"), pero inviable recuperar "hunter2" a partir del hash. Al hacer login, vuelves a hashear lo que escribe el usuario y comparas hashes.

El hash necesita "sal"

Si todos los hashes usan la misma función sin más, dos personas con la misma contraseña tendrán el mismo hash, y un atacante puede precalcular tablas (rainbow tables) con los hashes de millones de contraseñas comunes.

La sal (salt) es un valor aleatorio distinto para cada usuario que se mezcla con la contraseña antes de hashear. Así, dos contraseñas iguales producen hashes distintos, y las tablas precalculadas dejan de servir.

// Conceptual con bcrypt (NO está en este sandbox):
import bcrypt from "bcrypt";

// Al registrarse: genera sal + hash (bcrypt la incluye dentro del hash).
const hash = await bcrypt.hash(password, 10); // 10 = coste/rondas

// Al iniciar sesión: compara sin "deshashear" nada.
const ok = await bcrypt.compare(passwordIntroducida, hash);

bcrypt es lento a propósito (el "coste" controla cuántas rondas). Esa lentitud es deseable: frena los ataques de fuerza bruta.

En los ejercicios no tenemos bcrypt en el sandbox, así que simularemos un hash determinista sencillo. La idea (un solo sentido + sal) es la misma.

Tokens JWT

Tras validar la contraseña, no quieres pedirla en cada petición. Emites un token: una credencial firmada que el cliente guarda y reenvía.

Un JWT (JSON Web Token) tiene tres partes separadas por puntos:

cabecera.payload.firma

Cabecera y payload van en Base64 (legibles por cualquiera: no cifres ahí nada sensible). Lo que protege el token es la firma: si alguien manipula el payload, la firma deja de cuadrar y el servidor rechaza el token.

// Conceptual con jsonwebtoken (NO está en este sandbox):
import jwt from "jsonwebtoken";

const token = jwt.sign({ sub: usuario.id, rol: usuario.rol }, SECRETO, {
  expiresIn: "1h",
});

const payload = jwt.verify(token, SECRETO); // lanza si la firma no es válida

En los ejercicios simularemos la firma con una función firmar(texto, secreto) determinista (en producción sería HMAC-SHA256). El token es cuerpo.firma; al verificar se recalcula la firma y, si el payload se manipuló, deja de cuadrar y se rechaza. Así practicas la garantía de integridad real, no solo decodificar Base64.

El flujo completo

  1. POST /login con usuario y contraseña.
  2. El servidor valida la contraseña contra el hash guardado.
  3. Si es correcta, firma un token y lo devuelve.
  4. El cliente lo guarda y, en cada petición, lo manda en la cabecera:
Authorization: Bearer <token>
  1. El servidor verifica el token en cada petición y sabe quién pide qué.

Ejemplos

Token firmado: alterar el payload invalida la firma

// "Firma" didáctica determinista (en producción: HMAC-SHA256).
function firmar(texto, secreto) {
  let h = 0;
  for (const c of texto + "|" + secreto) h = (Math.imul(h, 31) + c.charCodeAt(0)) | 0;
  return (h >>> 0).toString(16);
}
function crearToken(payload, secreto) {
  const cuerpo = btoa(JSON.stringify(payload));
  return cuerpo + "." + firmar(cuerpo, secreto);
}
function verificarToken(token, secreto) {
  const [cuerpo, firma] = token.split(".");
  if (!cuerpo || firma !== firmar(cuerpo, secreto)) return null; // alterado
  return JSON.parse(atob(cuerpo));
}

const token = crearToken({ sub: "u1", rol: "user" }, "s3cr3t");
console.log(verificarToken(token, "s3cr3t").rol); // "user"

// Un atacante cambia el payload, pero sin el secreto no puede re-firmar:
const [, firma] = token.split(".");
const falso = btoa(JSON.stringify({ sub: "u1", rol: "admin" })) + "." + firma;
console.log(verificarToken(falso, "s3cr3t")); // null (rechazado)
Pon esto en práctica

DevPath es un curso práctico: aquí lees la teoría; en la app la pones en práctica con ejercicios que se ejecutan de verdad, sin conexión.

Empezar gratis en la app →
Autorización: exigir token y comprobar roles →