Al cerrar el módulo 7 dejamos una pregunta incómoda sobre la mesa. La función comprarEntradas de src/repositorios/compras-sql.js resuelve la sobreventa con una transacción impecable, bloquea la fila de la sesión, descuenta el aforo y emite las entradas. Y recibe un usuarioId… que llega en el cuerpo de la petición. Es decir: de lo que el cliente quiera enviar. Cualquiera puede comprar en nombre de Lucía, consultar los pedidos de Marc, publicar un evento en el Teatro Almendra sin ser su organizador o anular entradas ajenas.

En este módulo llenamos ese hueco. Y empezamos por lo que casi nadie explica bien: qué es exactamente autenticar, en qué se diferencia de autorizar, por qué HTTP nos lo pone difícil, y qué estrategias existen para resolverlo. Esta lección no escribe casi código: construye el mapa mental sin el cual las cinco lecciones siguientes son recetas que se copian sin entender.

Contenido

  1. Autenticación frente a autorización
  2. Factores de autenticación y MFA
  3. El problema central: HTTP no tiene estado
  4. Estrategias para mantener la identidad entre peticiones
  5. Cookies explicadas de verdad
  6. El modelo de amenazas de Escena Viva
  7. Por qué no se implementa criptografía propia
  8. El diseño elegido para Escena Viva
  9. Datos personales y RGPD
  10. Errores comunes y consejos
  11. Ejercicios
  12. Conclusión

  1. Autenticación frente a autorización

Son dos preguntas distintas que se hacen en momentos distintos.

Autenticación Autorización
Pregunta ¿Quién eres? ¿Qué puedes hacer?
Momento Al entrar (y en cada petición, al verificar la credencial) En cada acción concreta
Resultado Una identidad: usuario 64f…, rol organizador Un permiso: sí / no
Fallo típico Credencial inválida Identidad válida pero sin permiso
Código HTTP 401 Unauthorized (mal nombrado: significa «no autenticado») 403 Forbidden

En Escena Viva la diferencia es muy concreta:

  • Autenticación: Lucía escribe [email protected] y su contraseña. El servidor comprueba que la contraseña coincide con lo que tiene guardado y concluye: esta petición viene de Lucía.
  • Autorización: Lucía, ya identificada, pide GET /api/pedidos/ped-042. El servidor comprueba que ese pedido es suyo. Si es de Marc, la respuesta es 403 (o 404, ya veremos por qué a veces conviene).

Confundirlas produce agujeros reales. Los dos errores clásicos:

  1. Autenticar y dar por hecho el permiso. «Si ha iniciado sesión, es que puede.» Así nace el fallo más común de las APIs: GET /api/pedidos/:id devuelve el pedido a cualquier usuario autenticado que adivine el identificador. Se llama referencia directa insegura a objetos (IDOR), y lo atacamos en la lección 08-05.
  2. Autorizar sin autenticar bien. Confiar en un rol que llega del cliente, o en un usuarioId del cuerpo —exactamente lo que hace hoy comprarEntradas—. La autorización se apoya sobre la autenticación: si la base es de papel, el edificio también.

Hay un tercer concepto que conviene nombrar para no mezclarlo: identificación es decir quién eres (el correo); autenticación es demostrarlo (la contraseña). Un identificador nunca es una credencial. Enviar solo usuarioId es identificación pura, y por eso hoy la API es indefendible.

  1. Factores de autenticación y MFA

Un factor es una categoría de prueba. Hay tres clásicas:

Factor Qué es Ejemplos Debilidad principal
Algo que sabes Conocimiento secreto Contraseña, PIN, respuesta secreta Se reutiliza, se filtra, se adivina
Algo que tienes Posesión de un objeto Móvil con app TOTP, llave FIDO2, tarjeta Se pierde o se roba; el SMS se intercepta (SIM swapping)
Algo que eres Rasgo biométrico Huella, cara No se puede cambiar si se compromete; suele desbloquear un factor local, no viajar por la red

MFA (autenticación multifactor) es usar factores de categorías distintas. Contraseña + código TOTP es MFA. Contraseña + pregunta secreta no lo es: ambos son «algo que sabes», y ambos se filtran en la misma base de datos.

