Saltar al contenido
Framework Ecosystems LabsUn contrato, muchos ecosistemas, la misma prueba.

Clase 066 — Sesión con cookie#

⬅️ 065 · 📚 Parte 5 · 🎓 Clases · 067 ➡️ Parte 5 — Identidad y seguridad · Nivel 🟡 intermedio · Pista backendClase construida — 4 implementaciones verificadas contra contrato.json.

🎯 Objetivo#

Recordar al usuario entre peticiones sin exponerlo. HTTP no tiene memoria; la cookie es el mecanismo estándar para dársela [rfc6265], y casi todo lo que puede salir mal está en los detalles: qué viaja en la cookie, con qué atributos, y qué pasa de verdad al cerrar sesión.

🧩 La situación#

Un usuario entra con su contraseña y el servidor le da una cookie. Con ella, /perfil responde quién es. Al salir, la cookie muere — y no solo en el navegador: la copia que un atacante hubiera robado tiene que dejar de abrir la puerta.

La regla que ordena toda la clase: la cookie identifica, no cuenta. A la cookie viaja un identificador opaco; los datos —quién eres, qué puedes hacer— viven en el servidor. En cuanto los datos viajan dentro de la cookie, el servidor pierde la capacidad de retirarlos.

🧮 El contrato#

Petición Respuesta
GET /perfil sin cookie 401
POST /entrar con credenciales malas 401, sin Set-Cookie
POST /entrar con credenciales buenas 200 · cookie sesion con HttpOnly, SameSite=Lax, Path=/ y sin la contraseña dentro
GET /perfil con la cookie 200 · {"usuario": "ana"}
POST /entrar trayendo sesion=fijada-por-el-atacante la cookie emitida no conserva ese valor
POST /salir 204 · cookie con Max-Age=0 o Expires en el pasado
GET /perfil reenviando la cookie vieja 401

Los dos últimos casos son la pareja que mide de verdad. El sexto comprueba que el navegador recibe la orden de tirar la cookie; el séptimo hace lo que haría un atacante con una copia robada: reenviarla después del cierre. Si el estado de la sesión viviera dentro de la cookie, ese caso no podría pasar — no habría nada en el servidor que borrar.

Y el quinto mide la fijación de sesión: el servidor no debe adoptar un identificador que trae el cliente; el que emite al autenticar tiene que ser suyo y nuevo [owasp-cheatsheets].

Para poder medir esto, el verificador ganó un tarro de cookies explícito: cada caso declara si viaja con las cookies guardadas (cookies: true) y solo quien declara guardar_cookies escribe en el tarro. Así el último caso puede reenviar deliberadamente una cookie que el servidor ya dio por muerta.

🔬 Los atributos, uno a uno#

Set-Cookie: sesion=k3o0…Zw; Path=/; HttpOnly; SameSite=Lax

<!-- generado: fichas -->

📖 Las palabras que esta clase define#

Si alguna de estas no te dice nada todavía, esta es la clase donde se aprende. Las definiciones viven en el glosario, que reúne las del programa entero.

Palabra Qué significa
Sesión Estado de un usuario guardado en el servidor, identificado por una cookie opaca. Es lo que permite cerrar sesión de verdad: se borra la entrada del almacén y la cookie robada deja de abrir.
Cookie Un dato que el servidor pide al navegador que guarde y le devuelva en cada petición. Sus atributos no son decoración: HttpOnly la esconde del script de la página y SameSite evita que viaje en peticiones que provoca otra página.
Fijación de sesión El ataque en que alguien planta un identificador de sesión antes del inicio de sesión y luego lo reutiliza ya autenticado. Se cierra emitiendo un identificador nuevo al autenticar.

🧰 Las piezas de esta clase, una por una#

Antes del código: qué es cada framework, qué versión se está usando y qué hace falta para ejecutarlo. Todo lo de esta sección sale de los archivos reales del repositorio —el catálogo, la receta de arranque y el manifiesto de dependencias de cada ecosistema—, así que no puede quedarse desactualizado sin que la validación lo detecte.

