Verificar si un token JWT ha expirado: exp, iat, nbf, desfase de reloj y renovación
Un JWT lleva su propia fecha de caducidad en la claim `exp`. Comprobarla parece trivial, y aun así la expiración provoca una buena parte de los errores de autenticación: tokens que caducan «antes de tiempo» por un reloj desfasado, sesiones que nunca expiran porque alguien guardó milisegundos en vez de segundos, o usuarios expulsados en mitad de un formulario porque nadie renovó el token.
exp, iat y nbf: tres fechas en segundos
Las claims temporales de un JWT usan el tipo NumericDate de RFC 7519: segundos desde el 1 de enero de 1970 en UTC, no milisegundos.
{
"sub": "usuario_42",
"iat": 1790000000, // emitido: 2026-09-21 14:13:20 UTC
"nbf": 1790000000, // no válido antes de esa fecha
"exp": 1790000900 // expira: 15 minutos después
}
exp es el momento a partir del cual el token no debe aceptarse. nbf (not before) es el momento antes del cual tampoco debe aceptarse. iat (issued at) solo informa de cuándo se emitió; no invalida nada por sí sola, aunque algunos sistemas la usan para rechazar tokens demasiado antiguos. Las tres son opcionales en la especificación, pero una API debería exigir exp.
Comprobar la expiración en JavaScript, Python y la terminal
// Leer exp (sin verificar la firma: solo para mostrar o programar la renovación)
const payload = JSON.parse(atob(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')));
const expiraEn = new Date(payload.exp * 1000); // segundos -> milisegundos
const expirado = Date.now() >= payload.exp * 1000;
const segundosRestantes = payload.exp - Math.floor(Date.now() / 1000);
import time, jwt # PyJWT
claims = jwt.decode(token, options={'verify_signature': False})
expirado = time.time() >= claims['exp']
# Ver la fecha de expiración de un token
exp=$(printf '%s' "$TOKEN" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null | jq .exp)
date -u -d "@$exp" # Linux (en macOS: date -u -r "$exp")
El error más frecuente es mezclar unidades. Date.now() en JavaScript devuelve milisegundos. Si se compara directamente con exp, todo token parece caducado desde hace décadas; si se guarda Date.now() como exp al crear el token, este caduca dentro de más de 50.000 años. Multiplica o divide por 1000 exactamente en un sitio.
Leer exp no basta: la expiración se valida en el servidor
Leer exp del payload sirve para mostrar información o para decidir cuándo renovar en el cliente. No sirve para decidir si se acepta una petición: cualquiera puede fabricar un token con un exp en el año 2100. En el servidor, la expiración se comprueba como parte de la verificación completa: firma, algoritmo permitido, iss, aud y fechas.
// Node.js con jose: la verificación incluye exp y nbf
import { jwtVerify, errors } from 'jose';
try {
const { payload } = await jwtVerify(token, key, {
algorithms: ['RS256'], issuer: 'https://auth.ejemplo.com/', audience: 'https://api.ejemplo.com',
clockTolerance: 30,
});
} catch (e) {
if (e instanceof errors.JWTExpired) { /* 401: el cliente debe renovar */ }
throw e;
}
Cada librería señala la expiración con su propio error: JWTExpired (código ERR_JWT_EXPIRED) en jose, TokenExpiredError en jsonwebtoken y jwt.ExpiredSignatureError en PyJWT. Conviene distinguirlo de una firma inválida. Un token expirado es normal y el cliente puede renovarlo; una firma inválida puede indicar manipulación.
El desfase de reloj
El emisor y el servidor que valida comparan fechas con sus propios relojes. Si difieren unos segundos, un token recién emitido puede parecer «todavía no válido» (nbf en el futuro) o caducar antes de lo esperado. Por eso las librerías permiten un margen: clockTolerance en jose, leeway en PyJWT o clockTolerance en jsonwebtoken.
Usa un margen pequeño, de 30 a 60 segundos, y mantén los servidores sincronizados con NTP. Un margen de varios minutos alarga en silencio la vida de todos los tokens, que es justo lo que la expiración corta intenta evitar.
Si un token funciona en un entorno y falla en otro con el mismo exp, compara la hora real de ambos servidores antes de revisar el código. Los contenedores y las máquinas virtuales pueden desincronizarse tras suspenderse.
Renovar antes de que expire
Los tokens de acceso deberían vivir poco, entre 5 y 15 minutos, para limitar el daño si se filtran. Para que eso no obligue al usuario a iniciar sesión cada cuarto de hora, se combinan con un refresh token de larga duración que obtiene tokens de acceso nuevos.
Hay dos estrategias en el cliente, y lo mejor es usar ambas. La proactiva programa una renovación poco antes de exp, por ejemplo un minuto antes. La reactiva, en un interceptor HTTP, captura el 401, renueva una sola vez y repite la petición original.
let renovacion = null; // evita varias renovaciones simultáneas
async function fetchConAuth(url, opciones = {}) {
let res = await fetch(url, { ...opciones, headers: { Authorization: `Bearer ${tokenActual}` } });
if (res.status === 401) {
renovacion ??= renovarToken().finally(() => { renovacion = null; });
await renovacion;
res = await fetch(url, { ...opciones, headers: { Authorization: `Bearer ${tokenActual}` } });
}
return res;
}
Activa la rotación de refresh tokens en tu proveedor de identidad: cada uso devuelve un refresh token nuevo e invalida el anterior, de modo que un refresh token robado deja de funcionar en cuanto el usuario legítimo lo usa. Recuerda que la expiración de un JWT no se puede adelantar. Si necesitas revocar un token antes de exp, necesitas una lista de revocación, por ejemplo de valores jti, o tokens tan cortos que la espera sea aceptable.
Fuentes y lecturas recomendadas
RFC 7519 define JWT y las claims exp, nbf e iat como NumericDate. RFC 8725 recoge las buenas prácticas de seguridad de JWT. RFC 6749 y la guía OAuth 2.0 Security Best Current Practice describen los refresh tokens y su rotación. La documentación de jose, jsonwebtoken y PyJWT detalla sus errores de expiración y opciones de tolerancia.
Frequently asked questions
¿Cómo saber si un token JWT ha expirado?
Decodifica el payload y compara `exp` (en segundos) con la hora actual: en JavaScript, `Date.now() >= payload.exp * 1000` indica que expiró. En el servidor, deja que la librería lo compruebe al verificar el token.
¿La claim exp está en segundos o milisegundos?
En segundos desde 1970 (NumericDate). Multiplica por 1000 solo al convertirla a una fecha de JavaScript.
¿Por qué mi token expira antes de tiempo?
Normalmente por desfase de reloj entre el emisor y el servidor, o por una conversión errónea entre segundos y milisegundos. Sincroniza los relojes con NTP y usa un margen de 30 a 60 segundos.
¿Qué hago cuando recibo TokenExpiredError?
Responde con 401 y deja que el cliente renueve el token con su refresh token y repita la petición. No lo trates como una firma inválida: la expiración es un evento normal.
¿Cuánto debe durar un token JWT de acceso?
Entre 5 y 15 minutos es lo habitual para tokens de acceso, combinados con refresh tokens con rotación. Así un token filtrado sirve poco tiempo sin molestar al usuario.
¿Se puede invalidar un JWT antes de que expire?
No directamente: un JWT firmado es válido hasta `exp`. Para revocarlo antes necesitas una lista de revocación (por ejemplo, de `jti`) o consultar al proveedor mediante introspección.