Ordenados por robustez real, de menos a más: SMS < correo electrónico < TOTP (aplicación de códigos) < llave física FIDO2/WebAuthn. El SMS es mejor que nada, pero es el eslabón que rompen los ataques dirigidos.

En Escena Viva implementaremos el primer factor bien hecho (contraseña, lección 08-02) y dejaremos el segundo factor documentado como extensión natural: la cuenta de un administrador que puede anular entradas y cambiar roles es exactamente el sitio donde el MFA deja de ser opcional.

  1. El problema central: HTTP no tiene estado

HTTP es un protocolo sin estado: cada petición es independiente y el servidor no recuerda nada de la anterior. Es una virtud de diseño (permite escalar, balancear, reintentar), pero significa que:

Aunque Lucía se identifique en POST /auth/login, la siguiente petición POST /api/compras llega como si el servidor no la hubiera visto nunca.

La única salida es que cada petición transporte una prueba de identidad. Toda la autenticación web se reduce a responder tres preguntas sobre esa prueba:

  1. ¿Dónde se guarda en el cliente? (cookie, memoria de JavaScript, almacén nativo)
  2. ¿Cómo viaja? (cookie automática, cabecera Authorization)
  3. ¿Cómo la verifica el servidor? (buscándola en un almacén, o comprobando una firma)

Las estrategias que vienen a continuación son combinaciones distintas de esas tres respuestas.

  1. Estrategias para mantener la identidad entre peticiones

Estrategia Cómo viaja Estado en servidor Ventajas Inconvenientes Cuándo elegirla
Básica HTTP Authorization: Basic base64(usuario:clave) en cada petición Ninguno Trivial de implementar Manda la contraseña una y otra vez; sin cierre de sesión; sin caducidad; inutilizable sin TLS Casi nunca en producción; herramientas internas y pruebas rápidas
Sesión de servidor + cookie Cookie con un identificador aleatorio, enviada por el navegador automáticamente Sí: la sesión vive en el servidor Revocación instantánea; la cookie no revela nada; madura y bien entendida Estado compartido (necesita Redis con varios procesos); vulnerable a CSRF si no se protege; incómoda entre dominios Aplicaciones web renderizadas en servidor y SPAs del mismo sitio
Token autocontenido (JWT) Authorization: Bearer <token> No (el token se autoverifica) Sin estado, escala fácil; vale entre dominios y para móviles; útil entre microservicios No se puede revocar sin añadir estado; si se guarda mal en el navegador, el XSS lo roba; carga útil visible APIs consumidas por móvil, SPAs entre dominios, servicio a servicio
Clave de API Cabecera propia (X-API-Key) o Authorization Sí (tabla de claves) Simple para integraciones; fácil de rotar y revocar por cliente Identifica una aplicación, no a una persona; sin caducidad natural; se filtra en repositorios Integraciones máquina a máquina, webhooks, servicios internos
OAuth 2.0 / OpenID Connect Token emitido por un tercero (Google, GitHub, Auth0) Depende del flujo Delega contraseñas al proveedor; SSO; consentimiento granular Complejo; dependencia de un tercero; muchos flujos y muchas formas de equivocarse «Entrar con Google», acceso a APIs de terceros, identidad corporativa

Y la pregunta práctica, respondida sin ambigüedad:

  • App móvil nativa: tokens (JWT de acceso corto + refresco en el almacén seguro del sistema). Las cookies encajan mal fuera del navegador.
  • Web con servidor que renderiza HTML: sesiones de servidor con cookie. Es la opción más segura y sencilla; el navegador ya sabe hacer su parte.
  • Servicio a servicio: claves de API o credenciales de cliente de OAuth 2.0, con rotación y ámbito reducido. Nunca la contraseña de una persona.
  • SPA + API del mismo sitio: cualquiera de las dos; sesiones si es un único origen, tokens si hay varios clientes.

Escena Viva es una API con front-end estático y aspiraciones de app móvil. Por eso acabará en JWT, pero con la cookie recuperada donde de verdad importa (el token de refresco).

  1. Cookies explicadas de verdad

