← Volver al blog
🔑 Seguridad9 min de lectura·

Cómo se valida un JWT y qué es JWKS, explicado con diagramas

Cada vez que una API recibe un Authorization: Bearer … tiene que responder una pregunta en milisegundos: ¿este token es auténtico y sigue siendo válido? La respuesta no es «preguntarle a una base de datos», sino verificar una firma criptográfica. Y para verificar esa firma sin compartir ningún secreto entra en juego JWKS. En este artículo desarmamos el JWT pieza por pieza, seguimos el proceso de validación paso a paso y explicamos qué es exactamente un JWKS y por qué hace que todo escale.

Qué es un JWT por dentro

Un JSON Web Token es una cadena de texto con tres partes separadas por puntos: header.payload.signature. Cada parte es un JSON codificado en Base64Url (no cifrado: cualquiera puede leerlo). Lo importante es que la tercera parte, la firma, hace que el contenido sea a prueba de manipulación.

Header{ "alg": "RS256", "typ": "JWT", "kid": "a1b2c3" }Payload{ "sub": "u-42", "iss": "auth.app", "exp": 1735700000 }SignatureRSASHA256( base64(header)+ '.'+base64(payload))..Cada parte se codifica en Base64Url y se une con puntos: header.payload.signatureeyJhbGciOiJSUzI1NiIsImtpZCI6ImExYjJjMyJ9.eyJzdWIiOiJ1LTQyIn0.QW5kVGhlU2lnbmF0dXJl…
Las tres partes de un JWT. El header y el payload son JSON legibles; la firma los sella.
  • Header: dice cómo está firmado el token. Dos campos clave: alg (el algoritmo, p. ej. RS256) y kid (el identificador de la clave que lo firmó).
  • Payload: los claims, datos sobre el usuario y el token: sub (quién), iss (quién lo emitió), aud (para quién es), exp (cuándo expira).
  • Signature: el resultado de firmar header.payload con la clave del emisor. Es lo único que no se puede falsificar.

⚠️ El payload NO es secreto

Base64Url es codificación, no cifrado. Cualquiera puede decodificar el payload y leer los claims. Nunca pongas datos sensibles (contraseñas, PII, secretos) dentro de un JWT. Lo que protege la firma es la integridad (que no se altere), no la confidencialidad.

Firmar y verificar: la magia de la criptografía asimétrica

Hay dos familias de algoritmos de firma. Con los simétricos (HS256) se usa la misma clave secreta para firmar y para verificar: quien verifica podría también falsificar tokens. Con los asimétricos (RS256, ES256) hay un par de claves: una privada que solo tiene el emisor y firma, y una pública que cualquiera puede usar para verificar pero no para firmar.

Emisortiene la clave PRIVADAFirma el (header.payload)sign(datos, privada) → signatureAPIsolo la clave PÚBLICAdatosJWT firmadoLa API verifica sin conocer el secretoverify(datos, signature, pública) → ✓ / ✗Si un bit del payload cambia, la firma deja de validar.
El emisor firma con su clave privada; la API verifica con la clave pública. El secreto nunca sale del emisor.

Esta asimetría es lo que hace práctico el modelo distribuido: puedes tener cientos de APIs validando tokens, todas con la clave pública, sin que ninguna pueda emitir tokens falsos. Por eso los proveedores de identidad serios (Cognito, Auth0, Keycloak, Google) firman con RS256 y no con HS256.

🚫 El ataque clásico: alg = none y la confusión de algoritmos

Dos vulnerabilidades históricas: (1) aceptar alg: none (un token «sin firma» que algunas librerías mal configuradas daban por válido); y (2) la confusión de algoritmos, donde un atacante cambia RS256 por HS256 y usa la clave pública (que es conocida) como si fuera el secreto HMAC. La defensa: fija el algoritmo esperado en el verificador y nunca dejes que el token decida cómo quiere ser validado.

El proceso de validación, paso a paso

Cuando llega una petición con un JWT, la API no solo comprueba la firma: valida una serie de condiciones en orden. Si cualquiera falla, el token se rechaza con 401.

ClienteAPIJWKS1. Request + Bearer <JWT>2. Lee el header → kid y alg3. ¿kid en caché? Si no, GET /.well-known/jwks.json4. Devuelve el set de claves públicas5. Elige la clave por kid y verifica la firma6. Valida claims: iss, aud, exp, nbf7a. Firma + claims OK → 2007b. Firma/claim inválido → 401El JWKS se cacheano se descarga en cada request
Validación de un JWT en la API. La clave pública se obtiene del JWKS (y se cachea) para verificar la firma antes de revisar los claims.
  • 1-2. Leer el header. La API extrae alg y kid. El kid dice cuál de las claves públicas del emisor hay que usar.
  • 3-4. Obtener la clave pública. Si no la tiene cacheada, consulta el endpoint JWKS del emisor y busca la clave cuyo kid coincide.
  • 5. Verificar la firma. Recalcula la firma de header.payload con esa clave pública y la compara con la del token. Si no cuadra, el token fue alterado o lo firmó otro.
  • 6. Validar los claims. Aunque la firma sea válida, hay que comprobar: iss (¿lo emitió quien espero?), aud (¿es para mi API?), exp (¿no ha expirado?) y nbf (¿ya es válido?).
  • 7. Decidir. Firma correcta y claims correctos → sigue. Si no → 401 Unauthorized.

