🛠 Unidad ISS-19 · Logout — capa 🛠 CONSTRUIR
Capa Página Para qué 🧠 Aprender — no está en la fuente comprender, explicar y relacionar 🛠 Construir esta página ejecutar, programar y verificar ✅ GATE Condiciones de cierre condición para pasar al bloque siguiente Esta es la guía ejecutable. El cuerpo de abajo es el ISS técnico verbatim: comandos, rutas, versiones, verificaciones y criterios, sin simplificar.
Guía de estudio. Esta unidad no tiene cuaderno en
material/django/iss/ISS-19/aprendizaje/. La página publica solo el ISS técnico.
ISS-19 — Logout
Objetivo
Invalidar el refresh presentado, sin guardar el access token.
Requisitos
- ISS-18 superado.
Construcción
LogoutAPIView es OPEN. Recibe {"refresh": "..."}. La clase se agrega en el archivo de vistas, que ya tiene login y refresh.
ARCHIVO: apps/security/auth_views.py
UBICAR el import de serializers y AGREGAR LogoutSerializer.
ENCIMA DE:
def _find_user(username, email):
AGREGAR LogoutAPIView. Si el hash no existe, 401. Si el token ya fue
reemplazado, revoca la familia y responde 401. En cualquier otro caso
lo deja inactive, escribe revoked_at y responde 204.
ARCHIVO: apps/security/urls.py
UBICAR:
from apps.security.auth_views import LoginAPIView, RefreshAPIView
REEMPLAZAR POR:
from apps.security.auth_views import LoginAPIView, LogoutAPIView, RefreshAPIView
DEBAJO DE:
path("auth/refresh/", RefreshAPIView.as_view(), name="auth-refresh"),
AGREGAR:
path("auth/logout/", LogoutAPIView.as_view(), name="auth-logout"),
hash desconocido → 401
token ya reemplazado → revoca la familia → 401
en cualquier otro caso → status inactive, revoked_at = ahora → 204
Un segundo logout del mismo token, si no fue rotado, vuelve a dejarlo inactivo y responde 204.
La prueba sigue en OPEN.
ARCHIVO: apps/security/tests.py
AL FINAL DE class AuthFlowTests, AGREGAR:
def test_logout_invalidates_refresh(self):
login = self.client.post(
"/api/auth/login/",
{"username": "ana", "password": PASSWORD},
format="json",
)
raw = login.data["refresh"]
logout = self.client.post("/api/auth/logout/", {"refresh": raw}, format="json")
self.assertEqual(logout.status_code, status.HTTP_204_NO_CONTENT)
again = self.client.post("/api/auth/refresh/", {"refresh": raw}, format="json")
self.assertEqual(again.status_code, status.HTTP_401_UNAUTHORIZED)
Explicación
Logout no pide access token. La credencial de la sesión persistida es el refresh. Cerrar sesión en un dispositivo no tiene por qué cerrar otra familia. Si el valor presentado ya fue rotado, se trata como reuso y sí se apaga la familia: alguien está presentando un token que ya no debía existir.
No hay tabla de access tokens que limpiar.
Cómo probarlo
Capa HTTP, acceso OPEN. Logout no lleva Bearer. El cuerpo lleva el refresh que acaba de emitir el login.
python manage.py test apps.security.tests.AuthFlowTests.test_logout_invalidates_refresh
│
▼
login → refresh R fila active en refresh_tokens
│
▼
POST /api/auth/logout/ {refresh: R} 204
│
▼
POST /api/auth/refresh/ {refresh: R} 401
En Swagger, sin Authorize: login, copie refresh, POST /api/auth/logout/ y vuelva a presentarlo en POST /api/auth/refresh/. La segunda llamada responde 401.
Criterios de aceptación
- AC-19-01: logout responde 204.
- AC-19-02: ese refresh ya no puede renovar: el siguiente refresh responde 401.
Verificación
test_logout_invalidates_refresh.
Evidencias
- EVI-19-01: test PASS.
GATE
| AC | Verificación | Evidencia | Resultado |
|---|---|---|---|
| AC-19-01 | HTTP 204 | EVI-19-01 | PASS |
| AC-19-02 | refresh posterior 401 | EVI-19-01 | PASS |
✅ GATE de la unidad ISS-19 — este bloque no añade ningún criterio nuevo.
Las condiciones de cierre son las de esta misma página:
- 📋 Criterios de aceptación → Criterios de aceptación
- ✅ GATE → GATE
- 🔎 Verificación → Verificación
Con el GATE en verde queda habilitado el bloque siguiente de la ruta.
Navegación de la ruta: ← ISS-18 · 🛠 Construir · ↑ Ruta Django · → ISS-20 · 🛠 Construir