La différence de base : JWK décrit une clé cryptographique en JSON ; JWKS est un ensemble de JWKs. La différence devient opérationnelle avec kid, rotation, discovery, cache et sélection de clé.
J’utilise JWK Inspector pour une clé et JWKS Validator pour token + key set.
Une JWK représente une clé
{
"kty": "RSA",
"kid": "prod-2026-09",
"use": "sig",
"alg": "RS256",
"n": "...",
"e": "AQAB"
}
Je regarde kty, kid, use, key_ops, alg, puis les paramètres propres au type de clé.
kid est un identifiant de sélection, pas une preuve de confiance.
Un JWKS contient plusieurs JWKs
{
"keys": [
{ "kty": "RSA", "kid": "old", "alg": "RS256", "use": "sig", "n": "...", "e": "AQAB" },
{ "kty": "RSA", "kid": "new", "alg": "RS256", "use": "sig", "n": "...", "e": "AQAB" }
]
}
Le chevauchement old/new est normal pendant une rotation.
Relier kid au JWKS
JWT kid
↓
chercher dans JWKS
↓
filtrer kty/use/key_ops/alg
↓
vérifier la signature
Sans match, je vérifie freshness du JWKS et issuer attendu au lieu de prendre la première clé.
jwks_uri doit appartenir à une chaîne de confiance
expected issuer
↓
metadata/discovery de confiance
↓
jwks_uri
↓
JWKS
↓
matching kid
↓
signature verification
Je ne fais pas confiance automatiquement à une jku fournie par un token non vérifié.
Compatibilité alg / kty
Je m’attends à des associations cohérentes :
RS256 -> RSA
ES256 -> EC P-256
Je respecte aussi use: sig et key_ops lorsqu’ils existent.
Missing kid, duplicate kid et ambiguïté
Je traite explicitement :
kidabsent du set ;- plusieurs clés avec le même
kid; - token sans
kidavec plusieurs candidats ; kty/algincompatible ;- ancienne clé retirée trop tôt.
L’ambiguïté est une erreur, pas une raison de tester au hasard.
Vérification Node avec un JWKS distant de confiance
Avec jose, je peux garder l’issuer, l’audience et les algorithmes attendus dans la configuration serveur :
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"],
});
Le point important n’est pas la bibliothèque : la source du JWKS, l’algorithm allowlist et les claims attendus viennent d’une configuration de confiance.
Comprendre le cache HTTP du JWKS
Un endpoint peut publier des indications comme :
Cache-Control: public, max-age=300
ETag: "jwks-v42"
Je ne télécharge pas le JWKS à chaque requête et je ne le conserve pas indéfiniment. Pour un kid inconnu, un refresh contrôlé peut être pertinent, toujours contre la même source de confiance, avec rate limit pour éviter un fetch storm.
Je distingue :
unknown kid + JWKS stale -> refresh contrôlé
unknown kid + JWKS fresh -> token/issuer à examiner
JWKS HTTP 5xx -> dépendance temporaire
JWKS timeout -> réseau/issuer
kid dupliqué -> configuration ambiguë
Observer rotation et sélection en production
Sans stocker les JWT, je peux mesurer :
- nombre d’
unknown_kidpar issuer ; - refreshes du JWKS ;
- latence et erreurs du
jwks_uri; kidvus pendant la fenêtre de rotation ;- vérifications utilisant encore l’ancienne clé.
Un pic d’unknown_kid après publication d’une nouvelle clé pointe souvent vers cache/propagation. Le même pic sans rotation peut révéler des tokens d’un autre environnement.
Cache et rotation
Télécharger JWKS à chaque requête surcharge l’issuer ; le cacher indéfiniment ignore les nouvelles clés. Un compromis utile est cache + refresh contrôlé sur unknown kid, rate-limité et toujours lié à la même source de confiance.
Voir JWT key rotation pour le chevauchement et le stale cache.
JWK Thumbprint n’est pas kid
RFC 7638 définit un thumbprint calculé de manière canonique. kid est choisi par l’émetteur. Le thumbprint aide à comparer le matériel de clé, mais ne remplace pas la politique d’issuer.
JWK Inspector calcule le thumbprint localement.
Tests négatifs de sélection de clé
kid connu + RS256 + RSA -> vérifier
kid inconnu -> refresh contrôlé ou reject
kid dupliqué -> reject pour ambiguïté
kid match + kty incompatible -> reject
jku arbitraire dans le header -> ne pas changer de trust source
JWKS d’un autre issuer -> reject même si signature valide
Ces tests évitent une stratégie dangereuse consistant à essayer des clés jusqu’à ce qu’une signature passe.
Matrice JWK vs JWKS
| Question | JWK | JWKS |
|---|---|---|
| Une clé ? | oui | ensemble |
Tableau keys ? | non | oui |
| Rotation avec chevauchement ? | limitée | naturelle |
Sélection kid ? | optionnelle | fréquente |
Cible de jwks_uri ? | non | oui |
Mon workflow sûr
- Je définis l’expected issuer depuis la configuration.
- J’utilise sa metadata/JWKS de confiance.
- Je lis
algetkiduniquement comme indices. - Je sélectionne le
kidapproprié. - Je filtre
kty,use,key_ops,alget curve si nécessaire. - Je rejette absence ou ambiguïté de sélection.
- Je vérifie la signature du compact JWT original.
- Ensuite je valide
iss,aud,expet les autres claims. - Pour unknown
kid, j’analyse cache/rotation sans changer de trust source. - Je corrèle unknown-kid, refresh et erreurs HTTP pour confirmer la cause.
JWKS n’est donc pas seulement « plusieurs JWK » : c’est un élément du cycle de découverte, sélection, cache et rotation des clés.