Claims de JWT: iss, sub, aud, exp, nbf, iat y jti, y cómo validarlas con la firma

Un JWT es útil solo si el servidor comprueba dos cosas: que la firma es auténtica y que las claims dicen lo que deben decir. Las claims son los campos del payload, y la RFC 7519 reserva siete nombres cortos para las más comunes. Esta guía explica qué significa cada una, cómo se valida, en qué orden se hacen las comprobaciones (firma, `exp`, `nbf`, `iss`, `aud`) y qué errores aparecen en la práctica, con un token real y resultados obtenidos al ejecutar el código.

Las siete claims registradas

Los nombres tienen tres letras porque el token viaja en cada petición y conviene que sea pequeño. Su significado, según la sección 4.1 de la RFC 7519:

iss   issuer (emisor)            quién creó y firmó el token
sub   subject (sujeto)           de quién trata, normalmente un ID de usuario
aud   audience (audiencia)       para quién está pensado (texto o array)
exp   expiration (expiración)    a partir de este momento hay que rechazarlo
nbf   not before (no antes de)   antes de este momento hay que rechazarlo
iat   issued at (emitido el)     cuándo se creó
jti   JWT ID                     identificador único del token

En el estándar base todas son opcionales. Las especificaciones construidas sobre JWT las hacen obligatorias: un ID token de OpenID Connect debe llevar iss, sub, aud, exp e iat, y la RFC 9068 exige lo mismo para los access tokens de OAuth en formato JWT. El resto de campos (scope, email, roles…) son claims propias del sistema que emite el token.

kid y JWKS no son claims. kid es un parámetro de la cabecera que indica qué clave se usó, y el JWKS (RFC 7517) es el documento con las claves públicas del emisor que sirve para comprobar la firma.

Un token real, decodificado

Este es el payload de un token firmado para este artículo con HS256:

{
  "scope": "orders:read",
  "iss": "https://auth.example.com/",
  "sub": "user_8f3a2c",
  "aud": "https://api.example.com",
  "iat": 1792400000,      // 2026-10-19T08:53:20Z
  "nbf": 1792400000,
  "exp": 1792400900,      // 15 minutos después
  "jti": "4f1c9d2e"
}

Las fechas son NumericDate: segundos enteros desde el 1 de enero de 1970 en UTC, no milisegundos. En JavaScript es fácil equivocarse: exp: Date.now() + 900000 crea un token válido durante unos 57 000 años, y comparar exp con Date.now() sin dividir entre 1000 hace que todos los tokens parezcan caducados. Con el decodificador de JWT puedes ver las claims de cualquier token con las fechas ya convertidas.

El orden correcto: primero la firma

Cualquiera puede fabricar un token con el iss, el aud y el exp que quiera; basta con codificar un JSON en Base64url. Por eso las claims no significan nada hasta que se verifica la firma. El orden que debe seguir un servidor es este:

1. alg      ¿es un algoritmo permitido? (nunca aceptar "none")
2. firma    verificar con la clave del emisor (secreto HS256 o clave pública del JWKS)
3. exp      ahora < exp (con un pequeño margen)
4. nbf      ahora >= nbf
5. iss      coincide exactamente con el emisor de confianza
6. aud      contiene el identificador de esta API

Decodificar no es validar. Leer el payload sin clave está bien para depurar, pero una aplicación que toma decisiones a partir de claims sin verificar la firma acepta tokens inventados. Las bibliotecas hacen todos estos pasos en una sola llamada si les pasas las opciones adecuadas, como se ve a continuación.

exp, nbf e iat: la ventana de validez

El token solo se puede usar entre nbf y exp. Como los relojes de distintos servidores no están perfectamente sincronizados, las bibliotecas permiten un margen; la RFC 7519 dice que normalmente no debería superar unos pocos minutos. Este es el mismo token comprobado con la biblioteca jose en distintos momentos:

import { jwtVerify } from "jose";

await jwtVerify(jwt, key, {
  issuer: "https://auth.example.com/",
  audience: "https://api.example.com",
  clockTolerance: 60,            // segundos de margen para exp y nbf
});

// 60 s después de iat:        válido
// 1000 s después de iat:      ERR_JWT_EXPIRED  "exp" claim timestamp check failed
// 30 s antes de nbf:          ERR_JWT_CLAIM_VALIDATION_FAILED  "nbf" claim timestamp check failed

