FundamentosSeguridad

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

petición ¿quién?authn ¿puede?authz recurso falla → 401 falla → 403

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.