Todo lo que viene después se apoya en cookies, así que conviene entenderlas al detalle. Una cookie es un par nombre/valor que el servidor pide al navegador que guarde y reenvíe automáticamente.

HTTP/1.1 200 OK
Set-Cookie: sid=Zk9x3Qb7...; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=1800
Content-Type: application/json

A partir de ahí, el navegador incluye por su cuenta:

GET /api/pedidos HTTP/1.1
Host: api.escenaviva.test
Cookie: sid=Zk9x3Qb7...

Ese automatismo es la gran ventaja de las cookies (no hay que programar nada en el cliente) y también su gran peligro (se envían aunque la petición la origine otra web: eso es CSRF).

Atributo Qué hace Ataque que mitiga
HttpOnly JavaScript no puede leerla (document.cookie no la ve) XSS: aunque inyecten un script, no pueden exfiltrar la cookie
Secure Solo viaja por HTTPS Interceptación en red (Wi-Fi abierta, proxy)
SameSite=Strict No se envía en ninguna petición originada por otro sitio CSRF (máxima protección; rompe la navegación desde enlaces externos)
SameSite=Lax Se envía en navegaciones de nivel superior con GET, no en POST ni en peticiones en segundo plano CSRF en el 95 % de los casos, sin romper enlaces entrantes
SameSite=None Se envía siempre; obliga a Secure Ninguno: es para escenarios entre dominios y exige defensa CSRF explícita
Domain Amplía la cookie a subdominios (Domain=escenaviva.test) Omitirlo la limita al host exacto: menos alcance, menos riesgo
Path Limita a qué rutas se envía (Path=/auth/refrescar) Reduce la exposición del token de refresco
Max-Age / Expires Caducidad. Sin ellos, la cookie es de sesión y muere al cerrar el navegador Limita la ventana de una cookie robada
Prefijo __Host- El navegador exige Secure, Path=/ y sin Domain Impide que un subdominio comprometido plante cookies en el dominio padre

Tres reglas que aplicaremos sin excepción:

  1. Toda cookie de autenticación lleva HttpOnly y Secure.
  2. SameSite=Lax por defecto; None solo con una razón escrita y protección CSRF adicional.
  3. La cookie nunca contiene datos del usuario, solo un identificador opaco o un token firmado.

  1. El modelo de amenazas de Escena Viva

Modelar amenazas es preguntarse, antes de escribir código: ¿quién ataca, qué quiere y qué pasa si lo consigue?

Atacante Qué quiere Cómo lo intentaría hoy Impacto
Revendedor Acaparar entradas del Festival de Jazz Automatizar POST /api/compras sin límite Aforo agotado en segundos; pérdida de reputación
Curioso con la API abierta Ver pedidos de otros GET /api/pedidos/ped-001, ped-002… Fuga de datos personales; incidente RGPD
Suplantador Comprar en nombre de Lucía Enviar usuarioId de Lucía en el cuerpo Cargos indebidos, entradas robadas
Competidor de una sala Publicar o alterar eventos ajenos POST /api/eventos sin ser organizador de esa sala Sabotaje del catálogo
Automatización de credenciales Entrar con contraseñas filtradas de otros sitios Miles de POST /auth/login Cuentas comprometidas en masa

Y los ataques que hay que conocer por su nombre, porque las lecciones siguientes se refieren a ellos constantemente:

  • XSS (Cross-Site Scripting): se inyecta JavaScript en una página y se ejecuta con los permisos del usuario. Es el motivo de HttpOnly y de no guardar tokens en localStorage.
  • CSRF (Cross-Site Request Forgery): otra web hace que tu navegador envíe una petición autenticada sin que lo sepas, aprovechando el envío automático de cookies. Se combate con SameSite y tokens sincronizadores (08-03).
  • Fijación de sesión: el atacante planta un identificador de sesión conocido antes del login y espera a que la víctima entre con él. Defensa: regenerar el identificador al iniciar sesión (08-03).
  • Secuestro de sesión: robar una sesión ya válida (por XSS, red insegura o registros mal hechos). Defensa: HttpOnly, Secure, caducidad corta, rotación.
  • Fuerza bruta: probar muchas contraseñas contra una cuenta. Defensa: hash lento y límite de intentos (08-02, 08-06).
  • Relleno de credenciales (credential stuffing): probar pares correo/contraseña filtrados de otros servicios contra el nuestro. Defensa: límites, detección de anomalías, comprobación contra listas de contraseñas filtradas y MFA.

  1. Por qué no se implementa criptografía propia

