UDT-002: Logout + Refresh Token con rotación y chain revocation #3

Merged
dmolinari merged 36 commits from feature/UDT-002 into main 2026-04-14 17:37:47 +00:00
Owner

Summary

  • Implementa refresh token opaque 256-bit hasheado con SHA-256, persistido en BD con familia de rotación y chain revocation en reuso
  • Agrega endpoints /api/v1/auth/refresh y /api/v1/auth/logout con autenticación segura y detección de token reuse
  • Frontend: authStore extendido con refreshToken + expiresAt persistidos, interceptor axios con singleton promise queue para resolver 401 en paralelo
  • Expiración absoluta 7 días (no sliding) — cada rotación hereda fecha de expiración original
  • 36 commits TDD desde origin/main: domain → application → infrastructure → API → tests backend + frontend

Highlights

  • Opaque 256-bit refresh token: NO JWT — permite revocación efectiva. BD guarda solo hash SHA-256, cliente guarda raw.
  • Absolute 7d expiry heredado: Cada rotación mantiene ExpiresAt original — cierra vector de extensión indefinida via rotaciones sucesivas.
  • Chain revocation por FamilyId: Si un token revocado se reutiliza → se revocan todos los activos de esa familia + usuario debe re-loguearse.
  • Singleton promise queue frontend: Un solo POST /refresh en vuelo aunque N requests fallen con 401 simultáneamente. Patrón módulo-scope resuelve race condition de refresco paralelo.

Tests

Suite Status Count
SIGCM2.Application.Tests PASS 64/64
SIGCM2.Api.Tests ⚠️ 6 pass, 2 pre-existentes* 6/8
Frontend (Vitest) PASS 27/27
Total 97 pass, 2 pre-existentes 97/99

*Los 2 failures son pre-existentes del UDT-001 (Login tests con BD connection issue via TestHost) — NO hay regresión nueva.

Follow-ups (deuda técnica, no bloqueante)

  • W-01: Refactorizar exclusión /auth/refresh en axiosClient de includes() a endsWith() para robustez futura
  • W-02: Alinear LogoutResponseDto.mensaje con contrato simplificado de API docs
  • W-03: Mapear /auth/logout en MSW del test para limpiar stderr
  • W-04: Aplicar migración V002 a SIGCM2 productivo (T-003, requiere confirmación usuario)

Smoke test

Ver docs/smoke-test-udt-002.md para 7 escenarios de validación manual:

  1. Login → persistencia de tokens en localStorage
  2. Expiración y refresh automático silencioso
  3. Logout → revocación de familia
  4. Intento de reuso → 401 + revocación familia
  5. Verificación de hash en BD vs raw token
  6. Token raw NUNCA en logs
  7. ClockSkew = 0 → expiración puntual

Branch: feature/UDT-002 (36 commits desde origin/main)
Migración: V002__create_refresh_token.sql incluida
Tests: 100% TDD (RED → GREEN para cada capa)
Seguridad: 0 tokens raw en BD, 0 raw en logs, chain revocation en reuso

Pronto para review y merge a main.

## Summary - Implementa refresh token opaque 256-bit hasheado con SHA-256, persistido en BD con familia de rotación y chain revocation en reuso - Agrega endpoints `/api/v1/auth/refresh` y `/api/v1/auth/logout` con autenticación segura y detección de token reuse - Frontend: authStore extendido con refreshToken + expiresAt persistidos, interceptor axios con singleton promise queue para resolver 401 en paralelo - Expiración absoluta 7 días (no sliding) — cada rotación hereda fecha de expiración original - 36 commits TDD desde origin/main: domain → application → infrastructure → API → tests backend + frontend ## Highlights - **Opaque 256-bit refresh token**: NO JWT — permite revocación efectiva. BD guarda solo hash SHA-256, cliente guarda raw. - **Absolute 7d expiry heredado**: Cada rotación mantiene ExpiresAt original — cierra vector de extensión indefinida via rotaciones sucesivas. - **Chain revocation por FamilyId**: Si un token revocado se reutiliza → se revocan todos los activos de esa familia + usuario debe re-loguearse. - **Singleton promise queue frontend**: Un solo POST /refresh en vuelo aunque N requests fallen con 401 simultáneamente. Patrón módulo-scope resuelve race condition de refresco paralelo. ## Tests | Suite | Status | Count | |---|---|---| | SIGCM2.Application.Tests | ✅ PASS | 64/64 | | SIGCM2.Api.Tests | ⚠️ 6 pass, 2 pre-existentes* | 6/8 | | Frontend (Vitest) | ✅ PASS | 27/27 | | **Total** | **97 pass, 2 pre-existentes** | **97/99** | *Los 2 failures son pre-existentes del UDT-001 (Login tests con BD connection issue via TestHost) — NO hay regresión nueva. ## Follow-ups (deuda técnica, no bloqueante) - **W-01**: Refactorizar exclusión `/auth/refresh` en axiosClient de `includes()` a `endsWith()` para robustez futura - **W-02**: Alinear `LogoutResponseDto.mensaje` con contrato simplificado de API docs - **W-03**: Mapear `/auth/logout` en MSW del test para limpiar stderr - **W-04**: Aplicar migración V002 a SIGCM2 productivo (T-003, requiere confirmación usuario) ## Smoke test Ver `docs/smoke-test-udt-002.md` para 7 escenarios de validación manual: 1. Login → persistencia de tokens en localStorage 2. Expiración y refresh automático silencioso 3. Logout → revocación de familia 4. Intento de reuso → 401 + revocación familia 5. Verificación de hash en BD vs raw token 6. Token raw NUNCA en logs 7. ClockSkew = 0 → expiración puntual --- **Branch**: feature/UDT-002 (36 commits desde origin/main) **Migración**: V002__create_refresh_token.sql incluida **Tests**: 100% TDD (RED → GREEN para cada capa) **Seguridad**: 0 tokens raw en BD, 0 raw en logs, chain revocation en reuso Pronto para review y merge a main.
dmolinari added 36 commits 2026-04-14 17:01:20 +00:00
Transaction-scoped tests conflicted with the repository opening its own connection,
blocking on FK locks for the uncommitted seeded user and causing timeouts.
Switched to the Respawn pattern used by UsuarioRepositoryTests ([Collection("Database")])
which commits seed data and resets between test classes.
Avoids substring-match false positives on future endpoints whose URL could
contain /auth/refresh or /auth/login as infix (W-01 from verify report).
dmolinari merged commit 5b3797a81c into main 2026-04-14 17:37:47 +00:00
dmolinari deleted branch feature/UDT-002 2026-04-14 17:37:47 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dmolinari/SIG-CM2.0#3