⚠️ Verificar la firma no es suficiente

Un error muy común es dar por bueno un token solo porque la firma valida. Un token legítimo pero expirado, emitido por otro issuer o destinado a otra audiencia también debe rechazarse. Firma y claims, siempre.

Qué es JWKS y por qué existe

JWKS (JSON Web Key Set) es simplemente un documento JSON, publicado por el emisor en una URL conocida (normalmente /.well-known/jwks.json), que contiene el conjunto de claves públicas con las que firma sus tokens. Cada clave trae su propio kid, de modo que el verificador sabe cuál usar.

Emisorpublica su endpoint/.well-known/jwks.jsonJSON Web Key Setkid: a1b2c3RSA pública · activa (firma los nuevos)kid: x9y8z7RSA pública · activa (rotación en curso)kid: old-01RSA pública · antigua (aún válida)El `kid` del header del JWT dice qué clave usar para verificar.
El JWKS publica varias claves públicas, cada una con su kid. El JWT indica en su header qué kid lo firmó.

Un JWKS real se ve así:

GET https://auth.example.com/.well-known/jwks.json

{
  "keys": [
    {
      "kty": "RSA",
      "use": "sig",
      "kid": "a1b2c3",
      "alg": "RS256",
      "n": "0vx7agoebGcQSuuPiLJXZptN...",   // módulo (clave pública)
      "e": "AQAB"                            // exponente
    },
    {
      "kty": "RSA",
      "use": "sig",
      "kid": "x9y8z7",
      "alg": "RS256",
      "n": "qDi7Tx4DhNvZ8Yx2...",
      "e": "AQAB"
    }
  ]
}

JWKS resuelve dos problemas de golpe:

  • Distribución de claves sin secretos. Las APIs no necesitan que nadie les «entregue» la clave: la descargan del endpoint público. Añadir una API nueva no requiere configurar nada en el emisor.
  • Rotación de claves transparente. El emisor puede introducir una clave nueva (nuevo kid) y empezar a firmar con ella, manteniendo la anterior en el set mientras existan tokens vigentes firmados con ella. Como el verificador elige por kid, la rotación no rompe nada.

El rol del kid en la rotación

El kid del header es el pegamento entre el token y el JWKS. Cuando el emisor rota claves, los tokens viejos siguen trayendo el kid antiguo y los nuevos elkid nuevo; ambas claves conviven en el JWKS durante la transición. Por eso una API bien hecha, si recibe un kid desconocido, refresca el JWKS (una vez) antes de rechazar: quizá es una clave recién rotada.

Cachear el JWKS: el detalle que no se puede olvidar

Descargar el JWKS en cada request sería un desastre de rendimiento y una dependencia dura del emisor. La práctica correcta es cachear las claves y refrescarlas solo cuando aparece un kid desconocido o cuando vence un TTL prudente.

# Pseudocódigo del verificador (patrón habitual con una librería JWT)
claves = cache.get_jwks()                 # cacheado en memoria

token = leer_bearer(request)
header = decodificar_header(token)        # sin verificar aún

clave = claves.por_kid(header["kid"])
if clave is None:                         # kid nuevo -> quizá rotaron
    claves = cache.refresh_jwks()         # refresca UNA vez
    clave = claves.por_kid(header["kid"])

payload = jwt.verificar(
    token,
    clave_publica=clave,
    algoritmos=["RS256"],                 # fija el alg: nunca confíes en el header
    issuer="https://auth.example.com",    # valida iss
    audience="mi-api",                    # valida aud
)                                          # la librería también valida exp/nbf
# si algo falla -> excepción -> 401

✅ Deja que una librería madura haga el trabajo

No implementes la verificación de firma a mano. Usa una librería probada (p. ej. PyJWT con PyJWKClient, jose, o la del propio proveedor) que ya maneja Base64Url, selección por kid, fijado de algoritmo y validación de claims. Reinventar criptografía es la receta para introducir bugs de seguridad.

Buenas prácticas al validar JWT

  • Fija el algoritmo esperado (RS256); rechaza none y HMAC si firmas con RSA.
  • Valida siempre iss, aud y exp, no solo la firma.
  • Cachea el JWKS y refréscalo ante un kid desconocido; nunca lo descargues por request.
  • Usa tokens de vida corta: el JWT es autovalidable, así que «revocarlo» antes de exp es difícil.
  • Si necesitas revocación inmediata, apóyate en tokens cortos + refresh, o una lista de revocación consultada aparte.
  • Transporta el token solo por TLS y, en navegador, evita exponerlo (patrón BFF + cookie HttpOnly).

💡 JWT es stateless: su mayor virtud y su mayor límite

Como el token lleva su propia prueba de validez, la API no consulta un almacén de sesiones: escala muy bien. El precio es que no puedes «apagar» un token ya emitido hasta que expire. De ahí la regla de oro: vida corta para los access tokens.

En resumen

Validar un JWT es verificar una firma criptográfica y luego comprobar sus claims. Con algoritmos asimétricos (RS256), el emisor firma con su clave privada y cualquier API verifica con la pública, sin compartir secretos. JWKS es el endpoint donde el emisor publica esas claves públicas, identificadas por kid, lo que permite distribuir claves y rotarlas sin tocar las APIs. Cachea el JWKS, fija el algoritmo, valida siempre los claims y usa tokens de vida corta: con eso tienes autenticación distribuida, segura y que escala.