La diferencia básica es sencilla: JWK describe una clave criptográfica en JSON; JWKS es un conjunto de JWKs. La diferencia práctica aparece al seleccionar claves con kid, rotarlas, descubrir jwks_uri, cachearlas y vincularlas con un issuer confiable.
Uso JWK Inspector para una clave individual y JWKS Validator para validar un token contra un set.
Una JWK representa una clave
{
"kty": "RSA",
"kid": "prod-2026-09",
"use": "sig",
"alg": "RS256",
"n": "...",
"e": "AQAB"
}
Campos relevantes:
kty: tipo de clave;kid: identificador de selección;use: uso previsto;key_ops: operaciones permitidas;alg: algoritmo declarado;- RSA:
n,e; - EC:
crv,x,y.
kid no demuestra confianza; ayuda a elegir candidato.
Un JWKS contiene varias JWKs
{
"keys": [
{ "kty": "RSA", "kid": "old", "alg": "RS256", "use": "sig", "n": "...", "e": "AQAB" },
{ "kty": "RSA", "kid": "new", "alg": "RS256", "use": "sig", "n": "...", "e": "AQAB" }
]
}
Durante key rotation es normal que convivan claves old/new.
Cómo uso kid para seleccionar
JWT kid
↓
buscar en JWKS
↓
filtrar kty/use/key_ops/alg
↓
verificar firma
Si no hay match, pregunto si el JWKS está actualizado y si pertenece al issuer esperado. No elijo la primera clave disponible.
jwks_uri debe formar parte de un trust path
expected issuer
↓
metadata/discovery confiable
↓
jwks_uri
↓
JWKS
↓
matching kid
↓
signature verification
No sigo automáticamente una jku arbitraria del JWT header y confío en la clave que aparezca allí.
Compatibilidad entre alg y kty
Un match de kid no basta. Espero combinaciones coherentes:
RS256 -> RSA
ES256 -> EC P-256
También respeto use: sig y key_ops cuando están presentes.
Missing kid y duplicate kid
Casos que trato explícitamente:
- token tiene
kid, set no tiene match; - varios keys comparten
kid; - token no tiene
kidy hay varios candidatos; - match tiene
ktyoalgincompatible; - old key desapareció antes de expirar tokens emitidos.
Ambigüedad es error, no una invitación a probar claves al azar.
Verificación con jose y un remote JWKS
En Node.js, un patrón útil es fijar el issuer y audience desde configuración y dejar que la librería resuelva la clave dentro del JWKS de confianza:
import { createRemoteJWKSet, jwtVerify } from "jose";
const issuer = "https://login.example.com/";
const jwks = createRemoteJWKSet(
new URL("https://login.example.com/.well-known/jwks.json")
);
const { payload, protectedHeader } = await jwtVerify(token, jwks, {
issuer,
audience: "api://orders",
algorithms: ["RS256"],
});
Lo importante no es copiar este snippet literalmente, sino mantener tres límites claros: URL de confianza, algorithm allowlist y claims esperados.
Cache HTTP y refresh ante unknown kid
Un JWKS endpoint puede responder con headers como:
Cache-Control: public, max-age=300
ETag: "jwks-v42"
No fuerzo un fetch por request. Tampoco ignoro para siempre una nueva clave. Ante kid desconocido, un verifier robusto suele hacer un refresh controlado, respetar cache/rate limits y volver a intentar solo contra el mismo issuer/JWKS de confianza.
Errores que quiero distinguir:
unknown kid + JWKS stale -> refresh controlado
unknown kid + JWKS fresh -> token/issuer sospechoso
JWKS fetch 5xx -> dependencia temporal
JWKS timeout -> red/issuer
kid duplicado -> configuración ambigua
Qué observo en producción
Para no convertir una rotación normal en un incidente opaco, mido:
unknown_kidpor issuer;- refreshes de JWKS;
- latencia y errores HTTP del
jwks_uri; kidmás visto por ventana de tiempo;- verificaciones que siguen usando la old key durante overlap.
No registro private keys ni JWTs completos. Un pico de unknown_kid justo tras publicar una new key suele apuntar a cache; un pico sin rotación puede apuntar a tokens del issuer equivocado.
Cache y key rotation
No quiero descargar JWKS en cada request, pero tampoco cachearlo para siempre. Un patrón útil es cache + refresh controlado ante un kid desconocido, con rate limiting y sin cambiar la fuente confiable.
La guía de key rotation explica overlap y stale cache.
JWK Thumbprint no es kid
RFC 7638 define un thumbprint canónico calculado desde miembros concretos de la JWK. kid es un identificador elegido por el emisor. El thumbprint ayuda a comparar material de clave, pero no sustituye issuer/trust policy.
JWK Inspector calcula el thumbprint localmente.
Tests negativos que protegen la selección
kid conocido + RS256 + RSA -> verificar
kid desconocido -> refresh controlado o reject
kid duplicado -> reject por ambigüedad
kid match + kty incompatible -> reject
jku arbitraria en header -> no cambiar trust source
JWKS de otro issuer -> reject aunque la firma cuadre
Esta batería evita que un fallo de selección se convierta en “probar cualquier clave hasta que una funcione”.
Matriz JWK vs JWKS
| Pregunta | JWK | JWKS |
|---|---|---|
| ¿Una sola clave? | sí | no, set |
¿Tiene keys array? | no | sí |
| ¿Soporta overlap de rotación? | no por sí sola | sí |
¿Selección por kid? | opcional | frecuente |
¿Se publica por jwks_uri? | no directamente | normalmente |
Mi workflow seguro
- Obtengo expected issuer desde configuración confiable.
- Uso su metadata/JWKS conocido.
- Leo
algykiddel JWT solo como hints. - Selecciono el
kidcorrecto. - Filtro
kty,use,key_ops,algy curve cuando aplica. - Rechazo selección ausente o ambigua.
- Verifico la firma sobre el compact JWT original.
- Después valido
iss,aud,expy demás claims. - Ante unknown
kid, investigo cache/rotation sin cambiar el trust source. - Correlaciono unknown-kid y JWKS refresh con métricas para confirmar la causa.
JWKS no es solo «muchas JWK». Es una pieza del ciclo de descubrimiento, selección, cache y rotación de claves.