Buenas prácticas con JWT: header, payload, firma y errores frecuentes
Qué hay en un JWT, qué significa decodificar frente a verificar, cómo se comparan HS256, RS256 y ES256, qué hacer con la expiración, por qué `alg: none` sigue causando incidentes y dónde guardar el token.
Qué es un JWT
Un JWT (JSON Web Token, RFC 7519) es un token compacto y autocontenido formado por tres partes separadas por puntos: un header, un payload y una firma. El header dice qué algoritmo se usó para firmar, el payload contiene los claims (datos del token: quién lo emitió, sobre qué usuario, hasta cuándo es válido) y la firma garantiza que el contenido no se ha alterado desde que el emisor lo firmó. Las tres partes son texto en base64url, así que un JWT es trivial de transmitir por HTTP o incluirlo en un header Authorization.
El JWT estándar (JWS Compact Serialization, RFC 7515) no cifra el payload, solo lo firma. Cualquiera con el token puede leer su contenido. Lo que aporta es autenticidad: si la firma es válida con la clave correcta, sabes que el emisor produjo ese token concreto. Si también quieres que el contenido sea confidencial, JWE cifra además del firmado; es un formato distinto de 5 partes que esta herramienta no soporta en v1.
Anatomía: header, payload y firma
El header es un JSON pequeño que casi siempre contiene alg (algoritmo de firma) y a veces typ: JWT, kid (identificador de la clave cuando hay varias) o cty. El payload es otro JSON con los claims del token. Los claims registrados (RFC 7519 §4.1) incluyen iss (emisor), sub (subject), aud (audiencia), exp (expiración), nbf (not before), iat (issued at) y jti (identificador único). Puedes añadir claims propios siempre que no choquen con los reservados.
La firma se calcula sobre la concatenación header_b64url.payload_b64url aplicando el algoritmo declarado en el header. Si alguien cambia un carácter del header o del payload, la firma deja de cuadrar y la verificación falla. Por eso para confiar en un JWT hay que ejecutar esa verificación, no solo leer su contenido.
Decodificar no es verificar
Es la confusión más común al empezar con JWT. Decodificar un token es tan simple como hacer base64url sobre el header y el payload y leer el JSON. Cualquiera puede crear un JWT con los claims que quiera; lo único que separa un token válido de uno manipulado es que la firma encaje con la clave que tu sistema considera autorizada. Si tu código lee el payload y confía en el campo roles sin verificar firma, es como si confiara en un campo enviado por un usuario anónimo.
La herramienta JWT decoder de ToolsOps separa explícitamente las dos pestañas: Decodificar muestra el contenido, Verificar firma comprueba con tu secret o clave pública. El estado por defecto es siempre “sin verificar” hasta que aportes una clave.
HS256 vs RS256 vs ES256
Los tres algoritmos firman con SHA-256, pero usan primitivas criptográficas distintas:
- HS256 (HMAC con SHA-256): firma y verificación usan el mismo secret compartido. Útil cuando emisor y verificador son la misma aplicación o pertenecen a la misma organización con secreto común. Si el verificador es un tercero, compartir el secreto deja de ser viable.
- RS256 (RSASSA-PKCS1-v1_5 con SHA-256): firma con clave privada, verificación con clave pública. Estándar de facto para identity providers (Auth0, Cognito, Keycloak, Azure AD): firman con su privada, todo el mundo verifica con la pública publicada en su JWKS. Firma larga, verificación rápida.
- ES256 (ECDSA sobre P-256 con SHA-256): mismo modelo público/privado que RS256 pero con curvas elípticas. Firmas más cortas (64 bytes para ES256 vs 256 bytes para RS256 con 2048 bits) y verificación más rápida. ES384 y ES512 usan curvas P-384 y P-521 respectivamente. Nota: el nombre ES512 se refiere a SHA-512; la curva es P-521, no P-512.
Regla general: HS256 dentro de un mismo sistema; RS256 o ES256 cuando hay terceros que verifican. EdDSA (Ed25519) está apareciendo en identity providers modernos pero el soporte en Web Crypto API no es universal en todos los browsers en 2026; por eso la herramienta no lo incluye en v1.
Expiración, nbf y iat
exp indica el timestamp (segundos UTC) a partir del cual el token deja de ser válido. nbf indica el timestamp antes del cual el token aún no está activo (poco usado en práctica). iat indica cuándo se emitió, útil para calcular antigüedad o detectar tokens muy viejos que no se ajustan a la política de expiración esperada.
Un servidor que respete RFC 7519 §4.1.4 debe rechazar un token con exp en el pasado, aunque la firma sea válida. La firma criptográfica garantiza que el token no se ha alterado, pero no garantiza que siga siendo aceptable: el exp es la frontera temporal. Permitir un poco de clock skew (30-60 segundos) es habitual para tolerar relojes desincronizados entre emisor y verificador.
`alg: none` y la confusión RS/HS
Son dos vectores de ataque clásicos contra implementaciones JWT defectuosas. alg: none es un valor permitido por RFC 7515 §4.1.1 para tokens sin firma. Cuando una biblioteca acepta ese valor sin que el código de la aplicación lo bloquee, un atacante puede generar un token con cualquier payload y firma vacía, y el servidor lo aceptará como válido. La herramienta rechaza activamente esos tokens: nunca los marca como firma válida.
La confusión RS/HS es más sutil. Si tu servidor está configurado para verificar RS256 con una clave pública RSA, y la biblioteca permite que el algoritmo se lea del header, un atacante puede enviar un token con header alg: HS256 firmado con HMAC usando esa misma clave pública como secret. El verificador intenta HS256 con la clave pública como secret, la firma cuadra y el token pasa. Mitigación: nunca permitir que el algoritmo de verificación dependa del header; fijarlo en el código del servidor.
Dónde guardar un JWT en el navegador
La pregunta más común. Las opciones son cookie HttpOnly + Secure + SameSite, memoria de la aplicación, o localStorage. La regla práctica: evita localStorage. Cualquier JavaScript en tu origen puede leerlo, incluido el código que se ejecute por un XSS exitoso.
- Cookie HttpOnly + Secure + SameSite Lax o Strict: opción más segura. El JavaScript no la lee, así un XSS no se lleva el token. Requiere coordinación servidor/cliente para que la cookie se mande con cada petición a la API.
- Memoria (variable JS): el token vive solo durante la sesión actual del navegador. Se pierde al recargar. Combinado con un refresh token en cookie HttpOnly, es un patrón común en SPAs.
- localStorage: persistente, accesible por JavaScript. Evítalo para tokens.
Errores comunes
- Confiar en el payload sin verificar firma. La herramienta lo recuerda explícitamente en la propia UI.
- No validar
exp,issyaud. La firma válida no implica que el token sea aceptable para tu sistema. - Aceptar
alg: nonesin checks explícitos en el código. - Permitir que el algoritmo de verificación venga del header. Fija el algoritmo en código.
- Guardar el token en localStorage en lugar de cookie HttpOnly.
- Reutilizar el mismo JWT para múltiples APIs con audiencias distintas sin separar emisiones.
- Logear el JWT entero en errores. Los logs suelen acabar en agregadores con menos control de acceso que la propia API.
Cómo usar el decoder de ToolsOps
El decodificador y verificador de JWT de ToolsOps acepta un token pegado, lo separa en sus tres partes y decodifica header y payload con resaltado y claims temporales interpretados a tu hora local. Para verificar la firma, cambia a la pestaña Verificar firma y pega tu secret HMAC (en UTF-8, hex o base64), tu clave pública en PEM o tu JWK. La verificación corre localmente con Web Crypto API y no hay petición que transporte el token, el secret ni la clave.
Para integridad de archivos descargados, no de tokens, la herramienta hermana es la calculadora de hash y checksum (cluster Seguridad). El concepto detrás está cubierto en la guía sobre funciones hash. El hub de seguridad agrupa ambas herramientas y sus guías.
Preguntas frecuentes
- ¿Un JWT está cifrado?
- No. Un JWT estándar (JWS) lleva el payload codificado en base64url, no cifrado. Cualquiera con el token puede leer el contenido. Lo que aporta el JWT es integridad y autenticidad mediante una firma; quien no tenga la clave correcta no puede generar un token válido, pero sí puede leerlo. Para tokens con payload cifrado existe JWE, una variante distinta de 5 partes que esta herramienta no soporta en v1.
- ¿Cuánto debería durar un access token?
- La recomendación habitual es entre 5 y 15 minutos, combinado con un refresh token de larga duración guardado en cookie HttpOnly. Tokens más largos exponen más superficie si uno se filtra; tokens más cortos requieren más renovaciones. Para sistemas internos sin refresh, 1 hora es razonable. Para sistemas críticos, considera 5 minutos.
- ¿Necesito firmar siempre con RS256 si tengo identity provider?
- Si el verificador es un tercero (otra empresa, otra app, un cliente público), sí: RS256 o ES256 permiten que cualquiera verifique con la clave pública sin compartir secreto. Si emisor y verificador son la misma aplicación (un backend monolítico firmando para sí mismo), HS256 con secret compartido es válido y más simple.
- ¿Qué diferencia hay entre JWT, JWS y JWE?
- JWS (JSON Web Signature) es lo que la mayoría llama JWT: token de 3 partes con header.payload.signature, payload en claro. JWE (JSON Web Encryption) es token de 5 partes con payload cifrado además de la firma. JWT es el contenedor abstracto definido en RFC 7519 que apunta a una de las dos representaciones; en la práctica casi siempre significa JWS compact.
- ¿Cómo sé qué clave usar para verificar?
- Si el header trae un claim `kid`, sirve para seleccionar la clave correcta entre varias publicadas en un JWKS. Si tu identity provider expone un endpoint `.well-known/jwks.json`, contiene un array de claves cada una con su `kid`; buscas el JWK cuyo `kid` coincide y lo usas para verificar. Si no hay `kid`, hay una sola clave activa.
- ¿Puedo confiar en el claim `sub` sin verificar la firma?
- No. Sin verificar la firma, los claims son datos que cualquiera podría haber escrito. Decodificar muestra qué dice el token, no si es verdad. Antes de usar `sub`, `aud`, `roles` o cualquier otro claim para decisiones de autorización, confirma que la firma es válida con la clave correcta.
- ¿Por qué tantos consejos dicen no usar localStorage para JWT?
- Porque localStorage es accesible por cualquier JavaScript que se ejecute en el origen, incluido el código inyectado por un ataque XSS. Si un atacante consigue ejecutar JavaScript en tu página (vía dependencia comprometida, contenido user-generated mal filtrado, extensión maliciosa), puede leer el JWT y enviarlo a su servidor. Las cookies HttpOnly no son accesibles por JavaScript, lo que limita el alcance de un XSS al menos respecto al token.
- ¿Qué pasa si rota la clave en el identity provider?
- Los tokens emitidos con la clave antigua dejan de verificar contra la nueva. Si tu aplicación cachea el JWKS, debe refrescarlo periódicamente o cuando una verificación falla. Algunos identity providers exponen `kid` específico por clave: si en el header viene un `kid` que no conoces, fetchea el JWKS de nuevo antes de marcar el token como inválido.