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: el algoritmo de firma (p. ej.
HS256). - Payload: los datos (claims):
{ sub: "u1", rol: "admin", exp: ... }. - Firma: se calcula con una clave secreta que solo conoce el servidor.
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 escuerpo.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
POST /logincon usuario y contraseña.- El servidor valida la contraseña contra el hash guardado.
- Si es correcta, firma un token y lo devuelve.
- El cliente lo guarda y, en cada petición, lo manda en la cabecera:
Authorization: Bearer <token>
- 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)