Qué es un JWT y cuál es su estructura: header, payload y firma, campo por campo
Un JWT (JSON Web Token) parece un único string opaco, pero son tres partes codificadas en Base64url y separadas por puntos: un header que describe cómo está protegido el token, un payload con los datos (las claims) y una firma sobre las dos primeras. Saber qué campos van en cada parte, y cuáles controla un atacante, es la base para decodificar tokens de forma segura y para entender los errores que lanzan las librerías cuando un token está mal formado.
Qué es un JWT
Un JWT es un formato estándar (RFC 7519) para transmitir afirmaciones firmadas entre dos partes: por ejemplo, "este usuario es el 42, tiene rol editor y la sesión caduca a las 15:00". El servidor que lo emite lo firma; cualquier servicio con la clave adecuada puede verificar que nadie lo modificó, sin consultar una base de datos. Por eso se usa mucho como token de acceso en API, en OAuth 2.0 y en OpenID Connect.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 <- header
.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ <- payload
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c <- firma
header : {"alg":"HS256","typ":"JWT"}
payload: {"sub":"1234567890","name":"John Doe","iat":1516239022}
Las dos primeras partes son JSON codificado en Base64url y cualquiera puede leerlas sin clave. La firma solo impide que se modifiquen sin que se note. Como las dos primeras partes siempre empiezan por {", casi siempre comienzan por eyJ una vez codificadas, lo que permite reconocer un JWT en los logs.
El header: cómo está protegido el token
alg algoritmo de firma: HS256, RS256, ES256, EdDSA... (obligatorio)
typ tipo, normalmente "JWT"; "at+jwt" indica un access token de OAuth (RFC 9068)
kid id de la clave: qué clave del JWKS del emisor firmó el token
cty tipo de contenido; "JWT" si el payload es otro JWT anidado
jku URL de un JWK Set } rara vez necesarios y peligrosos si
x5u URL de una cadena X.509 } el verificador los sigue desde el token
jwk clave pública incrustada }
Todo lo que hay en el header lo controla un atacante hasta que la firma se verifica. Un verificador no debe dejar que alg elija el algoritmo, porque eso abre la puerta a ataques como alg: none, ni descargar claves desde jku o x5u. kid solo sirve para buscar entre claves en las que ya confías; nunca lo uses directamente en una ruta de archivo o una consulta SQL.
El payload: las claims
El payload es un objeto JSON de claims. RFC 7519 registra siete, todas opcionales en la especificación base, aunque perfiles como OpenID Connect o los access tokens de OAuth hacen obligatorias varias.
iss emisor: quién creó el token "https://auth.ejemplo.com/"
sub sujeto: de quién trata "usuario_8f3a2"
aud audiencia: para quién es "https://api.ejemplo.com" (o un array)
exp expiración, en segundos desde 1970 1790000000
nbf no válido antes de (segundos) 1789996400
iat emitido en (segundos) 1789996400
jti id único del token, para revocación "b1f0c2e4-..."
y claims propias de la aplicación: "scope": "pedidos:leer", "roles": ["admin"]
Las claims de tiempo son NumericDate: segundos desde 1970, no milisegundos. Escribir Date.now() en exp crea un token que caduca dentro de más de 50.000 años. Usa Math.floor(Date.now() / 1000).
Mantén el payload pequeño y sin datos sensibles. El cliente y cualquiera que intercepte el token pueden leerlo, viaja en cada petición y las cookies se limitan a unos 4 KB. Si el contenido debe ser secreto, usa un JWE cifrado.
La firma
La firma se calcula sobre el string exacto header.payload con el algoritmo y la clave indicados. Cambiar un solo carácter de cualquiera de las dos partes la invalida. No cubre nada fuera del token, así que un token robado es válido allí donde se presente hasta que caduque.
firma = HMAC-SHA256(secreto, base64url(header) + "." + base64url(payload)) # HS256
firma = RSA-SHA256-firmar(clavePrivada, la misma entrada) # RS256
Cinco partes: cuando es un JWE
Un JWT cifrado (JWE, RFC 7516) tiene cinco partes: header protegido, clave cifrada, vector de inicialización, texto cifrado y etiqueta de autenticación. Su header lleva alg para la gestión de claves (por ejemplo RSA-OAEP o dir) y enc para el cifrado del contenido (por ejemplo A256GCM). El payload no se puede leer sin la clave, así que un token con cuatro puntos no está mal formado: está cifrado, y un decodificador de JWS no puede con él.
"JWT must consist of header, payload and signature" y errores parecidos
Las librerías lanzan errores como "jwt malformed" o "the JWT is not in the form of header.payload.signature" cuando el string no se divide en las partes esperadas. El token casi nunca llegó dañado; se pasó el string equivocado.
"Bearer eyJhbGci..." -> no se quitó el prefijo "Bearer "
"\"eyJhbGci...\"" -> comillas de un valor JSON o de un archivo .env
"eyJhbGci...\n" -> salto de línea o espacios al final
"undefined" / "null" -> faltaba la cabecera o la cookie
"a1b2c3..." (sin puntos) -> un access token opaco, no un JWT
cinco partes -> un JWE: hay que descifrarlo
Leer un JWT en PHP sin confiar en él
Para depurar, puedes leer el payload en cualquier lenguaje decodificando la segunda parte. En PHP, por ejemplo:
<?php
[$h, $p] = explode('.', $jwt);
$payload = json_decode(base64_decode(strtr($p, '-_', '+/')), true);
echo date('c', $payload['exp']); // fecha de expiración legible
Eso solo lee las claims. Para aceptar el token en una petición hay que verificar la firma con una librería mantenida, como firebase/php-jwt en PHP o jose en JavaScript, indicando el algoritmo, el emisor y la audiencia esperados.
Fuentes y lecturas recomendadas
RFC 7519 define JWT y las claims registradas. RFC 7515 define JWS, los parámetros del header y la serialización compacta. RFC 7516 define el formato JWE de cinco partes, RFC 9068 el perfil de access token JWT y RFC 8725 las buenas prácticas de seguridad.
Frequently asked questions
¿Qué es un JWT?
Un JSON Web Token: un formato estándar para enviar datos firmados entre sistemas, formado por tres partes en Base64url (header, payload y firma) separadas por puntos. Se usa sobre todo como token de acceso en API y en OAuth/OpenID Connect.
¿Cuál es la estructura de un JWT?
`header.payload.firma`. El header indica cómo está firmado el token, el payload contiene las claims y la firma cubre las dos primeras partes.
¿Qué diferencia hay entre el header y el payload?
El header contiene metadatos del propio token, como `alg`, `typ` y `kid`. El payload contiene las claims sobre el usuario o la sesión, como `sub`, `exp`, `aud` o los roles. Ambos son legibles por cualquiera y solo están protegidos contra modificaciones.
¿El payload de un JWT está cifrado?
No. En un JWT firmado normal el payload solo está codificado en Base64url y cualquiera puede leerlo. Usa JWE, un token de cinco partes, si el contenido debe ser confidencial.
¿exp e iat están en segundos o milisegundos?
En segundos desde la época Unix. Usar milisegundos, como `Date.now()` en JavaScript, produce fechas de expiración a decenas de miles de años.
¿Por qué aparece "jwt must consist of header, payload and signature"?
El valor pasado a la librería no es un JWT de tres partes: suele faltar quitar el prefijo `Bearer `, hay comillas o espacios, la cabecera no existía o es un token opaco o cifrado.