JWT Decoder Python — Guía para decodificar JWT con PyJWT

·DevOps Engineer & Python Automation Specialist·Revisado porMaria Santos·Publicado

Usa el Decodificador JWT gratuito directamente en tu navegador — sin instalación.

Probar Decodificador JWT online →

Cualquier API que use autenticación basada en tokens te entrega un JWT en algún momento, y averiguar qué contiene es una de esas tareas que aparece constantemente durante el desarrollo. Un decodificador JWT en Python toma esa cadena base64 opaca y la convierte en un diccionario de claims legible con el que puedes trabajar. El paquete de PyPI que necesitas es PyJWT — se instala con pip install PyJWT pero se importa con import jwt. Esta guía recorre jwt.decode() con verificación completa de firma, la decodificación sin secreto para inspección rápida, la decodificación manual con base64 sin ninguna librería, la verificación de clave pública RS256 y los errores más comunes que he encontrado en sistemas de autenticación en producción. Para una comprobación puntual, el Decodificador JWT online hace esto al instante sin ningún código. Todos los ejemplos están pensados para Python 3.10+ y PyJWT 2.x.

  • pip install PyJWT, luego import jwt — el nombre del paquete y el nombre de importación son distintos, lo que confunde a casi todo el mundo.
  • jwt.decode(token, key, algorithms=["HS256"]) devuelve un dict plano con los claims. Pasa siempre algorithms explícitamente.
  • Para inspeccionar claims sin verificación: jwt.decode(token, options={"verify_signature": False}, algorithms=["HS256"]).
  • Para tokens RSA/EC: pip install PyJWT cryptography — el backend cryptography es obligatorio para algoritmos asimétricos.
  • La decodificación manual (base64 + json) funciona sin ninguna librería pero omite toda validación de firma y expiración.

¿Qué es la decodificación JWT?

Un JSON Web Token es tres segmentos codificados en base64url separados por puntos: un encabezado (algoritmo y tipo de token), un payload (los claims — ID de usuario, roles, tiempo de expiración) y una firma. Decodificar un JWT significa extraer los segmentos de encabezado y payload, decodificarlos con base64url y parsear el JSON resultante en un diccionario de claims.

El encabezado indica qué algoritmo se utilizó para firmar el token y a veces incluye un kid (identificador de clave) para encontrar la clave de verificación correcta. El payload contiene los datos reales: a quién se emitió el token (sub), cuándo expira (exp), para qué servicio está destinado (aud), más cualquier claim personalizado que defina tu aplicación. El segmento de firma demuestra que el token no fue manipulado, pero necesitas la clave secreta o la clave pública para verificarlo. Decodificar y verificar son operaciones separadas. Puedes decodificar el payload sin verificar la firma (útil para depuración), pero nunca confíes en claims no verificados para tomar decisiones de autorización.

Before · json
After · json
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c3JfOGYyYSIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTcxMTgxNTYwMH0.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
{
  "sub": "usr_8f2a",
  "role": "admin",
  "exp": 1711815600
}

jwt.decode() — Decodificar y verificar con PyJWT

jwt.decode() es la función principal de la librería PyJWT. Recibe la cadena de token codificada, la clave secreta (para algoritmos HMAC) o la clave pública (para RSA/EC) y una lista algorithms obligatoria. La función verifica la firma, comprueba los claims estándar como exp y nbf, y devuelve el payload como un diccionario Python. Si algo falla — firma incorrecta, token expirado, algoritmo incorrecto — lanza una excepción específica.

Ejemplo mínimo funcional

Python 3.10+
import jwt

# A shared secret between the issuer and this service
SECRET_KEY = "k8s-webhook-signing-secret-2026"

# Encode a token first (simulating what an auth server would issue)
token = jwt.encode(
    {"sub": "usr_8f2a", "role": "admin", "team": "platform"},
    SECRET_KEY,
    algorithm="HS256"
)
print(token)
# eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c3Jf...

# Decode and verify the token
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
print(payload)
# {'sub': 'usr_8f2a', 'role': 'admin', 'team': 'platform'}
print(payload["role"])
# admin

El parámetro algorithms es una lista, no una cadena individual, y es obligatorio en PyJWT 2.x. Esto es una medida de seguridad: sin él, un atacante podría crear un token con alg: none en el encabezado y saltarse la verificación por completo. Especifica siempre exactamente qué algoritmos acepta tu aplicación. Si solo emites tokens HS256, la lista debe ser ["HS256"] — no ["HS256", "RS256", "none"]. Mantener la lista acotada reduce la superficie de ataque.

