Autenticación y autorización
Quién eres y qué puedes hacer — los dos controles que protegen tu aplicación, y los errores que los saltan por completo.
En esta página
Son las dos preguntas de seguridad que tu aplicación responde en cada petición, y confundirlas es el origen de una familia entera de vulnerabilidades:
- Autenticación (authn) — ¿quién eres? Verificar identidad: contraseña, passkey, OAuth, 2FA.
- Autorización (authz) — ¿tienes permiso para esto? Verificar acceso: roles, propiedad del recurso, políticas.
Los códigos HTTP lo dicen con precisión: 401 Unauthorized = “no sé quién eres” (fallo de autenticación); 403 Forbidden = “sé quién eres, y no puedes” (fallo de autorización).
El flujo completo de una petición
Dos puertas distintas, en orden. La segunda se comprueba en cada recurso, no una vez por sesión.
Contraseñas: la única forma correcta de guardarlas
Nunca en claro, nunca cifradas (lo cifrado se descifra), nunca con un hash rápido (MD5, SHA-256 a secas — una GPU prueba miles de millones por segundo). Lo correcto: un hash lento con salt — bcrypt, scrypt o Argon2.
import bcrypt from 'bcrypt';
// Registro: el salt va incluido en el hash resultante
const hash = await bcrypt.hash(password, 12); // 12 = factor de coste
// Login: comparación en tiempo constante
const valida = await bcrypt.compare(passwordIntroducida, hash);
- El salt (aleatorio, por usuario) hace inútiles las tablas precalculadas: dos usuarios con la misma contraseña tienen hashes distintos.
- El factor de coste hace el hash deliberadamente caro (~100 ms): imperceptible en un login, demoledor para quien intenta millones.
Mantener la sesión: cookies vs. tokens
HTTP no tiene memoria: tras el login, cada petición debe demostrar quién eres. Dos estrategias dominan:
| Sesión en servidor (cookie) | Token autocontenido (JWT) | |
|---|---|---|
| El servidor guarda | La sesión (memoria/Redis) | Nada: el token lleva los datos firmados |
| Revocar al instante | ✅ Borras la sesión | ⚠️ Difícil: el token vale hasta expirar |
| Varios servicios | Necesitan compartir el almacén | ✅ Cada uno verifica la firma |
| Riesgo típico | Fijación/robo de cookie | Guardarlo en localStorage (XSS lo lee) |
Para una aplicación web clásica, la cookie de sesión HttpOnly; Secure; SameSite sigue siendo la opción más segura y simple. Los JWT brillan entre servicios; usados como sesión de navegador, con expiración corta y refresh token — y jamás en localStorage.
El fallo de autorización número 1: IDOR
Broken Access Control encabeza el OWASP Top 10, y su forma más común es dolorosamente simple. Un usuario autenticado pide:
GET /api/facturas/1041 ← su factura
GET /api/facturas/1042 ← ...la de otro cliente. ¿Y el servidor la devuelve?
Eso es un IDOR (Insecure Direct Object Reference): la aplicación comprobó quién eres pero no si ese recurso es tuyo. La defensa es incluir la propiedad en cada consulta, en el servidor:
// ❌ Autenticado ≠ autorizado
const factura = await db.factura.findUnique({ where: { id } });
// ✅ La consulta misma exige la propiedad
const factura = await db.factura.findFirst({
where: { id, clienteId: usuarioActual.id },
});
if (!factura) return res.sendStatus(404); // ni confirmes que existe
Reglas que evitan la familia entera de fallos:
- Autoriza en el servidor, en cada petición. Ocultar el botón en el frontend no es autorización — la API sigue expuesta a quien escriba la URL.
- Denegar por defecto: sin permiso explícito, la respuesta es no.
- Mínimo privilegio: cada rol (y cada servicio) con lo justo para operar.
Lo que nunca escribes tú mismo
La criptografía y los flujos de identidad son el territorio con los errores más caros y menos visibles. Usa librerías y estándares probados — bcrypt/Argon2, OAuth 2.0/OpenID Connect, passkeys/WebAuthn — y reserva tu ingenio para tu dominio. En seguridad, la originalidad es un antipatrón.