Clase 066 — Sesión con cookie#
⬅️ 065 · 📚 Parte 5 · 🎓 Clases · 067 ➡️ Parte 5 — Identidad y seguridad · Nivel 🟡 intermedio · Pista
backend✅ Clase construida — 4 implementaciones verificadas contracontrato.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=LaxHttpOnly— el script de la página no puede leerla. Un XSS ya no puede exfiltrar la sesión condocument.cookie(puede seguir usándola desde la página; la clase 073 vuelve sobre esto).SameSite=Lax— la cookie no viaja en peticiones que otra página provoca, salvo la navegación de nivel superior. Es la primera línea contra el CSRF, y la clase 072 mide qué cubre y qué no.Path=/— delimita dónde viaja. Aquí, toda la aplicación.Secure— solo sobre HTTPS [rfc8446]. El laboratorio corre sobre HTTP en127.0.0.1, así que exigirlo en el contrato sería afirmar sin medir: se queda en la prosa, pero en producción no es opcional [owasp-cheatsheets].
<!-- 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.
- Documentación oficial: https://expressjs.com/
- Estado en el catálogo: activo
- Versión que ejecuta esta clase:
express ^5.1.0, express-session ^1.18.1 - Necesita en el PATH:
node,pnpm
Preparar sus dependencias, dentro de su directorio:
pnpm install --silent --ignore-scriptsArrancarla suelta, sin el verificador:
PORT=3000 node server.mjsQué 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.
- Documentación oficial: https://fastapi.tiangolo.com/
- Estado en el catálogo: activo
- Versión que ejecuta esta clase:
fastapi>=0.115, uvicorn>=0.30 - Necesita en el PATH:
python
Arrancarla suelta, sin el verificador:
PORT=3000 python -m uvicorn main:app --host 127.0.0.1 --port 3000Qué 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.
- Documentación oficial: https://spring.io/projects/spring-boot
- Estado en el catálogo: activo
- Versión que ejecuta esta clase:
spring-boot 3.5.6, Java 21, spring-boot-starter-web - Necesita en el PATH:
java,mvn
Preparar sus dependencias, dentro de su directorio:
mvn -q -B package -DskipTestsArrancarla suelta, sin el verificador:
PORT=3000 java -jar target/clase-066-1.0.0.jar --server.port=3000Qué 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.
- Documentación oficial: https://learn.microsoft.com/aspnet/core/
- Estado en el catálogo: activo
- Versión que ejecuta esta clase:
net10.0 - Necesita en el PATH:
dotnet
Preparar sus dependencias, dentro de su directorio:
dotnet build -c Release --nologo -v quietArrancarla suelta, sin el verificador:
PORT=3000 dotnet run -c Release --no-build --urls http://127.0.0.1:3000Qué 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.mjsdice 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#
- Guardar datos en la cookie para «ahorrar» el almacén. Funciona hasta que hay que revocar. Si el estado viaja con el cliente, cerrar sesión es una sugerencia.
- Cerrar sesión borrando solo la cookie. El navegador la tira; la copia robada sigue abriendo. La sesión se mata en el servidor [owasp-cheatsheets].
- Conservar el identificador de antes de autenticar. Es la fijación de sesión: el atacante planta un identificador, la víctima entra, y el atacante ya está dentro [hoffman-web-application-security].
- Identificadores adivinables. Un contador o un
randomcorriente en vez del generador criptográfico. NIST pide identificadores impredecibles y con entropía suficiente [nist-800-63b]. - Sin
HttpOnly«para leerla desde el frontend». Si el frontend necesita saber quién eres, se lo dice un endpoint (/perfil), no la cookie. - Sesiones sin caducidad. Este laboratorio la omite deliberadamente para no medir relojes; en producción, caducidad absoluta y de inactividad [nist-800-63b].
✅ Verificación#
node scripts/run-class.mjs 066Los 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#
- Por qué sí y por qué no
- Clase 067 — Token de acceso — la alternativa sin estado, y qué se pierde al elegirla
- Clase 072 — CSRF — el ataque que explota que la cookie viaja sola
- Clase 034 — Limitación de tasa — la misma lección sobre estado en el proceso
Fuentes#
- [rfc6265] RFC 6265 — HTTP State Management Mechanism. IETF, 2011 — https://www.rfc-editor.org/rfc/rfc6265
- [owasp-cheatsheets] OWASP Cheat Sheet Series (Session Management). OWASP — https://cheatsheetseries.owasp.org/
- [nist-800-63b] SP 800-63B — Digital Identity Guidelines: Authentication and Lifecycle Management. NIST — https://pages.nist.gov/800-63-3/sp800-63b.html
- [hoffman-web-application-security] Hoffman, Andrew. Web Application Security. O'Reilly Media, 2020. ISBN 9781492053118 — https://openlibrary.org/isbn/9781492053118
- [rfc8446] RFC 8446 — The Transport Layer Security (TLS) Protocol Version 1.3. IETF, 2018 — https://www.rfc-editor.org/rfc/rfc8446