Algo que me confundió al principio: PyJWT 2.x cambió jwt.encode() para devolver una cadena en lugar de bytes. Si lees respuestas antiguas en Stack Overflow que llaman a .decode("utf-8") sobre el token codificado, ese código es de la era PyJWT 1.x y lanzará un AttributeError en la versión 2.x. El token ya es una cadena — úsalo directamente.

Ciclo completo con expiración

Python 3.10+ — encode then decode with exp
import jwt
from datetime import datetime, timedelta, timezone

SECRET_KEY = "webhook-processor-secret"

# Create a token that expires in 1 hour
payload = {
    "sub": "svc_payment_processor",
    "iss": "auth.internal.empresa.com",
    "aud": "https://api.example.com",
    "exp": datetime.now(timezone.utc) + timedelta(hours=1),
    "permissions": ["orders:read", "refunds:create"],
}

token = jwt.encode(payload, SECRET_KEY, algorithm="HS256")

# Later, when the token arrives in a request header:
try:
    decoded = jwt.decode(
        token,
        SECRET_KEY,
        algorithms=["HS256"],
        audience="https://api.example.com",
        issuer="auth.internal.empresa.com",
    )
    print(f"Service: {decoded['sub']}")
    print(f"Permissions: {decoded['permissions']}")
except jwt.ExpiredSignatureError:
    print("Token expired — request re-authentication")
except jwt.InvalidAudienceError:
    print("Token not intended for this API")
except jwt.InvalidIssuerError:
    print("Token issued by unknown authority")
Nota:PyJWT convierte los objetos datetime a timestamps Unix automáticamente durante la codificación. Durante la decodificación, los claims exp, iat y nbf se devuelven como enteros, no como objetos datetime. Debes convertirlos tú mismo con datetime.fromtimestamp(payload["exp"], tz=timezone.utc).

Decodificar un JWT sin verificación de firma

A veces necesitas leer los claims antes de poder verificar el token. Un escenario habitual: el encabezado del token contiene un campo kid (identificador de clave) y necesitas obtener la clave pública correspondiente desde un endpoint JWKS antes de poder verificar. PyJWT soporta esto con la opción verify_signature: False. Sigues pasando la lista algorithms, pero el argumento key se ignora.

Python 3.10+ — unverified decode
import jwt

token = (
    "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZy0xNzI2In0"
    ".eyJzdWIiOiJ1c3JfM2M3ZiIsInNjb3BlIjoicmVhZDpvcmRlcnMiLCJpc3MiOiJhdXRoLmV4YW1wbGUuY29tIn0"
    ".signature_placeholder"
)

# Step 1: Read claims without verification to get routing info
unverified = jwt.decode(
    token,
    options={"verify_signature": False},
    algorithms=["RS256"]
)
print(unverified)
# {'sub': 'usr_3c7f', 'scope': 'read:orders', 'iss': 'auth.example.com'}

# Step 2: Read the header to find which key to use
header = jwt.get_unverified_header(token)
print(header)
# {'alg': 'RS256', 'typ': 'JWT', 'kid': 'sig-1726'}
# Now use header['kid'] to fetch the correct public key from your JWKS endpoint
Aviso:Los tokens no verificados no son de confianza. Usa este patrón solo para decisiones de enrutamiento (qué clave obtener, qué tenant consultar). Nunca tomes decisiones de autorización basadas en claims no verificados. Un atacante puede poner cualquier cosa en el payload.

Hay una distinción sutil aquí. jwt.get_unverified_header() solo lee el encabezado — el primer segmento. La llamada a jwt.decode() con verify_signature: False lee el payload (segundo segmento). Entre ambas puedes extraer todo lo que contiene un token sin necesidad de una clave. PyJWT sigue validando que el token tenga la estructura correcta (tres segmentos separados por puntos, base64 válido, JSON válido) incluso cuando la verificación de firma está desactivada. Si el token es estructuralmente inválido, lanza DecodeError independientemente de las opciones que pases.

Referencia de parámetros de jwt.decode()

La firma completa es jwt.decode(jwt, key, algorithms, options, audience, issuer, leeway, require). Todos los parámetros posteriores a algorithms son solo de palabra clave.

