Saltar al contenido
En esta página

Arquitectura de autenticación JWT

5 min de lectura

Qué pasa por debajo cuando tu app usa JWT: el recorrido completo desde el login hasta el logout, paso a paso y con cada pieza explicada.

¿Alguna vez te has preguntado qué pasa por debajo cuando entras a una aplicación y esta te "recuerda" en cada pantalla? Hoy vamos a recorrer entero el flujo de autenticación con JWT, desde el login hasta el logout.

Empecemos por el problema, porque de ahí sale todo: el servidor no te recuerda. HTTP no guarda nada entre una petición y la siguiente, así que cada una llega como si fuera la primera. La salida clásica es que él lo apunte —una fila de sesión que busca cada vez—, y eso cuesta una consulta por petición y un almacén al que lleguen todos tus servidores.

La otra salida es darle la vuelta: que no lo recuerde nadie y que la prueba de quién eres viaje contigo. Para que se sostenga, el servidor tiene que poder comprobarla sin buscar nada y tú no tienes que poder editarla, porque si pudieras te cambiarías el identificador por el del administrador. Un dato firmado con un secreto que solo él conoce cumple las dos cosas.

Eso es un JWT (JSON Web Token): tres tramos separados por puntos, donde la cabecera dice con qué algoritmo se firmó, el payload lleva los datos —quién eres, hasta cuándo vale— y la firma permite comprobar que nadie los tocó. Los dos primeros son texto codificado, no cifrado, así que se pueden leer. Edítalo o pega uno tuyo:

Un JWT por dentro

..

cabecera

{
  "alg": "HS256",
  "typ": "JWT"
}

payload

{
  "sub": "u_42",
  "exp": 1787480100
}

Caduca el 23 ago 2026, 10:15 UTC

firma

Bytes, no texto. Sin el secreto del servidor no dicen nada, y por eso este tramo no se decodifica.

Abre un tramo para verlo decodificado.

Cambia un carácter del payload y la firma deja de cuadrar: ahí está la propiedad entera. En lugar de que el servidor recuerde quién eres, la prueba la llevas tú y él solo tiene que saber comprobarla. Eso sí, no es secreta: cualquiera que la tenga puede leer lo que hay dentro.

Todo empieza en el login

Al hacer login envías tus credenciales, normalmente un correo y una contraseña. Esa es la única vez en todo el flujo que la contraseña viaja por la red, y el único momento que necesita ir sobre HTTPS sí o sí.

El servidor recibe esas credenciales y hace tres cosas, en este orden:

  1. Verifica quién eres. Busca al usuario y compara la contraseña que llega con el hash que tiene guardado — nunca guarda la contraseña en claro.
  2. Arma el payload del token. Es un objeto pequeño con quién eres, en el claim sub, y hasta cuándo vale el token, en el claim exp. Un claim es cada uno de los campos que van dentro del token, y esos dos nombres cortos vienen del estándar.
  3. Firma el payload con su secreto y te devuelve el token. Sin esa firma, el payload sería un JSON que cualquiera puede editar: bastaría cambiar el sub por el de otro usuario para pasar a ser esa persona. La firma es lo que permite al servidor fiarse de un dato que ha estado en tus manos.
Cargando la animación…

El paso que decide todo lo demás es el último: el servidor firma el token, te lo entrega y no guarda ninguna copia. No hay tabla de sesiones, no hay fila que apunte a ti. Esa decisión es la que explica todo lo que viene después, tanto lo bueno como lo incómodo.

Cada petición lleva el token

Ya tienes el token. A partir de aquí, cada petición que hace la aplicación lo manda en la cabecera Authorization, con el formato Bearer <token>. "Bearer" significa literalmente "portador": vale por el hecho de tenerlo, como un billete de tren sin nombre.

