Login: refresh rotation + detección de reuso
Objetivo
Rotación de refresh tokens con detección de robo/replay, sin necesidad de que el usuario haga nada.
Cómo empieza
POST /auth/login con email+password válidos y sin 2FA → IssueSessionUseCase crea una Session en Redis y firma access+refresh.
Pasos principales
POST /auth/refreshllega con un refresh token.RefreshUseCaselo verifica (firma JWT) y busca la sesión por el hash del token (sessions.findByRefreshTokenHash) — el token en claro nunca se guarda.- Si la sesión existe y está activa: se rota — nueva
Session(nuevo id, nuevo hash), la vieja se reemplaza (sessions.rotate). El refresh token usado deja de ser válido para siempre. - Si el token tiene firma válida pero no mapea a ninguna sesión activa → ya fue rotado antes. Eso es reuso:
sessions.revokeAllForUser(sub)yTokenReusedError. Mata todas las sesiones del usuario, no solo esa. - Sesión encontrada pero marcada
revoked→ mismo tratamiento (revoke-all + error), es la señal más fuerte de robo. - Sesión encontrada pero solo expirada (no revocada) → caída normal, revoca solo esa sesión, error distinto (
UnauthorizedError, noTokenReusedError).
Reglas de negocio
- La detección de reuso solo funciona porque cada rotación invalida el token anterior — si se reutilizara el mismo refresh en varias renovaciones, esto no detectaría nada.
Interacciones
Ninguna externa — todo vive en Redis (RedisSessionRepository), sin tocar Postgres.
Dónde seguir mirando
petid-api/src/application/auth/refresh.use-case.ts(la lógica completa está ahí, es corta)petid-api/src/application/auth/issue-session.use-case.ts(emisión inicial, compartida por login normal y login 2FA)login.use-case.ts
Casos especiales
LoginUseCaseverifica el hash contra unDUMMY_HASHconstante incluso cuando el email no existe (this.hasher.verify(DUMMY_HASH, cmd.password)) — mitigación de timing attack para que no se pueda enumerar emails midiendo cuánto tarda la respuesta.- Distinguir “reuso” (revoke-all) de “expiró sola” (revoke individual) importa para no castigar de más a un usuario que simplemente dejó el refresh token caducar.