Framework Qué es Desde Licencia Quién lo mantiene
Express framework web de Node.js (JavaScript) 2010 MIT OpenJS Foundation
FastAPI framework web de Python (Python) 2018 MIT proyecto independiente
Spring Boot framework de aplicación de JVM (Java) 2014 Apache-2.0 Broadcom/VMware y colaboradores
ASP.NET Core framework web de .NET (C#) 2016 MIT Microsoft y .NET Foundation

🔧 Express#

Definió el modelo de middleware encadenado que copiaron casi todos los frameworks de Node.js. Minimalista no significa biblioteca: posee el bucle de peticiones.

Preparar sus dependencias, dentro de su directorio:

pnpm install --silent --ignore-scripts

Arrancarla suelta, sin el verificador:

PORT=3000 node server.mjs

Qué hay dentro de su directorio:

Archivo Qué es
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
package.json manifiesto de Node.js: nombre, tipo de módulo y dependencias con su rango de versión
pnpm-lock.yaml archivo de bloqueo: la versión exacta de cada dependencia y de sus dependencias
pnpm-workspace.yaml raíz de instalación propia, y la prohibición de ejecutar scripts al instalar
server.mjs código JavaScript (módulo ES)

🔧 FastAPI#

Deriva validación, serialización y documentación OpenAPI de las anotaciones de tipo. Demostró que el tipado opcional de Python podía ser infraestructura, no adorno.

Arrancarla suelta, sin el verificador:

PORT=3000 python -m uvicorn main:app --host 127.0.0.1 --port 3000

Qué hay dentro de su directorio:

Archivo Qué es
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
main.py código Python
requirements.txt dependencias de Python, una por línea, con versión fijada

🔧 Spring Boot#

Autoconfiguración y servidor incrustado sobre Spring. Convirtió un framework famoso por su configuración XML en uno de arranque inmediato.

Preparar sus dependencias, dentro de su directorio:

mvn -q -B package -DskipTests

Arrancarla suelta, sin el verificador:

PORT=3000 java -jar target/clase-066-1.0.0.jar --server.port=3000

Qué hay dentro de su directorio:

Archivo Qué es
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
pom.xml manifiesto de Maven: el proyecto, su Java, sus dependencias y cómo se empaqueta
src/main/java/labs/Aplicacion.java código Java
src/main/resources/application.properties configuración de Spring Boot: lo que se ajusta sin tocar el código

🔧 ASP.NET Core#

Reescritura multiplataforma y de código abierto de la pila web de Microsoft. Sus API mínimas trajeron el estilo de los microframeworks al ecosistema .NET.

Preparar sus dependencias, dentro de su directorio:

dotnet build -c Release --nologo -v quiet

Arrancarla suelta, sin el verificador:

PORT=3000 dotnet run -c Release --no-build --urls http://127.0.0.1:3000

Qué hay dentro de su directorio:

Archivo Qué es
Clase066.csproj proyecto de .NET: el marco de destino y las dependencias
Program.cs código C#
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca

Si alguna cadena de herramientas no está en tu máquina, node scripts/doctor.mjs dice cuál falta y con qué comando se instala. No hace falta tenerlas todas: el verificador ejecuta lo que encuentra y declara lo que omitió.

<!-- fin generado: fichas -->

🌐 Las implementaciones — el código a la vista#

Tres frameworks traen la pieza de serie y uno no la trae, y esa asimetría es el hallazgo de la clase. Cada bloque es el archivo real del directorio implementaciones/.

Express · express/server.mjs#

app.use(
  session({
    name: "sesion",
    secret: "clave-de-firma-solo-para-el-laboratorio",
    resave: false,
    saveUninitialized: false,
    cookie: { httpOnly: true, sameSite: "lax", path: "/" },
  }),
);

express-session guarda la sesión en el servidor y a la cookie solo viaja el identificador, firmado con el secreto. Por eso cerrar sesión puede invalidar de verdad.

saveUninitialized: false es la línea que más se olvida: sin ella, una visita anónima crea sesión y recibe cookie. Menos estado y menos superficie.

Los dos gestos que exige el contrato:

  peticion.session.regenerate((error) => {
    if (error) return respuesta.status(500).json({ error: "sesion" });
    peticion.session.usuario = usuario;
  peticion.session.destroy(() => {
    respuesta.clearCookie("sesion", { path: "/" });
    respuesta.status(204).end();
  });

regenerate descarta el identificador con el que llegó la petición y emite uno nuevo: es la defensa contra la fijación de sesión. destroy borra la entrada del almacén y clearCookie le dice al navegador que tire la suya — hacen falta los dos, y el orden de importancia es ese.

FastAPI · fastapi/main.py#

FastAPI no trae sesiones de servidor. Lo que ofrece su ecosistema —el SessionMiddleware de Starlette— guarda los datos dentro de la cookie, firmados, y ese diseño no puede pasar el último caso del contrato: tras cerrar sesión no hay nada en el servidor que borrar. Así que la implementación compone la pieza que falta:

sesiones: dict[str, str] = {}
    identificador = secrets.token_urlsafe(32)
    sesiones[identificador] = credenciales.usuario

    respuesta = JSONResponse({"usuario": credenciales.usuario})
    respuesta.set_cookie(
        key="sesion",
        value=identificador,
        httponly=True,   # el script de la página no puede leerla
        samesite="lax",  # no viaja en peticiones que otra página provoca
        path="/",
    )

Se ignora cualquier cookie que traiga la petición y se emite un identificador nuevo en cada inicio: la fijación muere ahí. token_urlsafe sale del generador criptográfico del sistema; un contador o un random corriente serían adivinables.

Y el cierre, que es donde se ve por qué el estado tiene que vivir en el servidor:

    if sesion:
        sesiones.pop(sesion, None)
    respuesta = Response(status_code=204)
    respuesta.delete_cookie(key="sesion", path="/")

Sin el pop, una copia robada de la cookie seguiría abriendo la puerta.

Spring Boot · spring-boot/…/Aplicacion.java#

        HttpSession sesion = peticion.getSession(true);
        peticion.changeSessionId();
        sesion.setAttribute("usuario", usuario);

El almacén no lo pone Spring: lo pone el contenedor, Tomcat. HttpSession es la API de Servlet, anterior a Spring y compartida por todo el ecosistema JVM. changeSessionId() es el equivalente exacto del regenerate de Express.

getSession(true) crea si no existe; en /perfil se usa getSession(false) justo por lo contrario:

        HttpSession sesion = peticion.getSession(false);
        Object usuario = sesion == null ? null : sesion.getAttribute("usuario");

Y el detalle que esta clase existe para enseñar:

        if (sesion != null) {
            sesion.invalidate();
        }
        Cookie borrado = new Cookie("sesion", "");
        borrado.setMaxAge(0);
        borrado.setPath("/");
        respuesta.addCookie(borrado);

invalidate() borra el almacén, pero no ordena al navegador tirar la cookie. Esa cabecera de borrado se emite a mano. Es la única de las cuatro implementaciones donde el framework hace medio trabajo y no avisa.

ASP.NET Core · aspnet-core/Program.cs#

constructor.Services.AddDistributedMemoryCache();
constructor.Services.AddSession(opciones =>
{
    opciones.Cookie.Name = "sesion";
    opciones.Cookie.HttpOnly = true;
    opciones.Cookie.SameSite = SameSiteMode.Lax;
    opciones.Cookie.Path = "/";
    opciones.Cookie.IsEssential = true;
});

La sesión se apoya en IDistributedCache: aquí una caché en memoria, en producción una compartida. IsEssential = true es la trampa propia de este framework — sin ella la cookie queda sujeta a la política de consentimiento y el middleware puede decidir no emitirla.

    contexto.Session.SetString("usuario", usuario);

No hay regenerate porque no hace falta: el identificador viaja protegido con Data Protection, y un valor que este servidor no emitió no descifra. La fijación se cierra por construcción, no por un gesto que haya que acordarse de escribir.

    contexto.Session.Clear();
    contexto.Response.Cookies.Delete("sesion", new CookieOptions { Path = "/" });

Los mismos dos gestos que en Express, con los mismos dos motivos.

📊 Comparación#

Framework La pieza Dónde vive el estado Fijación Invalidar al salir
Express express-session almacén del middleware regenerate() explícito destroy() + clearCookie()
FastAPI no la trae: se compone diccionario propio identificador nuevo en cada inicio pop + delete_cookie()
Spring Boot HttpSession (Tomcat) contenedor de servlets changeSessionId() explícito invalidate() + cookie a mano
ASP.NET Core AddSession() IDistributedCache un valor ajeno no descifra Clear() + Cookies.Delete()

En los cuatro, el estado en memoria comparte la limitación de las clases 034 y 047: con dos instancias, cada una tendría sus sesiones. En producción el almacén es compartido — Redis es lo habitual, y en ASP.NET Core es literalmente cambiar el registro de IDistributedCache.

⚠️ Errores frecuentes#

✅ Verificación#

node scripts/run-class.mjs 066

Los casos están en contrato.json. El verificador ejecuta las implementaciones que encuentre y declara las que omitió.

🧪 Reto de transferencia#

Añade GET /sesiones que liste las sesiones vivas del usuario autenticado y POST /salir-de-todas que las mate todas — el botón «cerrar sesión en todos los dispositivos». Exige cambiar el almacén: de identificador → usuario a poder buscar por usuario. Comprueba con el contrato que tras salir de todas, ninguna cookie antigua vale.

🔗 Enlaces#

Fuentes#