Decodificar JWT en Python: PyJWT, Authlib y la trampa de verify=False
Python tiene dos librerías JWT sólidas: PyJWT para el caso común, Authlib cuando necesitas la stack JOSE completa. Las dos siguen el RFC 7519 de cerca. El snippet más copy-pasteado de internet para esta tarea es también el más peligroso.
PyJWT es el default
PyJWT (jpadilla/pyjwt) es la opción aburrida y correcta. Maneja HS256, RS256, ES256 y el resto del paisaje de algoritmos, con superficie pequeña. La verificación queda así:
```py import jwt
claims = jwt.decode( token, key=public_key, algorithms=["RS256"], audience="api.example.com", issuer="https://example.com", leeway=30, ) ```
El argumento algorithms es obligatorio desde PyJWT 2.0. Pasa una lista de un solo elemento cuando conoces el algoritmo del IdP; jamás aceptes lo que anuncia la cabecera del token, porque ese campo lo controla el propio token.
verify=False huele a inseguro
La mitad de los snippets de Stack Overflow para "decodificar JWT python" pasan options={"verify_signature": False} para saltarse la comprobación criptográfica. A veces es lo que quieres (loguear los claims de un token roto en un postmortem). En tiempo de petición casi nunca lo es.
```py # Aceptable en un REPL puntual de debug import jwt unsafe = jwt.decode(token, options={"verify_signature": False})
# Jamás en código de producción ```
Si te encuentras esta línea dentro de un handler de petición, trátala como bug. El sentido entero de un JWT es la firma; leer el payload sin comprobarla equivale a confiar en cualquier header que mande el cliente.
Rotación JWKS con PyJWT
Cuando el IdP rota claves no puedes hardcodear la pública. Tira de PyJWKClient para traer el JWKS, cachearlo y elegir la clave correcta por kid:
```py from jwt import PyJWKClient import jwt
jwks = PyJWKClient("https://example.com/.well-known/jwks.json")
def verify(token: str) -> dict: signing_key = jwks.get_signing_key_from_jwt(token).key return jwt.decode( token, signing_key, algorithms=["RS256"], audience="api.example.com", issuer="https://example.com", leeway=30, ) ```
PyJWKClient cachea en proceso. En despliegues con varios workers (gunicorn, uvicorn workers) puede que quieras una caché compartida (Redis, memcached) para evitar que cada worker traiga el JWKS al arrancar.
Authlib cuando necesitas el resto de JOSE
Si solo verificas JWT, PyJWT sobra. Si además necesitas emitir JWE (JWT cifrados), parsear estructuras JWS distintas de JWT, o montar servidores OAuth2 / OIDC, Authlib es el toolkit más amplio. Pesa más que PyJWT y sigue las specs JOSE de forma más estricta.
En FastAPI el cableado típico es una dependencia que devuelve los claims verificados:
```py from fastapi import Depends, HTTPException, status from fastapi.security import HTTPBearer
bearer = HTTPBearer()
def current_user(token = Depends(bearer)): try: return verify(token.credentials) except jwt.PyJWTError: raise HTTPException(status.HTTP_401_UNAUTHORIZED) ```
Las rutas declaran user = Depends(current_user) y jamás llaman a jwt.decode directamente. Una sola ruta de verificación, un solo sitio donde añadir logs de auditoría.
Ejemplo completo
pythonimport jwt
from jwt import PyJWKClient
jwks = PyJWKClient("https://example.com/.well-known/jwks.json")
def verify(token: str) -> dict:
signing_key = jwks.get_signing_key_from_jwt(token).key
return jwt.decode(
token,
signing_key,
algorithms=["RS256"],
audience="api.example.com",
issuer="https://example.com",
leeway=30,
) ¿Solo necesitas el resultado?
Cuando solo quieres leer qué hay en un token antes de escribir el código de verificación, pégalo en el decodificador JWT del navegador. Separa cabecera, payload y firma, muestra el alg y el kid, y nunca toca un servidor, así puedes inspeccionar tokens de producción sin filtrarlos.
Abrir Decodificador JWT →Preguntas frecuentes
¿Qué diferencia hay entre jwt.decode y jwt.get_unverified_header?
get_unverified_header lee solo la cabecera sin chequear firma. Es la forma segura de mirar el kid antes de llamar a decode. Jamás la llames sobre el payload y trátala con cuidado.
¿Cómo verifico un token firmado por Auth0 o Cognito?
Ambos publican un JWKS en una URL well-known. Apunta ahí PyJWKClient, fija issuer y audience con los valores de tu tenant y usa RS256 en algorithms. PyJWT resuelve el kid solo.
¿Por qué me sale InvalidAudienceError con un token que parece bueno?
PyJWT compara audience de forma exacta. Si el token trae aud como lista y tú pasas string, debe coincidir con una entrada; si pasas lista, todas las entradas deben encajar. Recorta las barras finales de las URLs primero, son los killers silenciosos.