La regla, sin matices: no diseñes criptografía y no implementes primitivas criptográficas. Usa bibliotecas revisadas por la comunidad durante años.

Las razones no son de humildad, son técnicas:

  • Un algoritmo puede ser correcto y aun así filtrar información por el tiempo de ejecución, por el consumo de memoria o por los mensajes de error.
  • Los detalles que deciden la seguridad son invisibles: rellenos, vectores de inicialización, modos de operación, generación de aleatoriedad.
  • La criptografía rota no falla de forma visible: sigue devolviendo resultados perfectos hasta el día que alguien la rompe.

Lo mismo vale para los esquemas de token caseros. La tentación clásica es guardar en la cookie algo como usuario=lucia|rol=administrador|caduca=... y firmarlo «con un hash del secreto y los datos concatenados». Ese diseño concreto es vulnerable a extensión de longitud, a confusión de separadores y a comparaciones no constantes. JWT existe precisamente porque este problema ya está resuelto, con especificación pública y bibliotecas auditadas.

Lo que sí haremos con node:crypto es lo que está pensado para usarse: generar bytes aleatorios (randomBytes), hashear tokens de un solo uso (createHash) y comparar en tiempo constante (timingSafeEqual, que ya vimos en el módulo 3 con los Buffers, y que aquí por fin cobra sentido).

  1. El diseño elegido para Escena Viva

Estas son las decisiones del módulo, escritas ahora para que las cinco lecciones siguientes sean su implementación:

Decisión Elección Motivo
Almacén de credenciales Usuario de Mongoose (M7) con hashContrasena y select: false El modelo ya existe; solo le faltaba la contraseña
Hash de contraseñas bcrypt con factor de coste medido Estándar maduro, sin dependencias nativas problemáticas
Sesión de servidor express-session + Passport local, enseñado y montado Es la base conceptual y la mejor opción para muchas apps
Autenticación oficial de la API JWT de acceso (15 min) + refresco en cookie httpOnly Front-end estático y futuro cliente móvil
Autorización RBAC con los roles ya modelados + propiedad del recurso Tres roles claros; la propiedad cubre lo que RBAC no
Política de acceso Centralizada en src/autorizacion/politica.js Se prueba sola (M9) y no se dispersa en if

Y el mapa de rutas resultante:

Zona Rutas Quién entra
Pública GET /api/eventos, GET /api/eventos/:id, GET /api/sesiones Cualquiera
Autenticación POST /auth/registro, /auth/login, /auth/refrescar, /auth/logout Cualquiera (con límites estrictos)
Autenticada POST /api/compras, GET /api/pedidos, GET /api/pedidos/:id Asistente (solo lo suyo)
Organizador POST /api/eventos, PATCH /api/eventos/:id/publicar, GET /api/salas/:sala/ventas Organizador de esa sala
Administración PATCH /api/usuarios/:id/rol, PATCH /api/entradas/:codigo/anular Administrador

El flujo que implementaremos entre 08-02 y 08-04:

sequenceDiagram
  participant C as Cliente (front-end)
  participant A as API Escena Viva
  participant D as Base de datos

  C->>A: POST /auth/registro {email, contrasena, nombre}
  A->>A: Validar con zod y normalizar el correo
  A->>A: bcrypt.hash(contrasena, coste)
  A->>D: Crear Usuario {email, hashContrasena, rol: 'asistente'}
  A-->>C: 201 {id, email, nombre, rol}  (sin el hash)

  C->>A: POST /auth/login {email, contrasena}
  A->>D: Buscar usuario por email (+select hashContrasena)
  A->>A: bcrypt.compare (tiempo constante)
  alt Credenciales correctas
    A->>D: Guardar hash del token de refresco
    A-->>C: 200 {tokenAcceso} + Set-Cookie: refresco (HttpOnly)
  else Credenciales incorrectas
    A-->>C: 401 mensaje genérico e idéntico
  end

  C->>A: GET /api/pedidos (Authorization: Bearer tokenAcceso)
  A->>A: Verificar firma, exp, iss, aud
  A->>D: Consultar pedidos filtrando por req.usuario.id
  A-->>C: 200 [pedidos propios]

