Cómo decodificar un token JWT de forma segura en el navegador (y qué hacer si se filtra)

Decodificar un JWT es inofensivo: el header y el payload son JSON codificado en Base64url y leerlos no requiere ninguna clave. Lo que no es inofensivo es dónde lo decodificas. Un JWT es una credencial al portador: quien lo tiene puede usarlo hasta que caduque. Pegar un token de producción en una web que lo envía a un servidor, adjuntarlo a un ticket o dejarlo en una captura de pantalla entrega el acceso.

Qué es un token JWT y por qué el riesgo es el propio token

Un token JWT de acceso, enviado en Authorization: Bearer …, lo acepta la API venga de quien venga. No está ligado a tu navegador ni a tu IP, salvo que el sistema use tokens vinculados al remitente como DPoP o TLS mutuo, algo poco habitual. Hasta que pasa su exp, un token copiado funciona igual que una sesión iniciada.

Los refresh tokens son peores: pueden obtener nuevos tokens de acceso durante días o semanas. Y los ID tokens de OpenID Connect contienen datos personales como el correo o el nombre, sensibles incluso después de caducar.

La regla es sencilla: decodificar está bien; enviar el token a cualquier sitio que no controlas, no.

Decodificar un token JWT en la consola del navegador

No necesitas ninguna web para decodificar un JWT. La consola del navegador (F12 › Consola) lo hace en cualquier página, y el token nunca sale de tu equipo.

function decodificarJwt(token) {
  const parte = (s) => {
    const b64 = s.replace(/-/g, '+').replace(/_/g, '/')
      .padEnd(Math.ceil(s.length / 4) * 4, '=');
    const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
    return JSON.parse(new TextDecoder().decode(bytes));   // seguro con UTF-8
  };
  const [header, payload] = token.split('.');
  return { header: parte(header), payload: parte(payload) };
}

const { payload } = decodificarJwt(token);
new Date(payload.exp * 1000).toISOString();   // exp está en segundos

Dos detalles hacen fallar los fragmentos de código más simples. Los JWT usan Base64url sin relleno, así que hay que volver a convertir - y _ y restaurar el = antes de atob. Y atob devuelve un carácter por byte, por lo que nombres con tildes o eñes salen mal si los bytes no se decodifican como UTF-8, que es lo que hace el paso con TextDecoder.

Los decodificadores online que funcionan por completo en el navegador son igual de seguros, pero no es fácil comprobar que una página no envía lo que pegas. Revisa la pestaña Red mientras decodificas, o usa la consola para tokens de producción.

Decodificar no es validar

Un payload decodificado dice lo que el token afirma, no si es auténtico. Cualquiera puede crear un token con "role": "admin"; solo la verificación de la firma con la clave del emisor demuestra que es real. El frontend puede decodificar un token para mostrar el nombre del usuario o programar la renovación antes de exp, pero toda decisión de autorización debe tomarse en el servidor, después de verificar.

Tampoco envíes un token a un servicio externo solo para "validarlo". La verificación solo necesita la clave pública del emisor (en RS256 o ES256), que puedes usar en local con una librería como jose.

Mantener los tokens fuera de logs, tickets y capturas

La mayoría de las filtraciones son accidentales: tokens en archivos HAR compartidos, en informes de errores con un comando curl copiado, en logs de CI que imprimen cabeceras, en herramientas de analítica que registran URLs con ?token= o en capturas de las herramientas de desarrollo.

Censúralos antes de compartir. Conserva el header y el payload si hacen falta para depurar, y elimina la firma: el token deja de ser utilizable pero las claims siguen legibles.

# quitar la firma antes de pegar un token en un ticket
printf '%s' "$TOKEN" | cut -d. -f1,2 | sed 's/$/.CENSURADO/'

Configura tus sistemas para que hagan lo mismo: los frameworks de logs y los rastreadores de errores como Sentry permiten reglas para ocultar Authorization, Cookie y parámetros llamados token. Nunca pongas tokens en URLs: las query strings quedan en logs del servidor, en el historial y en la cabecera Referer.

Si un token se ha filtrado

Los tokens de acceso normalmente no se pueden revocar uno a uno. Revisa su exp: si caduca en minutos, la exposición es corta. Los refresh tokens sí pueden y deben revocarse, mediante el endpoint de revocación del proveedor (RFC 7009) o su consola de administración, junto con las sesiones del usuario.

Si lo que se filtró es una clave de firma, y no un token suelto, ya no puedes confiar en ningún token firmado con ella. Rota la clave, elimínala del JWKS y asume que los usuarios tendrán que volver a iniciar sesión.

Que los tokens de acceso vivan poco, entre 5 y 15 minutos, es lo que convierte una filtración en un incidente menor.

Dónde guardar los tokens en el navegador

Todo lo que puede leer JavaScript, lo puede leer también el JavaScript inyectado por un fallo XSS: localStorage, sessionStorage y las variables en memoria. Además, localStorage persiste al cerrar la pestaña.

Para aplicaciones web propias, el patrón más robusto deja los tokens fuera de JavaScript: el servidor, o un backend-for-frontend, guarda los tokens y entrega al navegador una cookie de sesión HttpOnly, Secure y SameSite=Lax o Strict. Si una SPA tiene que manejar tokens, mantenlos en memoria, de vida corta y con rotación de refresh tokens.

Fuentes y lecturas recomendadas

RFC 6750 define el uso de tokens al portador y desaconseja ponerlos en URLs. RFC 7519 y RFC 7515 definen JWT y su codificación Base64url. RFC 7009 define la revocación de tokens y RFC 9449 los tokens DPoP. El borrador del IETF "OAuth 2.0 for Browser-Based Applications" trata el almacenamiento de tokens, y MDN documenta atob() y TextDecoder.

Frequently asked questions

¿Es seguro pegar un JWT en un decodificador online?

Solo si el decodificador funciona por completo en tu navegador y el token no es una credencial de producción que te importe. Un JWT sirve a quien lo tenga hasta que caduca, así que decodifica los tokens sensibles en local, por ejemplo en la consola del navegador.

¿Cómo decodifico un token JWT en JavaScript?

Divide el token por los puntos, convierte cada parte de Base64url a Base64 (`-`→`+`, `_`→`/` y relleno `=`), decodifica con `atob`, convierte a UTF-8 con `TextDecoder` y aplica `JSON.parse`. Librerías como `jose` ofrecen `decodeJwt()`.

¿Decodificar un JWT lo verifica?

No. Decodificar solo lee las claims, y cualquiera puede crear un token con las claims que quiera. La verificación comprueba la firma con la clave del emisor y debe hacerse en el servidor.

¿Qué hago si se ha filtrado un JWT?

Revisa su `exp`: los tokens de acceso de vida corta caducan pronto. Revoca el refresh token y las sesiones del usuario en el proveedor de identidad. Si se filtró la clave de firma, rótala y elimínala del JWKS.

¿Debo guardar los JWT en localStorage?

Es cómodo, pero cualquier script puede leerlo, incluido código inyectado por XSS. En aplicaciones web es preferible usar cookies HttpOnly, Secure y SameSite gestionadas por el servidor.

¿Cómo comparto un JWT para depurar sin filtrarlo?

Elimina la firma, la tercera parte, antes de compartirlo. El header y el payload siguen siendo legibles, pero el token ya no se puede usar.