Parámetro
Tipo
Valor por defecto
Descripción
jwt
str | bytes
(obligatorio)
La cadena JWT codificada que se va a decodificar
key
str | bytes | dict
(obligatorio)
Clave secreta (HMAC) o clave pública (RSA/EC) para la verificación
algorithms
list[str]
(obligatorio)
Algoritmos permitidos — ej. ["HS256"], ["RS256"]. Nunca lo omitas.
options
dict
{}
Modifica las banderas de verificación: verify_signature, verify_exp, verify_aud, etc.
audience
str | list[str]
None
Valor esperado del claim aud — lanza InvalidAudienceError si no coincide
issuer
str
None
Valor esperado del claim iss — lanza InvalidIssuerError si no coincide
leeway
timedelta | int
0
Segundos de tolerancia al desfase de reloj para las comprobaciones de exp y nbf
require
list[str]
[]
Claims que deben estar presentes — lanza MissingRequiredClaimError si faltan

El dict options ofrece control preciso sobre qué validaciones realiza PyJWT. Las claves corresponden a comprobaciones individuales: verify_signature, verify_exp, verify_nbf, verify_iss, verify_aud y verify_iat. Todas son True por defecto salvo que las establezcas explícitamente en False. En producción, déjalas todas en sus valores por defecto. La única vez que deshabilito comprobaciones individuales es durante el desarrollo, cuando trabajo con tokens de prueba caducados y necesito saltarme la expiración temporalmente.

Python 3.10+ — using options and require
import jwt

# Require specific claims to be present — raises MissingRequiredClaimError if absent
payload = jwt.decode(
    token,
    SECRET_KEY,
    algorithms=["HS256"],
    options={"require": ["exp", "iss", "sub"]},
    issuer="auth.internal.empresa.com",
)

# During development only: skip expiry to test with old tokens
dev_payload = jwt.decode(
    token,
    SECRET_KEY,
    algorithms=["HS256"],
    options={"verify_exp": False},  # DO NOT use in production
)

Decodificación manual de JWT con base64 y json

Puedes decodificar el payload de un JWT usando únicamente la biblioteca estándar de Python — sin pip install necesario. Esto es genuinamente útil en varios casos: scripts de depuración donde añadir una dependencia es excesivo, entornos CI restringidos donde solo está disponible la biblioteca estándar, funciones AWS Lambda donde quieres minimizar el tiempo de arranque en frío, o simplemente para entender qué es un JWT bajo el capó. El proceso es sencillo: divide por puntos, toma el segmento que quieres, añade relleno base64, decodifica y parsea el JSON.

Los módulos base64 y json están ambos en la biblioteca estándar de Python, por lo que este enfoque funciona en cualquier instalación de Python desde 3.6 en adelante. Las funciones siguientes tratan el encabezado (primer segmento) y el payload (segundo segmento) por separado:

Python 3.10+ — manual JWT decode without any library
import base64
import json

def decode_jwt_payload(token: str) -> dict:
    """Decode the JWT payload without signature verification.
    Works with any JWT — HS256, RS256, ES256, etc.
    """
    parts = token.split(".")
    if len(parts) != 3:
        raise ValueError(f"Expected 3 JWT segments, got {len(parts)}")

    payload_b64 = parts[1]
    # base64url uses - and _ instead of + and /
    # Python's urlsafe_b64decode handles this, but needs padding
    payload_b64 += "=" * (-len(payload_b64) % 4)
    payload_bytes = base64.urlsafe_b64decode(payload_b64)
    return json.loads(payload_bytes)


def decode_jwt_header(token: str) -> dict:
    """Decode the JWT header (algorithm, key ID, type)."""
    header_b64 = token.split(".")[0]
    header_b64 += "=" * (-len(header_b64) % 4)
    return json.loads(base64.urlsafe_b64decode(header_b64))


# Example usage
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c3JfOGYyYSIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTcxMTgxNTYwMH0.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"

header = decode_jwt_header(token)
print(f"Algorithm: {header['alg']}")
# Algorithm: HS256

claims = decode_jwt_payload(token)
print(f"Subject: {claims['sub']}")
print(f"Role: {claims['role']}")
# Subject: usr_8f2a
# Role: admin

El truco del relleno (+= "=" * (-len(s) % 4)) es lo que todo el mundo olvida. El base64url de JWT omite los caracteres = finales, pero urlsafe_b64decode de Python los requiere. Sin corregir el relleno, obtienes un binascii.Error: Incorrect padding.

