Vérifier un JWT, ce n’est pas un contrôle mais six, et la signature n’en est qu’un. En bref : c’est le vérificateur, et non le jeton, qui décide comment le jeton est vérifié. Épinglez l’algorithme côté serveur, cherchez la clé parmi celles que vous considérez déjà comme fiables pour cet émetteur, exigez exp, comparez iss et aud à l’identique, vérifiez typ pour qu’un type de jeton ne puisse pas se faire passer pour un autre, tolérez au plus une minute de décalage d’horloge, et mettez le JWKS en cache avec un rafraîchissement soumis à une limite de requêtes pour les valeurs kid inconnues. La suite décrit ce qui se passe quand l’un de ces points manque, mesuré sur de vraies bibliothèques.
Toutes les sorties de cette page proviennent de jetons émis pour l’article avec une clé ES256 fraîchement générée, vérifiés par jose 6.2.13 et jsonwebtoken 9.0.3 (avec jwks-rsa 4.1.0) sous Node.js 24.22, PyJWT 2.15.1 sous Python 3.14.8, et golang-jwt 5.3.1 avec keyfunc 3.8.2 sous Go 1.27.3, chacun dans son image Docker officielle.
Ce que vérifie une bibliothèque quand vous ne lui passez que la clé
La plupart des bugs JWT ne sont pas des bugs de la bibliothèque. Ce sont des appels qui vérifient la signature et s’arrêtent là, parce que l’appelant n’a jamais indiqué à la bibliothèque quel émetteur, quelle audience ou quel algorithme attendre. Voici ce que chaque bibliothèque a fait de trois jetons, appelée avec une clé publique valide et sans aucune autre option (PyJWT refuse de s’exécuter sans algorithms, il a donc reçu ["ES256"]) :
| Jeton | jose jwtVerify | jsonwebtoken verify | PyJWT decode | golang-jwt Parse |
|---|---|---|---|---|
Signé par la bonne clé, mais iss et aud d’un autre service | accepté | accepté | rejeté : Invalid audience | accepté |
Signé par la bonne clé, sans claim exp | accepté | accepté | accepté | accepté |
alg: "none", signature vide | rejeté | rejeté | rejeté | rejeté |
Deux leçons. D’abord, les quatre bibliothèques refusent par défaut les jetons non signés : le fameux problème none se loge donc aujourd’hui surtout dans des vérificateurs écrits à la main. Ensuite, un jeton sans exp reste valide pour toujours dans les quatre, et trois sur quatre acceptent un jeton émis pour un autre service du moment que la même clé l’a signé. exp, nbf, iss et aud sont des claims facultatifs dans la RFC 7519 §4.1 ; les bibliothèques les vérifient quand ils sont présents et quand la configuration le demande, et le reste vous incombe.
Erreur 1 : laisser le jeton choisir l’algorithme
L’en-tête alg arrive à l’intérieur du jeton que vous n’avez pas encore vérifié. Un vérificateur qui le lit et s’en sert pour aiguiller la vérification laisse l’expéditeur choisir les règles. La RFC 8725 §3.1 (JWT Best Current Practices) est catégorique : les bibliothèques doivent permettre à l’appelant de spécifier les algorithmes pris en charge et ne doivent en utiliser aucun autre, et chaque clé doit être utilisée avec exactement un algorithme.
L’échec classique est la confusion HS/RS (ou HS/ES), décrite dans la RFC 8725 §2.1 et répertoriée pour jsonwebtoken sous CVE-2015-9235. Un service détient une seule clé publique et vérifie « selon ce que dit l’en-tête ». Quelqu’un calcule un HMAC sur un en-tête contenant "alg": "HS256", avec pour secret le texte PEM de la clé publique. La clé publique est publique : n’importe qui peut donc le faire. Voici un vérificateur écrit à la main qui a exactement cette forme :
import { createHmac, timingSafeEqual, verify } from "node:crypto";
import { readFileSync } from "node:fs";
// Anti-pattern: the token picks the algorithm, one key object serves both families.
function naiveVerify(token, publicKeyPem) {
const [h, p, s] = token.split(".");
const { alg } = JSON.parse(Buffer.from(h, "base64url"));
const input = Buffer.from(`${h}.${p}`);
const sig = Buffer.from(s, "base64url");
if (alg === "HS256") {
const mac = createHmac("sha256", publicKeyPem).update(input).digest();
return mac.length === sig.length && timingSafeEqual(mac, sig);
}
if (alg === "ES256") {
return verify("sha256", input, { key: publicKeyPem, dsaEncoding: "ieee-p1363" }, sig);
}
throw new Error(`unsupported alg ${alg}`);
}
const pem = readFileSync("out/public.pem", "utf8");
const tokens = JSON.parse(readFileSync("out/tokens.json", "utf8"));
console.log("ES256 token from the issuer:", naiveVerify(tokens.ok, pem));
console.log("HS256 token keyed with the public PEM:", naiveVerify(tokens["HS256 keyed with the public PEM"], pem));
ES256 token from the issuer: true
HS256 token keyed with the public PEM: true
Les deux appels renvoient true. Le second jeton a été fabriqué sans la clé privée. Notez que la comparaison en temps constant n’y change rien : le code prend soin du temps d’exécution et se trompe malgré tout sur la confiance.
Le correctif consiste en une liste d’autorisation fixe côté vérificateur, idéalement réduite à une seule valeur, et en des clés qui portent leur algorithme. Les quatre bibliothèques ont rejeté le jeton forgé lorsqu’on leur passait algorithms: ["ES256"], chacune avec son propre message : jose "alg" (Algorithm) Header Parameter value not allowed, jsonwebtoken invalid algorithm, PyJWT The specified alg value is not allowed, golang-jwt signing method HS256 is invalid. Elles disposent aussi de lignes de défense secondaires, ce qui explique que la confusion soit rare aujourd’hui dans le code des bibliothèques :
- jsonwebtoken 9.x (depuis la 9.0.0, décembre 2022) déduit les algorithmes autorisés du type de clé lorsque vous omettez
algorithms: une clé EC n’autorise queES256,ES384etES512. - PyJWT refuse d’utiliser une clé publique PEM ou un certificat comme secret HMAC : The specified key is an asymmetric key or x509 certificate and should not be used as an HMAC secret.
- golang-jwt vérifie le type Go de la clé : la méthode HMAC attend un
[]byte, si bien qu’un*ecdsa.PublicKeyéchoue avec l’erreur key is of invalid type. - jose vérifie que le type de clé correspond à l’algorithme. Même sans option
algorithms, la vérification du jeton forgé avec la clé publique EC a échoué avecKeyObject instances must be of type "secret" for the HS256 algorithm.
Aucun de ces garde-fous ne remplace la liste d’autorisation. Ils protègent le cas courant, pas un chargeur de clés maison qui passe des octets bruts à la bibliothèque.
Erreur 2 : accepter « none » dans un chemin de code maison
La RFC 7518 §3.6 enregistre "alg": "none" pour les jetons dont l’intégrité est protégée par un autre moyen. Dans le tableau ci-dessus, toutes les bibliothèques l’ont rejeté par défaut. Le risque se situe dans le code qui entoure la bibliothèque : un « mode debug » qui saute la vérification, une branche qui traite une signature vide comme « pas encore signé », ou une liste d’interdiction qui compare alg en tenant compte de la casse. La révision de ce BCP, draft-ietf-oauth-rfc8725bis (dans la file d’attente du RFC Editor en octobre 2026), signale précisément ce cas : une liste d’interdiction pour none qu’un en-tête indiquant noNE contourne. Une liste d’autorisation ne présente pas cette faille.
Erreur 3 : faire confiance à kid, jku, x5u ou à une jwk embarquée
kid sert à choisir une clé parmi plusieurs que vous considérez déjà comme fiables. Ce n’est pas un pointeur vers une clé fournie par le jeton. La RFC 8725 §3.10 demande de traiter les en-têtes reçus comme des entrées non fiables :
- Cherchez
kiddans une table de correspondance, pas dans une requête ni dans un chemin. Unkidqui atteint du SQL, du LDAP ou un nom de fichier est un point d’injection. Les bibliothèques JWKS de cet article se contentent de le comparer aux valeurskiddes clés qu’elles ont récupérées depuis l’URL que vous avez configurée, et c’est la bonne approche. - Ignorez
jkuetx5u. Ce sont deux URL contenues dans le jeton. Les récupérer laisse l’expéditeur choisir où votre serveur se connecte et quelles clés il utilise. Configurez plutôt l’URL du JWKS par émetteur ; si vous devez vraiment utiliser ces URL, la RFC 8725 suggère de les comparer à une liste d’autorisation. - Ne vérifiez jamais un jeton d’accès avec une
jwkou unx5cembarqués dans son propre en-tête. Un jeton signé avec une clé qu’il transporte prouve seulement que l’expéditeur possède une clé, quelle qu’elle soit. Les protocoles qui utilisent bel et bien une clé embarquée, comme les preuves DPoP, la lient à autre chose (le claimcnfdu jeton d’accès). Consultez DPoP expliqué. - Traitez explicitement l’absence de
kid. jwks-rsa refuse de deviner quand le jeu contient plusieurs clés et que le jeton n’en désigne aucune (No KID specified and JWKS endpoint returned more than 1 key) ; une recherche écrite à la main qui renvoie « la première clé » choisira sans bruit la mauvaise après une rotation.
Erreur 4 : ne pas vérifier aud et iss, ou les vérifier de façon trop permissive
Un fournisseur d’identité signe généralement les jetons de nombreux clients et API avec la même clé. Si votre API ne vérifie pas aud, un jeton émis pour n’importe quelle autre application du même fournisseur fonctionne sur la vôtre : c’est la première ligne du tableau. La RFC 8725 §3.9 exige que le destinataire rejette un JWT dont l’audience est absente ou n’est pas la sienne, et la RFC 9068 §4 exige une correspondance exacte sur iss pour les jetons d’accès OAuth.
En pratique, trois détails comptent :
- Comparaison exacte des chaînes.
audetisssont des chaînes sensibles à la casse, ethttps://id.example.comn’est pashttps://id.example.com/. Copiez l’émetteur depuis le document de découverte du fournisseur, pas depuis un jeton que vous avez sous la main. audpeut être un tableau. Les bibliothèques acceptent le jeton si l’un des éléments correspond. C’est conforme à la RFC 7519, mais cela signifie qu’un jeton émis pour["https://api.example.com", "https://admin.example.com"]fonctionne sur les deux API. Si votre API ne doit accepter que des jetons émis pour elle seule, PyJWT proposeoptions={"strict_aud": True}; ailleurs, vérifiez vous-même la longueur du tableau.- Les clés doivent appartenir à l’émetteur. RFC 8725 §3.8 : si un jeton porte un
iss, la clé qui l’a vérifié doit être celle de cet émetteur. Avec plusieurs émetteurs, gardez un client JWKS par émetteur et choisissez-le d’après la configuration, jamais d’après le seulissnon vérifié du jeton. Chez les fournisseurs multilocataires qui partagent un même jeu de clés entre locataires (Microsoft Entra ID est le cas le plus courant), le contrôle deissest la seule chose qui sépare votre locataire de tous les autres ; l’article Validation du jeton d’ID OIDC traite ce point en détail.
Erreur 5 : des jetons sans exp et une marge de tolérance trop généreuse
Une bibliothèque vérifie exp lorsqu’il est présent. Lorsqu’il est absent, les quatre ont accepté le jeton. Si votre émetteur oublie un jour ce claim, ou si quelqu’un trouve un chemin de code qui émet des jetons sans lui, ces jetons n’expirent jamais. Rendez exp obligatoire : requiredClaims dans jose, options={"require": [...]} dans PyJWT, jwt.WithExpirationRequired() dans golang-jwt, et une vérification explicite dans jsonwebtoken, qui n’a pas d’option équivalente.
Le décalage d’horloge existe, mais il est faible. La RFC 7519 §4.1.4 autorise « une petite marge de tolérance, généralement de quelques minutes au plus ». Trente à soixante secondes suffisent pour des serveurs synchronisés par NTP. Une marge d’une heure, souvent ajoutée pour faire passer un test instable, allonge d’une heure la durée de vie de chaque jeton, y compris des jetons que vous vouliez voir expirer. L’option s’appelle clockTolerance dans jose et jsonwebtoken (en secondes), leeway dans PyJWT et jwt.WithLeeway dans golang-jwt. La même marge s’applique à nbf.
Lorsqu’une vérification échoue avec une erreur d’expiration ou de jeton pas encore valide sur un jeton qui « devrait » être valide, comparez l’horloge du serveur avec celle de l’émetteur avant de toucher à la marge de tolérance. Une machine virtuelle ou un conteneur qui reprend après une mise en veille avec une horloge en retard produit exactement les erreurs jwt not active et "nbf" claim timestamp check failed de la sortie ci-dessous.
Erreur 6 : accepter le mauvais type de jeton
Un fournisseur OpenID Connect signe les jetons d’ID et les jetons d’accès avec la même clé, souvent avec le même iss et parfois avec des aud qui se recoupent. Une API qui ne vérifie que la signature, l’émetteur et l’audience peut accepter un jeton d’ID comme jeton d’accès. La RFC 8725 §3.11 recommande un typage explicite, et la RFC 9068 le rend obligatoire pour les jetons d’accès JWT : l’en-tête typ doit valoir at+jwt (ou application/at+jwt), et les serveurs de ressources doivent rejeter toute autre valeur. Si votre fournisseur émet des jetons RFC 9068, vérifiez typ ; sinon, vérifiez un claim que seuls les jetons d’accès portent, comme scope ou client_id.
Erreur 7 : appeler decode là où il fallait verify
Chaque bibliothèque a une fonction qui analyse un jeton sans le vérifier : jwt.decode() dans jsonwebtoken, decodeJwt() dans jose, jwt.decode(..., options={"verify_signature": False}) dans PyJWT, ParseUnverified dans golang-jwt. Elles servent au routage (lire iss pour choisir un client JWKS) et au débogage. Une revue de code devrait considérer comme suspecte toute utilisation de ces fonctions sur le chemin d’une requête, et comme un bug toute décision fondée sur leur résultat, hormis le choix du vérificateur à appeler ensuite.
Erreur 8 : récupérer le JWKS à chaque requête, ou jamais
Récupérer le jeu de clés à chaque requête ajoute un aller-retour réseau à chaque appel et fait dépendre votre disponibilité de celle du fournisseur d’identité. Le mettre en cache pour toujours signifie que la première rotation des clés fait échouer toutes les requêtes jusqu’au prochain redémarrage. Le bon comportement, que mettent en œuvre les clients JWKS ci-dessous, combine un cache à durée de vie limitée et un rafraîchissement lorsqu’un jeton désigne un kid inconnu, avec une limite de requêtes sur ce rafraîchissement, pour qu’un flot de jetons aux valeurs kid aléatoires ne puisse pas transformer votre API en générateur de charge contre le fournisseur.
| Client | Durée de vie du cache (par défaut) | kid inconnu | Délai d’attente de la récupération |
|---|---|---|---|
jose createRemoteJWKSet | 10 minutes (cacheMaxAge) | nouvelle récupération, au plus une fois toutes les 30 s (cooldownDuration) | 5 s |
| jwks-rsa 4.1.0 | 10 minutes par clé, 5 clés | nouvelle récupération à chaque clé introuvable ; rateLimit désactivé sauf activation explicite (alors 10 par minute) | 30 s |
PyJWT PyJWKClient | 300 s (lifespan) | nouvelle récupération, au plus une fois toutes les 30 s (cooldown_duration) | 30 s |
keyfunc v3 NewDefault | rafraîchissement en arrière-plan toutes les heures | nouvelle récupération, au plus une fois toutes les 5 minutes | 1 minute |
Ces valeurs sont les valeurs par défaut des versions indiquées en début d’article, relevées dans leur code source. La différence est mesurable : 20 jetons aux valeurs kid aléatoires ont provoqué 20 récupérations du jeu de clés avec les réglages par défaut de jwks-rsa, 10 avec rateLimit: true (les dix autres ont échoué immédiatement avec JwksRateLimitError), et une seule récupération avec jose. Créez le client une fois par émetteur et réutilisez-le : un client JWKS construit dans le gestionnaire de requêtes a un cache vide à chaque fois. La rotation elle-même, côté émetteur, est traitée dans Rotation des clés JWKS sans interruption de service.
Un vérificateur correct, avec quatre bibliothèques
Les quatre vérificateurs ci-dessous font la même chose : ES256 uniquement, clés issues de l’URL JWKS de l’émetteur, iss et aud exacts, exp obligatoire, typ: at+jwt, 30 secondes de marge de tolérance. Chacun a été exécuté sur les mêmes huit jetons.
import { createRemoteJWKSet, jwtVerify } from "jose";
// Created once per issuer and reused: caches the key set, re-fetches it
// when a token names an unknown kid (at most once per cooldown).
const JWKS = createRemoteJWKSet(new URL(process.env.JWKS_URL), {
cacheMaxAge: 600_000, // re-fetch at least every 10 minutes
cooldownDuration: 30_000, // at most one kid-miss re-fetch per 30 s
});
export async function verifyAccessToken(token) {
const { payload } = await jwtVerify(token, JWKS, {
algorithms: ["ES256"], // fixed by the verifier, never taken from the token
issuer: "https://id.example.com",
audience: "https://api.example.com",
typ: "at+jwt", // RFC 9068 access token, not an ID token
requiredClaims: ["exp", "sub"], // exp is only checked when present
clockTolerance: 30, // seconds
});
return payload;
}
import jwt from "jsonwebtoken";
import jwksClient from "jwks-rsa";
const client = jwksClient({
jwksUri: process.env.JWKS_URL,
cache: true, // per-kid cache, 10 minutes by default
rateLimit: true, // off by default; caps JWKS fetches at 10 per minute
});
const getKey = (header, callback) =>
client.getSigningKey(header.kid, (err, key) => callback(err, key?.getPublicKey()));
export function verifyAccessToken(token) {
const options = {
algorithms: ["ES256"],
issuer: "https://id.example.com",
audience: "https://api.example.com",
clockTolerance: 30,
complete: true, // also return the header, to check typ
};
return new Promise((resolve, reject) =>
jwt.verify(token, getKey, options, (err, decoded) => {
if (err) return reject(err);
if (decoded.header.typ !== "at+jwt") return reject(new Error("unexpected typ"));
if (typeof decoded.payload.exp !== "number") return reject(new Error("exp is required"));
resolve(decoded.payload);
}),
);
}
import os
import jwt
# One client per issuer, reused across requests: caches the key set and
# re-fetches on an unknown kid (at most once per cooldown).
jwks = jwt.PyJWKClient(os.environ["JWKS_URL"], lifespan=600)
def verify_access_token(token: str) -> dict:
signing_key = jwks.get_signing_key_from_jwt(token)
decoded = jwt.decode_complete(
token,
signing_key,
algorithms=["ES256"], # required argument in PyJWT 2.x
issuer="https://id.example.com",
audience="https://api.example.com",
leeway=30,
options={"require": ["exp", "iss", "aud", "sub"]},
)
if decoded["header"].get("typ") != "at+jwt":
raise jwt.InvalidTokenError("unexpected typ")
return decoded["payload"]
package main
import (
"context"
"errors"
"time"
"github.com/MicahParks/keyfunc/v3"
"github.com/golang-jwt/jwt/v5"
)
type Verifier struct {
jwks keyfunc.Keyfunc
parser *jwt.Parser
}
func NewVerifier(ctx context.Context, jwksURL string) (*Verifier, error) {
// Background refresh every hour; an unknown kid triggers at most
// one extra fetch per 5 minutes (keyfunc defaults).
jwks, err := keyfunc.NewDefaultCtx(ctx, []string{jwksURL})
if err != nil {
return nil, err
}
parser := jwt.NewParser(
jwt.WithValidMethods([]string{"ES256"}),
jwt.WithIssuer("https://id.example.com"),
jwt.WithAudience("https://api.example.com"),
jwt.WithExpirationRequired(),
jwt.WithLeeway(30*time.Second),
)
return &Verifier{jwks: jwks, parser: parser}, nil
}
func (v *Verifier) VerifyAccessToken(raw string) (jwt.MapClaims, error) {
claims := jwt.MapClaims{}
token, err := v.parser.ParseWithClaims(raw, claims, v.jwks.Keyfunc)
if err != nil {
return nil, err
}
if token.Header["typ"] != "at+jwt" {
return nil, errors.New("unexpected typ")
}
return claims, nil
}
Les jetons de test : un jeton valide, et sept qui enfreignent chacun exactement une règle. Le JWKS était servi en HTTP depuis l’intérieur du conteneur, et chaque bibliothèque l’a récupéré via son client JWKS.
--- jose
ok ACCEPTED sub=user-42
aud for another API ERR_JWT_CLAIM_VALIDATION_FAILED: unexpected "aud" claim value
no exp ERR_JWT_CLAIM_VALIDATION_FAILED: missing required "exp" claim
expired 1 h ago ERR_JWT_EXPIRED: "exp" claim timestamp check failed
nbf in 1 h ERR_JWT_CLAIM_VALIDATION_FAILED: "nbf" claim timestamp check failed
typ JWT (an ID token) ERR_JWT_CLAIM_VALIDATION_FAILED: unexpected "typ" JWT header value
unknown kid ERR_JWKS_NO_MATCHING_KEY: no applicable key found in the JSON Web Key Set
HS256 keyed with the public PEM ERR_JOSE_ALG_NOT_ALLOWED: "alg" (Algorithm) Header Parameter value not allowed
--- jsonwebtoken
ok ACCEPTED sub=user-42
aud for another API JsonWebTokenError: jwt audience invalid. expected: https://api.example.com
no exp Error: exp is required
expired 1 h ago TokenExpiredError: jwt expired
nbf in 1 h NotBeforeError: jwt not active
typ JWT (an ID token) Error: unexpected typ
unknown kid JsonWebTokenError: error in secret or public key callback: Unable to find a signing key that matches '2026-09-z'
HS256 keyed with the public PEM JsonWebTokenError: invalid algorithm
--- PyJWT
ok ACCEPTED sub=user-42
aud for another API InvalidAudienceError: Audience doesn't match
no exp MissingRequiredClaimError: Token is missing the "exp" claim
expired 1 h ago ExpiredSignatureError: Signature has expired
nbf in 1 h ImmatureSignatureError: The token is not yet valid (nbf)
typ JWT (an ID token) InvalidTokenError: unexpected typ
unknown kid PyJWKClientError: Unable to find a signing key that matches: "2026-09-z"
HS256 keyed with the public PEM InvalidAlgorithmError: The specified alg value is not allowed
--- golang-jwt v5 + keyfunc v3
ok ACCEPTED sub=user-42
aud for another API token has invalid claims: token has invalid audience
no exp token has invalid claims: token is missing required claim: exp claim is required
expired 1 h ago token has invalid claims: token is expired
nbf in 1 h token has invalid claims: token is not valid yet
typ JWT (an ID token) unexpected typ
unknown kid token is unverifiable: error while executing keyfunc: key not found "2026-09-z"
failed keyfunc: could not read JWK from storage
HS256 keyed with the public PEM token signature is invalid: signing method HS256 is invalid
La plupart des messages des bibliothèques ci-dessus ont leur propre page dans le catalogue des erreurs, avec leurs causes et leurs correctifs, par exemple unexpected "aud" claim value, jwt not active et no applicable key found in the JSON Web Key Set.
Liste de contrôle
- Une configuration de vérificateur par émetteur, construite au démarrage : URL du JWKS, émetteur, audience, liste d’algorithmes.
- Algorithmes : une liste d’autorisation fixe, en général une seule valeur. Jamais déduite du jeton, ne contenant jamais
none. - Clés : uniquement depuis le JWKS ou le fichier de clés configuré ;
kidutilisé comme clé de recherche dans une table de correspondance ;jku,x5uetjwkembarquée ignorés pour les jetons d’accès. - Claims :
expobligatoire ;issetaudcomparés à l’identique ;nbfrespecté ; marge de tolérance de 30 à 60 secondes. - Type de jeton :
typvérifié (at+jwtpour les jetons d’accès RFC 9068) ou un autre claim qui distingue les jetons d’accès des jetons d’ID. - Cache JWKS : un seul client, réutilisé ; rafraîchissement périodique ; rafraîchissement sur
kidinconnu soumis à une limite de requêtes. - Aucun
decodesans vérification sur le chemin d’une requête. - Tests : un jeton valide et un jeton par règle ci-dessus, chacun de ces derniers devant échouer. Les huit jetons de cet article constituent un premier jeu.
Sources
- RFC 7519 : JSON Web Token, §4.1 (claims enregistrés), §7.2 (validation)
- RFC 7518 : JSON Web Algorithms, §3.6 (
none) - RFC 8725 : JSON Web Token Best Current Practices, §2.1, §3.1, §3.8 à §3.11
- draft-ietf-oauth-rfc8725bis, la mise à jour de la RFC 8725
- RFC 9068 : JWT Profile for OAuth 2.0 Access Tokens, §2.1 et §4
- Documentation de jose :
jwtVerify,createRemoteJWKSet - README de jsonwebtoken et de jwks-rsa
- Documentation de PyJWT : utilisation,
PyJWKClient - golang-jwt/jwt v5 et keyfunc v3