← Proyectos

CRUD-Test API de Usuarios con Autenticación JWT

Mi primer proyecto de backend en solitario, un CRUD de usuarios con MySQL y JWT. Al revisarlo tiempo después encontré que el token de registro abría toda la API; lo arreglé y cubrí cada fallo con tests.

Stack
Java · Spring · MySQL · Flyway · JWT
Estado
Terminado
Año
2025

Elige un ataque

Recién registrado, el usuario solo debería poder completar su perfil.

GET / HTTP/1.1Authorization: Bearer <token parcial>

Antes vulnerable

HTTP/1.1 200 OK

{ "content": [ …usuarios activos… ] }

Ahora bloqueado

HTTP/1.1 403 Forbidden

El punto de partida

Fue mi primer proyecto de backend en solitario, escrito sin ayuda de IA. Quería comprobar tres cosas: conectar una base de datos real (MySQL, con el esquema versionado en Flyway), autenticar con JWT y construir un CRUD de usuarios sobre eso. La lógica de negocio es sencilla a propósito: el objetivo era aprender cómo encajan esas piezas.

Registro en dos fases

Un usuario nuevo se registra con email y contraseña y queda en estado PENDING. La respuesta trae un token parcial de 15 minutos que solo sirve para una cosa: completar el perfil. Al hacerlo, la cuenta pasa a ACTIVE y ya puede iniciar sesión para recibir un token de acceso de una hora.

Los estados no cambian libremente: una máquina de cinco estados (PENDING, ACTIVE, INACTIVE, SUSPENDED, DELETED) define qué transiciones están permitidas, y una cuenta borrada no vuelve.

Lo que encontré al volver a él

Tiempo después, al preparar este caso, lo revisé con ayuda de IA, comparando lo que la API prometía con lo que de verdad hacía. Con más experiencia, los errores del primer proyecto saltaban a la vista:

  • El token parcial y el de acceso eran el mismo JWT: misma firma, mismo emisor, mismo usuario. Solo cambiaba la expiración. Con el token del registro se podía usar toda la API durante 15 minutos.
  • Un usuario PENDING podía iniciar sesión y recibir un token de acceso sin completar nada.
  • Cualquier usuario autenticado podía desactivar o renombrar la cuenta de otro cambiando el id.
  • El secreto con el que se firmaban los tokens estaba escrito en el código, en un repositorio público.
  • En una base de datos nueva, el registro fallaba siempre: ninguna migración creaba el plan por defecto que se asigna a cada usuario.

Cómo lo arreglé

  • Cada token lleva ahora su tipo (access o partial) en un claim, y comprobarlo es parte de validar el token. El filtro de seguridad solo acepta tokens de acceso; con un token parcial, la petición queda como anónima.
  • Solo una cuenta activa se autentica: ni el login ni un token emitido antes funcionan si la cuenta está pendiente o desactivada.
  • Renombrar o desactivar una cuenta exige ser su dueño o administrador.
  • El secreto llega por variable de entorno, y la aplicación no arranca sin él.
  • De paso, la expiración de los tokens se calcula en UTC. Antes asumía la zona -05:00, y en un servidor en otra zona los tokens duraban horas de más.

Cómo sé que está arreglado

Escribí un test de integración que recorre el flujo completo contra MySQL real (registro, activación, login y desactivación), con un caso por cada fallo. Para comprobar que los tests detectan de verdad los problemas, los corrí también contra el código anterior: fallaron 5 de 6. Con los arreglos pasan todos.

Lo que ya estaba bien

  • El consumo del token parcial usa bloqueo optimista (@Version): dos peticiones simultáneas con el mismo token no pueden activar la cuenta dos veces.
  • Una tarea programada limpia cada mes los tokens usados o expirados.
  • El esquema evolucionó en 17 migraciones de Flyway.

Lo que queda pendiente

El manejador de errores todavía convierte cualquier excepción no prevista en un 404, y Hibernate sigue con ddl-auto=update al lado de Flyway. Son los siguientes en la lista.