Nota:La decodificación manual no verifica la firma. Cualquiera puede modificar el payload de un JWT y recodificarlo. Usa este enfoque solo para inspección y depuración, nunca para lógica de autorización. Para cualquier cosa que importe, usa jwt.decode() con una clave real.

Decodificar JWTs desde respuestas API y archivos de tokens

Los dos escenarios más habituales en el mundo real: extraer un JWT de una respuesta HTTP (un endpoint de token OAuth, una API de inicio de sesión) y leer tokens desde archivos (credenciales de cuenta de servicio, secretos montados en Kubernetes, tokens en caché en disco). Ambos requieren un manejo de errores adecuado. Las peticiones de red fallan. Los archivos desaparecen. Los tokens expiran entre que se guardaron en caché y cuando se leen.

Los ejemplos siguientes usan httpx para las llamadas HTTP (puedes sustituirlo por requests si lo prefieres, el patrón es idéntico) y pathlib.Path para las operaciones con archivos. Cada ejemplo captura excepciones específicas de PyJWT en lugar de un genérico except Exception, para poder responder adecuadamente a cada modo de fallo: reautenticación en expiración, alerta ante firma inválida, reintento ante timeout de red.

Decodificar un JWT desde una respuesta API

Python 3.10+ — decode JWT from OAuth token endpoint
import jwt
import httpx  # or requests

TOKEN_ENDPOINT = "https://auth.example.com/oauth/token"
SECRET_KEY = "shared-webhook-signing-key"

def get_and_decode_token() -> dict:
    """Fetch an access token from the auth server and decode it."""
    try:
        response = httpx.post(
            TOKEN_ENDPOINT,
            data={
                "grant_type": "client_credentials",
                "client_id": "svc_order_processor",
                "client_secret": "cs_9f3a7b2e",
            },
            timeout=10.0,
        )
        response.raise_for_status()
    except httpx.HTTPError as exc:
        raise RuntimeError(f"Token request failed: {exc}") from exc

    token_data = response.json()
    access_token = token_data["access_token"]

    try:
        payload = jwt.decode(
            access_token,
            SECRET_KEY,
            algorithms=["HS256"],
            audience="https://api.example.com",
        )
        return payload
    except jwt.InvalidTokenError as exc:
        raise RuntimeError(f"Invalid token from auth server: {exc}") from exc


claims = get_and_decode_token()
print(f"Service: {claims['sub']}, Scopes: {claims.get('scope', 'none')}")

Decodificar un JWT desde un archivo

Python 3.10+ — read and decode a cached token file
import jwt
from pathlib import Path
from datetime import datetime, timezone

TOKEN_PATH = Path("/var/run/secrets/service-account-token")
PUBLIC_KEY_PATH = Path("/etc/ssl/auth/public_key.pem")

def decode_token_from_file() -> dict:
    """Read a JWT from a file, verify with a PEM public key."""
    try:
        token = TOKEN_PATH.read_text().strip()
        public_key = PUBLIC_KEY_PATH.read_text()
    except FileNotFoundError as exc:
        raise RuntimeError(f"Missing file: {exc.filename}") from exc

    try:
        payload = jwt.decode(
            token,
            public_key,
            algorithms=["RS256"],
            audience="https://internal-api.example.com",
        )
    except jwt.ExpiredSignatureError:
        exp_time = jwt.decode(
            token,
            options={"verify_signature": False},
            algorithms=["RS256"],
        ).get("exp", 0)
        expired_at = datetime.fromtimestamp(exp_time, tz=timezone.utc)
        raise RuntimeError(f"Token expired at {expired_at.isoformat()}")
    except jwt.InvalidTokenError as exc:
        raise RuntimeError(f"Token verification failed: {exc}") from exc

    return payload


claims = decode_token_from_file()
print(f"Subject: {claims['sub']}, Issuer: {claims['iss']}")

Decodificación JWT por línea de comandos

A veces solo necesitas inspeccionar un token desde la terminal sin escribir un script. Quizás estás depurando un flujo OAuth y quieres ver qué hay en la cabecera Authorization, o has cogido un token de las DevTools del navegador y quieres comprobar su expiración. La opción -c de Python convierte esto en un comando de una línea. Pasa el token por la entrada estándar y obtienes los claims como JSON formateado. Sin archivo de script, sin entorno virtual.

Bash
# Decode JWT payload from clipboard or variable (no verification)
echo "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3JfOGYyYSIsInJvbGUiOiJhZG1pbiJ9.sig" \
  | python3 -c "