Fíjate en el último paso: la consulta filtra por el usuario autenticado, no compara después. Es la diferencia entre una API segura y una con IDOR.

  1. Datos personales y RGPD

Autenticar implica guardar datos personales, y eso tiene consecuencias legales además de técnicas.

  • Minimización: guarda solo lo imprescindible. Para vender entradas bastan correo, nombre y rol. Ni DNI, ni teléfono, ni fecha de nacimiento «por si acaso».
  • Finalidad: los datos recogidos para vender entradas no se usan para marketing sin consentimiento separado.
  • Derecho de supresión: debe existir una vía real para borrar una cuenta. Ojo: las entradas vendidas suelen tener obligaciones contables, así que lo habitual es anonimizar el usuario y conservar el pedido.
  • Contraseñas: un hash bcrypt es un dato personal en la práctica. Trátalo como tal.
  • Registros: los logs con correos e IPs también son datos personales; necesitan política de retención.
  • Notificación de brechas: si se filtran credenciales hay obligación de notificar en plazo. Tener el incidente previsto por escrito, antes de que ocurra, es parte del trabajo.

En Escena Viva usamos correos ficticios con dominio .test precisamente para no manejar datos reales durante el curso.

Errores Comunes y Consejos

  • Creer que 401 significa «sin permiso». Significa «no autenticado» (el nombre de la especificación es desafortunado). Sin permiso es 403. Confundirlos hace que los clientes redirijan al login a un usuario que ya ha entrado.
  • Guardar el rol en el cliente y confiar en él. Ocultar un botón en el front-end no es autorización: la petición se puede enviar igual con curl.
  • Usar localStorage para el token porque «es más cómodo». Un solo XSS lo convierte en un robo de identidad silencioso. Lo veremos en detalle en 08-04.
  • Añadir SameSite=None para «arreglar» un problema de CORS. Suele ser el síntoma de un diseño de dominios mal pensado, y abre la puerta a CSRF.
  • Pensar que TLS resuelve la autenticación. HTTPS protege el transporte, no dice quién eres ni qué puedes hacer.
  • Consejo: escribe el modelo de amenazas antes del código, aunque sean diez líneas en un fichero. Es lo que evita implementar defensas para ataques que no te afectan y olvidar el que sí.
  • Consejo: distingue siempre credencial de identificador. Si un dato basta para actuar en nombre de alguien, es una credencial y merece protección de credencial.

Ejercicios

Ejercicio 1: clasificar peticiones

Para cada una de estas situaciones de Escena Viva, indica si el fallo es de autenticación o de autorización, y qué código HTTP corresponde:

  1. Alguien llama a POST /api/compras sin ninguna credencial.
  2. Marc, con sesión válida, pide GET /api/pedidos/ped-042, que es de Lucía.
  3. El organizador de la Sala Bóveda intenta publicar evt-001, del Teatro Almendra.
  4. Se envía un token de acceso caducado.
  5. Un asistente intenta PATCH /api/usuarios/:id/rol.

Ejercicio 2: elegir estrategia

Justifica en dos o tres frases qué estrategia de la tabla de la sección 4 elegirías para:

  1. Un panel interno de administración de Escena Viva, servido por el propio servidor con plantillas HTML.
  2. Una futura app móvil de Escena Viva para validar entradas en la puerta del Auditorio Ribera.
  3. Un servicio de facturación que llama cada noche a GET /api/salas/:sala/ventas.

Ejercicio 3: cabecera Set-Cookie

Escribe la cabecera Set-Cookie completa para el token de refresco de Escena Viva sabiendo que: la API vive en api.escenaviva.test, el token solo debe enviarse a /auth/refrescar, dura 30 días, no debe ser legible por JavaScript, solo viaja por HTTPS y no queremos que se envíe desde otros sitios. Explica cada atributo.

