Skip to content

feat(auth): revocación server-side de tokens JWT (denylist) - #69

Merged
MrDylanDev merged 2 commits into
mainfrom
feat/auth-jwt-revocation
Aug 12, 2026
Merged

feat(auth): revocación server-side de tokens JWT (denylist)#69
MrDylanDev merged 2 commits into
mainfrom
feat/auth-jwt-revocation

Conversation

@MrDylanDev

Copy link
Copy Markdown
Owner

Closes #57

PR Type

  • New feature
  • Bug fix
  • Documentation only
  • Code refactoring
  • Maintenance/tooling
  • Breaking change

Summary

  • Cada JWT incluye claim jti (UUID) para revocación individual.
  • Nueva tabla tokens_revocados (migración Flyway V4) + entidad + repositorio.
  • JwtAuthenticationFilter: tras validar la firma, rechaza tokens cuyo jti esté en la denylist o emitidos antes del último cambio de contraseña (401 anónimo).
  • logout() ahora revoca el token server-side, además de borrar la cookie.
  • resetPassword() invalida todas las sesiones previas del usuario: si te roban la cookie y cambiás la contraseña, el token robado muere.
  • Purge periódico de tokens expirados (@Scheduled, cada hora).

Diseño (decisión del issue)

Se eligió denylist en BD sobre refresh-token rotation, como el issue permite para una app de biblioteca:

  • El token revocado vive en la denylist hasta su expiry natural (1-24h). No hay invalidación instantánea sin cambiar jwt.secret (trade-off de JWT stateless, aceptado).
  • El mecanismo de marcador por email (comparación de iat) permite invalidar sesiones activas en resetPassword sin necesidad de rotación de refresh.

Changes

File Change
src/main/resources/db/migration/V4__tokens_revocados.sql Tabla de denylist
src/main/java/com/libromagico/model/TokenRevocado.java Entidad
src/main/java/com/libromagico/repository/TokenRevocadoRepository.java Repositorio (existsByJti, deleteByEmail, purge)
src/main/java/com/libromagico/service/TokenRevocationService.java revocarToken, estaRevocado, marcador por email, purge
src/main/java/com/libromagico/security/JwtTokenProvider.java Claim jti + getters de jti/iat/expiración
src/main/java/com/libromagico/security/JwtAuthenticationFilter.java Chequeo de denylist tras validar firma
src/main/java/com/libromagico/security/AuthController.java Logout revoca server-side
src/main/java/com/libromagico/service/UsuarioService.java resetPassword invalida sesiones previas
src/test/java/com/libromagico/security/TokenRevocationIntegrationTest.java 4 escenarios E2E

Test Plan

  • mvn -B -Pcoverage verify: 181 tests (177 previos + 4 nuevos), 0 fallos
  • Coverage gate OK (>=70%)
  • Flujo: login → logout → token revocado 401 en /me y en rutas protegidas; token vigente 200; resetPassword invalida sesión previa

Contributor Checklist

- Cada JWT incluye claim jti (UUID) para revocación individual.
- Tabla tokens_revocados (Flyway V4) + entidad + repositorio.
- JwtAuthenticationFilter: tras validar firma, rechaza tokens cuyo jti
  esté en la denylist o emitidos antes del último cambio de contraseña.
- Logout revoca el token server-side además de borrar la cookie.
- resetPassword invalida todas las sesiones previas del usuario (marcador
  por email: tokens emitidos antes del cambio -> 401).
- Purge periódico de tokens expirados (@scheduled, cada hora en :30).
- TokenRevocationIntegrationTest: 4 escenarios (logout revoca, token
  vigente autentica, ruta protegida 401 tras logout, resetPassword
  invalida sesiones previas).

181 tests verdes, coverage gate OK (>=70%).
The derived deleteByEmail deferred the DELETE SQL to flush, so the
deterministic marker insert (jti=SHA-256(email)) collided with the
existing row on the second consecutive password reset for the same
user, raising a unique constraint violation masked as a 401.

Execute the delete as a bulk JPQL query so the DELETE runs before the
marker INSERT. Add a regression test for the double-reset scenario.
@MrDylanDev
MrDylanDev merged commit edb0a79 into main Aug 12, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

1 participant