import sys, base64, json
token = sys.stdin.read().strip()
payload = token.split('.')[1]
payload += '=' * (-len(payload) % 4)
print(json.dumps(json.loads(base64.urlsafe_b64decode(payload)), indent=2))
"
# {
#   "sub": "usr_8f2a",
#   "role": "admin"
# }
Bash
# Decode JWT header to check algorithm and key ID
echo "eyJhbGciOiJSUzI1NiIsImtpZCI6InNpZy0xNzI2In0.payload.sig" \
  | python3 -c "
import sys, base64, json
token = sys.stdin.read().strip()
header = token.split('.')[0]
header += '=' * (-len(header) % 4)
print(json.dumps(json.loads(base64.urlsafe_b64decode(header)), indent=2))
"
# {
#   "alg": "RS256",
#   "kid": "sig-1726"
# }
Bash
# If PyJWT is installed, verify and decode in one step
python3 -c "
import jwt, sys, json
token = sys.argv[1]
payload = jwt.decode(token, options={'verify_signature': False}, algorithms=['HS256'])
print(json.dumps(payload, indent=2))
" "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3JfOGYyYSJ9.sig"

Como alternativa visual sin ninguna configuración de terminal, pega tu token en el Decodificador JWT de ToolDeck y ve el encabezado, el payload y el estado de verificación de la firma al instante.

python-jose y otras alternativas

python-jose es una librería JWT alternativa que soporta JWS, JWE (tokens cifrados) y JWK de forma nativa. Si tu aplicación necesita manejar JWTs cifrados (JWE) — donde el payload en sí está cifrado, no solo firmado — python-jose es la opción correcta porque PyJWT no soporta JWE en absoluto. La librería también tiene manejo integrado de conjuntos de claves JWKS, lo que simplifica la integración con proveedores de identidad como Auth0, Okta o Keycloak que exponen conjuntos de claves rotativas. La interfaz de decodificación es casi idéntica a la de PyJWT, por lo que cambiar entre ellas requiere cambios mínimos de código:

Python 3.10+ — python-jose
# pip install python-jose[cryptography]
from jose import jwt as jose_jwt

token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3JfOGYyYSIsInNjb3BlIjoib3JkZXJzOnJlYWQifQ.signature"

# Verified decode — same pattern as PyJWT
payload = jose_jwt.decode(
    token,
    "signing-secret-key",
    algorithms=["HS256"],
    audience="https://api.example.com",
)
print(payload)
# {'sub': 'usr_8f2a', 'scope': 'orders:read'}

# Unverified decode
claims = jose_jwt.get_unverified_claims(token)
header = jose_jwt.get_unverified_header(token)
print(f"Algorithm: {header['alg']}, Subject: {claims['sub']}")

Mi recomendación: empieza con PyJWT. Cubre el 95% de los casos de uso JWT, tiene la comunidad más grande y la API es limpia. Cambia a python-jose si necesitas soporte de JWE o prefieres su manejo de JWKS. Una tercera opción que merece mención es Authlib, que incluye el manejo de JWT dentro de un framework OAuth/OIDC mucho más amplio. Si ya usas Authlib para flujos de cliente OAuth, su módulo authlib.jose.jwt te evita añadir una segunda dependencia JWT. En cualquier otro caso, es una dependencia pesada solo para decodificar tokens.

Salida en terminal con resaltado de sintaxis

Leer claims JWT en crudo en un terminal está bien para comprobaciones rápidas, pero cuando depuras payloads de tokens con regularidad (yo hacía esto a diario mientras construía una pasarela de autenticación interna), la salida con colores marca una diferencia real. Los valores de cadena, los números, los booleanos y null se muestran en colores distintos, lo que significa que puedes detectar un permiso ausente o un timestamp de expiración incorrecto de un vistazo sin leer cada carácter.

La librería rich (pip install rich) tiene una función print_json que acepta una cadena JSON o un dict Python y lo imprime con resaltado de sintaxis completo en el terminal. Combínala con PyJWT para un flujo de inspección JWT en dos líneas:

Python 3.10+ — rich terminal output
# pip install rich PyJWT
import jwt
from rich import print_json

token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3JfOGYyYSIsInJvbGUiOiJhZG1pbiIsInBlcm1pc3Npb25zIjpbIm9yZGVyczpyZWFkIiwicmVmdW5kczpjcmVhdGUiXSwiZXhwIjoxNzExODE1NjAwfQ.sig"