Soluciones

Ejercicio 1

Caso Tipo Código
1 Autenticación (no hay identidad) 401
2 Autorización (identidad válida, recurso ajeno) 403 o 404 para no revelar existencia
3 Autorización (rol correcto, propiedad incorrecta) 403
4 Autenticación (credencial no válida ya) 401, indicando que ha caducado para que el cliente refresque
5 Autorización (rol insuficiente) 403

El caso 3 es especialmente instructivo: el rol organizador es correcto, y aun así la operación debe denegarse. RBAC solo no basta; hace falta comprobar la propiedad del recurso (08-05).

Ejercicio 2

  1. Sesiones de servidor con cookie. Es un único origen, el servidor renderiza HTML, el navegador gestiona la cookie sin código propio y la revocación es inmediata: si despides a un administrador, borras su sesión y queda fuera al instante.
  2. JWT de acceso corto + token de refresco guardado en el almacén seguro del sistema operativo. Las cookies no encajan fuera del navegador y el validador de entradas necesita funcionar con conectividad intermitente; un token firmado con caducidad corta se verifica sin consultar la base en cada lectura.
  3. Clave de API (o credenciales de cliente OAuth 2.0) con ámbito de solo lectura sobre ventas, rotable y revocable de forma independiente. No es una persona: no debe tener contraseña ni sesión.

Ejercicio 3

Set-Cookie: refresco=<token>; HttpOnly; Secure; SameSite=Strict; Path=/auth/refrescar; Max-Age=2592000
  • HttpOnly: document.cookie no la ve, así que un XSS no puede exfiltrarla.
  • Secure: solo por HTTPS; nunca en claro por la red.
  • SameSite=Strict: no se envía en peticiones originadas por otros sitios, lo que anula el CSRF sobre el endpoint de refresco.
  • Path=/auth/refrescar: no se adjunta a las llamadas normales de la API, reduciendo su exposición en logs y proxies.
  • Max-Age=2592000: 30 días en segundos; caducidad explícita en vez de cookie de sesión.
  • Sin Domain: la cookie queda ligada al host exacto api.escenaviva.test, sin propagarse a subdominios.

Conclusión

Ya tenemos el mapa. Autenticar es demostrar quién eres; autorizar es decidir qué puedes hacer; confundirlas es la causa de los agujeros más caros. HTTP no tiene estado, así que cada petición debe llevar una prueba de identidad, y las estrategias para lograrlo —básica, sesiones, JWT, claves de API, OAuth— tienen cada una su terreno. Las cookies, con sus atributos HttpOnly, Secure y SameSite, son la infraestructura sobre la que se apoya casi todo, y cada atributo neutraliza un ataque concreto. Conocemos el modelo de amenazas de Escena Viva, sabemos que no vamos a inventar criptografía y tenemos el diseño escrito: sesiones para entender, JWT con refresco en cookie para producir, RBAC más propiedad del recurso para autorizar.

Falta lo primero de todo: que exista una contraseña que verificar. El modelo Usuario lleva desde la lección 07-02 con su campo rol y sin ningún campo de contraseña, a propósito. En la siguiente lección, Registro de Usuarios y Hash de Contraseñas, llenamos ese hueco: por qué jamás se guarda una contraseña en claro ni con SHA-256, qué hace realmente bcrypt, cómo se elige su factor de coste, cómo se escribe un POST /auth/registro que no filtre qué correos existen, y cómo se hacen bien los tokens de verificación y de recuperación, que es donde más proyectos se rompen.

Curso de Node.js: De Principiante a Avanzado

Módulo 1: Introducción a Node.js

Módulo 2: Conceptos Básicos

Módulo 3: Sistema de Archivos y E/S

Módulo 4: HTTP y Servidores Web

Módulo 5: NPM y Gestión de Paquetes

Módulo 6: Framework Express.js

Módulo 7: Bases de Datos y ORMs

Módulo 8: Autenticación y Autorización

Módulo 9: Pruebas y Depuración

Módulo 10: Temas Avanzados

Módulo 11: Despliegue y DevOps

Módulo 12: Proyectos del Mundo Real

© Copyright 2026. Todos los derechos reservados