morphos / backend /tests /test_revocacion_sesiones.py
Jose Salazar
Sesiones revocables: logout real y cierre en todos los dispositivos
2f5c422
Raw
History Blame Contribute Delete
3.75 kB
"""Revocaci贸n de sesiones (ARCHITECTURE_REVIEW 搂2.3).
Las sesiones son cookies firmadas sin estado, y el logout se limitaba a borrar la cookie del
navegador: el token segu铆a siendo V脕LIDO all谩 donde se hubiera copiado, hasta
`session_max_age_s` (8h). No hab铆a forma de invalidar una sesi贸n filtrada, ni de echar a nadie
tras un incidente, salvo rotar `MORPHOS_SESSION_SECRET` y tirar a todos los usuarios a la vez.
La prueba que importa no es 芦logout devuelve 200禄, sino que **la cookie de antes ya no sirve**.
"""
from __future__ import annotations
import pytest
from fastapi.testclient import TestClient
from app.main import app
@pytest.fixture
def cliente():
with TestClient(app) as c:
yield c
def _alta(cliente, email):
r = cliente.post(
"/api/auth/registro",
json={"nombre": "V", "apellido": "Vet", "email": email, "password": "clave-segura-1"},
)
if r.status_code == 409:
r = cliente.post("/api/auth/login", json={"email": email, "password": "clave-segura-1"})
assert r.status_code == 200, r.text
return r
def test_la_cookie_deja_de_valer_tras_el_logout(cliente, alta_abierta):
"""El agujero concreto: una copia de la cookie sobreviv铆a al 芦cerrar sesi贸n禄."""
_alta(cliente, "rev1@example.com")
galleta = cliente.cookies.get("morphos_sesion")
assert cliente.get("/api/auth").json()["autenticado"] is True
cliente.post("/api/auth/logout")
# Se reenv铆a la cookie a mano, como har铆a quien la hubiera copiado.
r = cliente.get("/api/lab/pendientes", cookies={"morphos_sesion": galleta})
assert r.status_code == 401
def test_logout_todas_corta_las_demas_sesiones(cliente, alta_abierta):
"""芦Me han robado el port谩til禄: cierra las sesiones abiertas en otros dispositivos."""
_alta(cliente, "rev2@example.com")
portatil = cliente.cookies.get("morphos_sesion")
# Segunda sesi贸n (otro dispositivo) para la misma cuenta.
otro = TestClient(app)
r = otro.post("/api/auth/login", json={"email": "rev2@example.com", "password": "clave-segura-1"})
assert r.status_code == 200
otro.post("/api/auth/logout-todas")
r = cliente.get("/api/lab/pendientes", cookies={"morphos_sesion": portatil})
assert r.status_code == 401, "la sesi贸n del otro dispositivo sigui贸 viva"
def test_revocar_una_no_afecta_a_las_de_otras_cuentas(cliente, alta_abierta):
"""Aislar el corte: revocar no puede convertirse en una denegaci贸n de servicio ajena."""
_alta(cliente, "rev3@example.com")
ajena = TestClient(app)
_alta(ajena, "rev4@example.com")
cliente.post("/api/auth/logout-todas")
assert ajena.get("/api/auth").json()["autenticado"] is True
def test_una_sesion_nueva_tras_el_corte_si_vale(cliente, alta_abierta):
"""El corte es por fecha de emisi贸n: volver a entrar tiene que funcionar."""
_alta(cliente, "rev5@example.com")
cliente.post("/api/auth/logout-todas")
r = cliente.post(
"/api/auth/login", json={"email": "rev5@example.com", "password": "clave-segura-1"}
)
assert r.status_code == 200
assert cliente.get("/api/auth").json()["autenticado"] is True
def test_el_logout_borra_las_cookies_con_sus_atributos(cliente, alta_abierta):
"""`delete_cookie()` a secas emite un Set-Cookie sin samesite/path y algunos navegadores lo
tratan como una cookie distinta: la sesi贸n se quedaba en el navegador."""
_alta(cliente, "rev6@example.com")
r = cliente.post("/api/auth/logout")
borrados = [v for v in r.headers.get_list("set-cookie") if "morphos_sesion" in v]
assert borrados, "el logout no intent贸 borrar la cookie de sesi贸n"
assert "Path=/" in borrados[0]
assert "samesite=strict" in borrados[0].lower()