payload = jwt.decode(
    token,
    options={"verify_signature": False},
    algorithms=["HS256"]
)

# Colorized, indented JSON output in the terminal
print_json(data=payload)
# {
#   "sub": "usr_8f2a",           ← strings in green
#   "role": "admin",
#   "permissions": [
#     "orders:read",
#     "refunds:create"
#   ],
#   "exp": 1711815600            ← numbers in cyan
# }
Nota:La salida de rich contiene códigos de escape ANSI. No la escribas en archivos ni la devuelvas desde endpoints API — es solo para visualización en terminal. Usa json.dumps() cuando necesites salida en texto plano.

Procesamiento de grandes lotes de tokens

Los tokens JWT son pequeños (normalmente menos de 2 KB cada uno), pero hay escenarios donde los procesas en lote. Análisis de registros de auditoría tras un incidente de seguridad. Scripts de migración de sesiones al cambiar de proveedor de autenticación. Validación masiva por cumplimiento normativo donde necesitas demostrar que todos los tokens emitidos en los últimos 90 días fueron firmados con la clave correcta. Si tienes decenas de miles de tokens en un archivo de registro NDJSON, procesarlos línea a línea evita cargar el archivo entero en memoria y permite reportar resultados de forma incremental.

Validación en lote de tokens desde un registro de auditoría

Python 3.10+ — streaming token validation
import jwt
import json
from pathlib import Path

SECRET_KEY = "audit-log-signing-key"

def validate_token_log(log_path: str) -> dict:
    """Process an NDJSON file where each line has a 'token' field.
    Returns counts of valid, expired, and invalid tokens.
    """
    stats = {"valid": 0, "expired": 0, "invalid": 0}

    with open(log_path) as fh:
        for line_num, line in enumerate(fh, 1):
            line = line.strip()
            if not line:
                continue

            try:
                record = json.loads(line)
                token = record["token"]
            except (json.JSONDecodeError, KeyError):
                stats["invalid"] += 1
                continue

            try:
                jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
                stats["valid"] += 1
            except jwt.ExpiredSignatureError:
                stats["expired"] += 1
            except jwt.InvalidTokenError:
                stats["invalid"] += 1

    return stats


result = validate_token_log("auth-events-2026-03.ndjson")
print(f"Valid: {result['valid']}, Expired: {result['expired']}, Invalid: {result['invalid']}")
# Valid: 14832, Expired: 291, Invalid: 17

Extracción de claims desde una exportación NDJSON de tokens

Python 3.10+ — extract and transform claims from token log
import base64
import json
from datetime import datetime, timezone

def extract_claims_stream(input_path: str, output_path: str):
    """Read tokens line by line, decode payloads, write flattened claims."""
    with open(input_path) as infile, open(output_path, "w") as outfile:
        for line in infile:
            line = line.strip()
            if not line:
                continue

            record = json.loads(line)
            token = record.get("access_token", "")
            parts = token.split(".")
            if len(parts) != 3:
                continue

            payload_b64 = parts[1] + "=" * (-len(parts[1]) % 4)
            claims = json.loads(base64.urlsafe_b64decode(payload_b64))

            # Flatten into an audit-friendly record
            flat = {
                "timestamp": record.get("timestamp"),
                "subject": claims.get("sub"),
                "issuer": claims.get("iss"),
                "expired_at": datetime.fromtimestamp(
                    claims.get("exp", 0), tz=timezone.utc
                ).isoformat(),
            }
            outfile.write(json.dumps(flat) + "\n")

extract_claims_stream("token-audit.ndjson", "claims-extract.ndjson")
Nota:Para archivos de unos pocos cientos de MB, la lectura línea a línea es suficientemente eficiente. Si encuentras límites de rendimiento con volcados de tokens muy grandes, considera usar multiprocessing.Pool para distribuir la validación entre núcleos, ya que cada token es independiente.

Errores comunes

Estos cuatro errores aparecen repetidamente en revisiones de código y en Stack Overflow. Cada uno es fácil de cometer, y el mensaje de error que da PyJWT no siempre apunta directamente a la causa. He visto que solo el problema de nombres de paquetes ha hecho perder horas de depuración cuando alguien instala la librería equivocada y obtiene una API completamente inesperada.

Confundir los nombres de paquetes PyJWT y python-jwt

Problema: Ejecutar pip install jwt o pip install python-jwt instala un paquete completamente diferente. import jwt entonces falla o te da una API que no reconoces.