Del lado del servidor, la validación son tres comprobaciones y ninguna consulta. Primero parte el token en sus tres tramos. Después recalcula la firma con su secreto y la compara con la que trae el token: si alguien cambió aunque sea un carácter del payload, las dos firmas no coinciden y la petición se rechaza con un 401. Y por último mira el exp, la fecha de caducidad, contra su reloj:

Cargando la animación…

Lo interesante de este tramo es lo que no aparece: en ningún momento se consultó la base de datos. Es exactamente la propiedad que buscábamos al principio, y aquí es donde se cobra. El servidor no necesita saber nada de ti que no venga en el token, y por eso cualquier servicio que tenga el secreto puede validar por su cuenta.

El token caduca, y ahí entra el refresh

Como el token vale por tenerlo, si alguien te lo roba entra como tú. La defensa es que dure poco: quince minutos es un valor habitual. Pero eso abre la pregunta obvia — nadie va a escribir su contraseña cada quince minutos.

Aquí es donde la arquitectura pasa de un token a dos, cada uno con un papel distinto:

  • El access token es el que acabas de ver: corto, firmado, sin estado, viaja en cada petición.
  • El refresh token es una cadena larga y aleatoria, de un solo uso, que sí se guarda en una tabla del servidor y que en el cliente vive en una cookie httpOnly —una cookie que el JavaScript de la página no puede leer, de modo que un script inyectado en ella no puede llevársela y renovar tu sesión indefinidamente—. Su único trabajo es conseguirte un access token nuevo.

Que el refresh token viva en una tabla parece devolver el estado que acabábamos de quitar, y la diferencia está en cada cuánto se consulta: el access token se valida en cada petición sin tocar la base de datos; el refresh se busca una vez cada quince minutos. La consulta cara se paga una vez por tramo de sesión, no miles.

Cuando el access token caduca, la aplicación no te manda al login: llama al endpoint de refresco, el servidor busca el refresh token en su tabla, comprueba que sigue vigente, emite un par nuevo y marca el viejo como usado:

Cargando la animación…

Ese "marca el viejo como usado" es la parte que mucha gente se salta, y es importante: si un refresh token que ya se canjeó vuelve a aparecer, o hay un bug en el cliente o alguien está usando una copia robada. La respuesta en los dos casos es la misma, invalidar toda la cadena de tokens de esa sesión y obligar a un login.

El logout, y lo que el logout no puede hacer

Cerrar sesión mueve dos cosas: el cliente borra su access token y el servidor borra la fila del refresh token, con lo que esa sesión ya no se puede renovar nunca más.

Pero hay una tercera cosa que no se mueve, y aquí es donde la arquitectura enseña su contrapartida. Si existe una copia del access token en algún sitio —en un log, en un proxy, en el portapapeles de alguien—, esa copia sigue teniendo la firma correcta y un exp que todavía no ha llegado. Y el servidor valida sin consultar nada:

Cargando la animación…

Es decir: el logout es completo del lado del refresh token e incompleto del lado del access token. Cortas la renovación al instante y dejas una ventana igual a lo que le quede de vida al token: como mucho, quince minutos. Se puede cerrar esa ventana guardando el identificador de los tokens revocados y consultando esa lista en cada petición, pero eso devuelve al servidor justo el estado que este diseño quería evitar.

El flujo completo, de un vistazo

MomentoQué envía el clienteQué hace el servidor¿Consulta la base de datos?
Logincorreo y contraseñaverifica y firma el par de tokenssí
Petición normalaccess tokencomprueba firma y expno
Refrescorefresh tokenlo busca, lo rota, emite un par nuevosí
Logoutrefresh tokenborra la filasí

Esa columna de la derecha es la respuesta al problema del principio: la petición normal —la que ocurre miles de veces— no toca la base de datos. Los dos tokens, la rotación y el logout a medias son el precio de esa única propiedad.

Si quieres verlo en tu propio proyecto, abre las herramientas de desarrollo en la pestaña de red, entra a tu aplicación y mira la cabecera Authorization de cualquier petición: ahí está el token del que hemos estado hablando todo el post.