iat es sobre todo informativa, aunque algunos sistemas rechazan tokens demasiado antiguos según iat aunque no tengan exp. Los access tokens deberían durar poco, entre 5 y 60 minutos, y renovarse con un refresh token: un JWT sin estado no se puede revocar antes de exp salvo que mantengas una lista de tokens bloqueados.

iss y aud: los dos fallos más comunes

iss se compara como texto exacto, distinguiendo mayúsculas y la barra final. Con el token de arriba, configurar el emisor como https://auth.example.com (sin barra) da ERR_JWT_CLAIM_VALIDATION_FAILED unexpected "iss" claim value. Copia el valor del documento de descubrimiento del proveedor (/.well-known/openid-configuration) en lugar de escribirlo a mano.

aud es la claim que más se olvida comprobar, y eso es una vulnerabilidad real. Si dos APIs confían en el mismo proveedor de identidad y ninguna comprueba la audiencia, un token emitido para la API de solo lectura sirve también en la API de administración. Cada API debe exigir su propio identificador. Recuerda que aud puede ser un texto o un array: el token es válido si tu identificador es uno de los valores. Con una audiencia distinta, jose responde unexpected "aud" claim value.

sub es único solo dentro de su emisor. Guarda los usuarios por el par (iss, sub), no por sub solo, y no uses el email como clave, porque puede cambiar o reasignarse.

jti y cómo ver las claims en DevTools

jti es un identificador único del token. Por sí solo no protege nada; sirve cuando el servidor lo recuerda. Guardar el jti de cada token usado hasta su exp permite rechazar un token presentado dos veces, algo importante en enlaces de un solo uso como los de recuperación de contraseña, y da algo concreto que bloquear cuando un token se filtra.

Para ver iss y aud de una petición en el navegador, abre DevTools, pestaña Red (Network), selecciona la petición a la API y copia el token de la cabecera Authorization: Bearer …. El payload es la parte central, entre los dos puntos, en Base64url. Desde la terminal:

python3 -c 'import sys,base64,json; p=sys.argv[1].split(".")[1]; print(json.loads(base64.urlsafe_b64decode(p + "=" * (-len(p) % 4))))' "$TOKEN"

node -e 'const p=process.argv[1].split(".")[1]; console.log(JSON.parse(Buffer.from(p, "base64url")))' "$TOKEN"

El relleno con = es necesario porque los JWT lo omiten y el decodificador de Python no acepta entradas sin él. Para comprobar también la firma durante el desarrollo, usa el validador de JWT, que verifica HS256, RS256 y otros algoritmos en el navegador.

Fuentes y lecturas recomendadas

La RFC 7519 define JWT, las siete claims registradas (sección 4.1) y el formato NumericDate. La RFC 8725 recoge las buenas prácticas de validación: restringir algoritmos, comprobar iss y aud. La RFC 7517 define JWK y los conjuntos de claves (JWKS). OpenID Connect Core 1.0, sección 2, enumera las claims obligatorias del ID token, y la RFC 9068 el perfil JWT de los access tokens de OAuth 2.0. El registro de IANA «JSON Web Token Claims» lista todos los nombres registrados.

Frequently asked questions

¿Qué significan iss, sub y aud en un JWT?

iss es el emisor, quien creó y firmó el token. sub es el sujeto, normalmente el ID del usuario. aud es la audiencia, la API o aplicación a la que va destinado el token.

¿Qué diferencia hay entre exp, nbf e iat?

exp es el momento a partir del cual el token se rechaza, nbf el momento antes del cual se rechaza, e iat el momento en que se emitió. Los tres se expresan en segundos desde 1970 en UTC.

¿En qué orden se valida un JWT?

Primero se comprueba que el algoritmo está permitido y se verifica la firma con la clave del emisor. Después se validan exp, nbf, iss y aud. Sin verificar la firma, las claims no tienen ningún valor.

¿Puede aud ser un array?

Sí. aud puede ser un texto o un array de textos. El servidor solo debe aceptar el token si su propio identificador aparece entre los valores.

¿Por qué mi token aparece como caducado?

Lo más habitual es mezclar segundos y milisegundos: exp está en segundos y Date.now() devuelve milisegundos. También puede deberse a relojes desincronizados; un margen de unos 60 segundos suele bastar.

¿Es JWKS una claim del JWT?

No. El JWKS es un documento JSON con las claves públicas del emisor, que se usa para verificar la firma. El parámetro kid de la cabecera indica qué clave del conjunto corresponde al token.