Solución: Instala siempre con pip install PyJWT. El import es import jwt. Comprueba con pip show PyJWT para confirmar que tienes el paquete correcto.

Before · Python
After · Python
pip install jwt
# or
pip install python-jwt

import jwt  # wrong package — different API entirely
pip install PyJWT

import jwt  # correct — this is PyJWT
print(jwt.__version__)  # 2.x
Omitir el parámetro algorithms

Problema: En PyJWT 1.x, algorithms era opcional y por defecto permitía cualquier algoritmo. Esto creaba una vulnerabilidad de seguridad donde un atacante podía establecer alg: none. PyJWT 2.x ahora lanza DecodeError si falta algorithms.

Solución: Pasa siempre algorithms como una lista explícita. Usa solo los algoritmos con los que tu aplicación emite tokens.

Before · Python
After · Python
# PyJWT 2.x — this raises DecodeError
payload = jwt.decode(token, SECRET_KEY)
# DecodeError: algorithms must be specified
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
Usar jwt.decode() con el tipo de clave incorrecto para RS256

Problema: Pasar una cadena secreta a jwt.decode() con algorithms=["RS256"] lanza InvalidSignatureError. RS256 requiere una clave pública codificada en PEM, no una cadena secreta compartida.

Solución: Carga la clave pública PEM desde un archivo o variable de entorno. Instala el paquete cryptography: pip install PyJWT cryptography.

Before · Python
After · Python
# This fails — RS256 needs a public key, not a string secret
payload = jwt.decode(token, "my-secret", algorithms=["RS256"])
# InvalidSignatureError
public_key = open("public_key.pem").read()
payload = jwt.decode(token, public_key, algorithms=["RS256"])
Olvidar el relleno base64 al decodificar manualmente

Problema: La codificación base64url de JWT elimina los caracteres = finales. base64.urlsafe_b64decode de Python lanza binascii.Error: Incorrect padding si le pasas el segmento en crudo sin corregir el relleno.

Solución: Añade el relleno antes de decodificar: segment += '=' * (-len(segment) % 4). Esta fórmula siempre produce el número correcto de caracteres de relleno (0, 1, 2 o 3).

Before · Python
After · Python
import base64
payload_b64 = token.split(".")[1]
data = base64.urlsafe_b64decode(payload_b64)
# binascii.Error: Incorrect padding
import base64
payload_b64 = token.split(".")[1]
payload_b64 += "=" * (-len(payload_b64) % 4)
data = base64.urlsafe_b64decode(payload_b64)  # works

PyJWT vs alternativas — Comparación rápida

Método
Verifica firma
Valida claims
Tipos personalizados
Requiere instalación
PyJWT jwt.decode()
✓ (exp, aud, iss, nbf)
N/A (devuelve dict)
pip install PyJWT
PyJWT decodificación sin verificar
N/A
pip install PyJWT
Decodificación manual base64
N/A
No (stdlib)
python-jose jwt.decode()
N/A
pip install python-jose
Authlib jwt.decode()
N/A
pip install Authlib
PyJWT + cryptography
✓ (RSA/EC)
N/A
pip install PyJWT cryptography

PyJWT es el punto de partida correcto para la mayoría de las aplicaciones Python. Cubre HMAC y (con el backend cryptography) RSA y EC para la verificación de firma. Si necesitas JWE (tokens cifrados), cambia a python-jose o Authlib. La decodificación manual con base64 funciona para depuración pero no ofrece ninguna garantía de seguridad.

Cuándo uso cada opción: PyJWT para cualquier servicio web estándar con verificación HS256 o RS256. python-jose cuando la arquitectura incluye tokens cifrados o rotación de JWKS. Base64 manual para inspección rápida en entornos donde pip no está disponible (contenedores CI, hosts de producción restringidos, arranques en frío de AWS Lambda donde quieres minimizar dependencias). Authlib cuando el proyecto ya lo usa para flujos de cliente OAuth y añadir otra librería JWT sería redundante.

Como alternativa sin código, pega cualquier token en el Decodificador JWT para ver el encabezado y el payload decodificados con retroalimentación sobre la validación de claims.

Preguntas frecuentes

¿Cómo decodifico un JWT en Python sin verificar la firma?

Pasa options={"verify_signature": False} y algorithms=["HS256"] a jwt.decode(). Esto devuelve el dict del payload sin comprobar la firma. Úsalo solo para inspección — leer claims antes de obtener la clave pública correcta, o para depurar en desarrollo. Nunca omitas la verificación en tokens que controlen el acceso a recursos.

Python
import jwt

token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c3JfOGYyYSIsInJvbGUiOiJhZG1pbiJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"

payload = jwt.decode(
    token,
    options={"verify_signature": False},
    algorithms=["HS256"]
)
print(payload)
# {'sub': 'usr_8f2a', 'role': 'admin'}

¿Cuál es la diferencia entre PyJWT y python-jwt?

PyJWT (pip install PyJWT, import jwt) es la librería JWT más popular para Python, con más de 80 millones de descargas mensuales. python-jwt (pip install python_jwt) es una librería separada, mucho menos utilizada, con una API diferente. Si ves import jwt en el código de alguien, están usando PyJWT. La confusión de nombres viene de que el nombre del paquete en PyPI (PyJWT) difiere del nombre de importación (jwt). Usa PyJWT salvo que tengas una razón específica para no hacerlo.

¿Cómo decodifico un JWT con RS256 en Python?

Instala tanto PyJWT como el backend cryptography: pip install PyJWT cryptography. Luego pasa la clave pública codificada en PEM como argumento key y algorithms=["RS256"]. PyJWT delega la verificación de firma RSA a la librería cryptography. Sin el paquete cryptography instalado, PyJWT lanza un error cuando intentas usar algoritmos RSA o EC.

Python
import jwt

public_key = open("public_key.pem").read()

payload = jwt.decode(
    token,
    public_key,
    algorithms=["RS256"],
    audience="https://api.example.com"
)

¿Por qué PyJWT lanza ExpiredSignatureError?

PyJWT comprueba el claim exp (expiración) por defecto. Si la hora UTC actual supera el timestamp de exp, lanza jwt.ExpiredSignatureError. Puedes añadir tolerancia al desfase de reloj con el parámetro leeway: jwt.decode(token, key, algorithms=["HS256"], leeway=timedelta(seconds=30)). Esto da un margen de 30 segundos. Para deshabilitar la comprobación de expiración por completo (no recomendado en producción), pasa options={"verify_exp": False}.

Python
import jwt
from datetime import timedelta

try:
    payload = jwt.decode(token, secret, algorithms=["HS256"], leeway=timedelta(seconds=30))
except jwt.ExpiredSignatureError:
    print("Token has expired — prompt re-authentication")

¿Puedo leer claims de un JWT sin ninguna librería en Python?

Sí. Divide el token por los puntos, toma el segundo segmento (el payload), rellénalo con caracteres = para que su longitud sea múltiplo de 4, luego decodifica con base64url y parsea el JSON. Esto te da el dict de claims pero no verifica la firma. Es útil en entornos restringidos donde no puedes instalar PyJWT, o para scripts de depuración rápida.

Python
import base64, json

token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3JfOGYyYSJ9.signature"
payload_b64 = token.split(".")[1]
payload_b64 += "=" * (-len(payload_b64) % 4)  # fix padding
claims = json.loads(base64.urlsafe_b64decode(payload_b64))
print(claims)  # {'sub': 'usr_8f2a'}

¿Cómo valido el claim audience con PyJWT?

Pasa el parámetro audience a jwt.decode(): jwt.decode(token, key, algorithms=["HS256"], audience="https://api.example.com"). PyJWT compara el claim aud del token con el valor que proporcionas. Si el token no tiene claim aud, o el valor no coincide, lanza jwt.InvalidAudienceError. También puedes pasar una lista de audiences aceptables si tu servicio acepta tokens destinados a múltiples APIs.

Python
import jwt

payload = jwt.decode(
    token,
    secret,
    algorithms=["HS256"],
    audience=["https://api.example.com", "https://admin.example.com"]
)

Herramientas relacionadas

DV
Dmitri VolkovDevOps Engineer & Python Automation Specialist

Dmitri is a DevOps engineer who relies on Python as his primary scripting and automation language. He builds internal tooling, CI/CD pipelines, and infrastructure automation scripts that run in production across distributed teams. He writes about the Python standard library, subprocess management, file processing, encoding utilities, and the practical shell-adjacent Python that DevOps engineers use every day.

MS
Maria SantosRevisor técnico

Maria is a backend developer specialising in Python and API integration. She has broad experience with data pipelines, serialisation formats, and building reliable server-side services. She is an active member of the Python community and enjoys writing practical, example-driven guides that help developers solve real problems without unnecessary theory.