Les JSON Web Tokens (JWT) sont devenus le standard de facto pour l'authentification et l'autorisation des API modernes. OAuth 2.0, OIDC, sessions mobile, service-to-service : JWT est partout. Leur apparente simplicité cache une surface d'attaque réelle, régulièrement exploitée : alg: none, confusion HMAC/RSA, kid injection, secrets faibles, révocation impossible. Ce guide détaille la structure, les 12 vulnérabilités classiques, les bonnes pratiques 2026, les pièges de stockage et les alternatives (PASETO, opaque tokens) - pour développeurs back-end, AppSec engineers et pentesters.
1. Structure d'un JWT
1.1 Anatomie
Un JWT est composé de trois parties séparées par des points (.) :
<header_base64>.<payload_base64>.<signature_base64>
Exemple concret :
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtleS0yMDI2MDEifQ.
eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VyXzEyMzQ1Iiwi
YXVkIjoiYXBpLmV4YW1wbGUuY29tIiwiZXhwIjoxNzIwMDAwMDAwLCJpYXQiOjE3MTk5OTY0
MDAsInNjb3BlIjoicmVhZDpwcm9maWxlIHdyaXRlOm9yZGVycyJ9.
Adg1m2b3z...signature...xyz
1.2 Header décodé
{
"alg": "RS256",
"typ": "JWT",
"kid": "key-202601"
}alg: algorithme de signature.typ: type du token (JWT par défaut).kid: identifiant de la clé publique (pour rotation).
1.3 Payload décodé (claims)
{
"iss": "https://auth.example.com",
"sub": "user_12345",
"aud": "api.example.com",
"exp": 1720000000,
"iat": 1719996400,
"scope": "read:profile write:orders"
}Claims standards (RFC 7519) :
iss(Issuer) : qui a émis le token.sub(Subject) : qui est identifié.aud(Audience) : destinataire prévu.exp(Expiration) : Unix timestamp d'expiration.nbf(Not Before) : timestamp avant lequel le token n'est pas valide.iat(Issued At) : timestamp d'émission.jti(JWT ID) : identifiant unique (utile pour révocation).
Claims custom : tout champ ajouté (scope, role, org_id, etc.).
1.4 Signature
La signature protège l'intégrité du header et du payload. Formule :
signature = sign(base64(header) + "." + base64(payload), secret_or_private_key)
Vérifiée côté serveur via :
- Clé partagée (secret) pour HMAC.
- Clé publique pour RSA/ECDSA/EdDSA.
Un JWT sans signature valide ne doit jamais être accepté. Toute l'intégrité du modèle JWT repose sur cette règle.
1.5 JWT ≠ chiffré par défaut
Un JWT classique est signé (intégrité), pas chiffré (confidentialité). Le payload est lisible par tout le monde en décodant la base64. Ne jamais mettre de données sensibles en clair dans le payload.
Pour le chiffrement, il existe JWE (JSON Web Encryption), variante moins utilisée qui encapsule un payload chiffré.
2. Algorithmes de signature
2.1 Familles
| Algorithme | Famille | Clés | Usage recommandé |
|---|---|---|---|
| HS256 | HMAC + SHA-256 | Secret partagé | M2M interne, petites apps mono-tenant |
| HS384/HS512 | HMAC + SHA | Secret partagé | Idem avec hash plus fort |
| RS256 | RSA + SHA-256 | Paire clé privée/publique (2048+ bits) | Standard pour OIDC et multi-consumers |
| RS384/RS512 | RSA + SHA | Paire | Idem |
| ES256 | ECDSA P-256 + SHA-256 | Paire | Plus compact que RSA, performant |
| ES384/ES512 | ECDSA P-384/P-521 | Paire | Sécurité renforcée |
| EdDSA (Ed25519) | Edwards curves | Paire | Le plus moderne, sécurité + performance |
| PS256/PS384/PS512 | RSA-PSS | Paire | RSA avec padding moderne |
| none | Aucun | Rien | JAMAIS EN PRODUCTION |
2.2 Recommandations 2026
- Nouveau projet, multi-consumers : EdDSA (Ed25519) - meilleure sécurité, meilleure performance, clés plus compactes.
- Écosystème traditionnel : RS256 reste acceptable et universellement supporté.
- Service interne mono-tenant : HS256 avec secret long (256 bits minimum) peut suffire.
- ES256 excellent compromis taille/perf.
2.3 Ce qu'il faut bannir
- none : jamais accepté côté serveur.
- HS256 avec clé RSA : vulnérabilité de confusion (voir §5.2).
- HS256 avec secret faible : bruteforce possible.
- Algorithmes dépréciés : SHA-1 variants, MD5 variants.
3. Cas d'usage JWT
3.1 Authentication tokens (ID Token OIDC)
Token prouvant qui est l'utilisateur. Émis par l'IdP après authentification. Consommé par l'application pour créer la session locale.
Ne pas envoyer en continu aux APIs - c'est le rôle de l'access token.
3.2 Authorization tokens (Access Token OAuth 2.0)
Token envoyé à chaque requête API dans le header Authorization: Bearer <token>. Prouve que le client est autorisé à appeler l'API au nom de l'utilisateur.
Durée de vie courte : 15 minutes à 1 heure maximum.
3.3 Refresh tokens
Token de longue durée (quelques jours à quelques mois) qui permet d'obtenir un nouveau access token sans ré-authentifier l'utilisateur. Émis et stocké côté client en sécurité.
Souvent opaque plutôt que JWT, car la révocation doit être instantanée.
3.4 M2M (machine-to-machine)
OAuth 2.0 Client Credentials Grant : un service obtient un access token via client_id + client_secret (ou mTLS). Utilisé pour communications inter-services.
3.5 Webhooks signés
Certains fournisseurs signent leurs webhooks avec un JWT pour permettre au destinataire de vérifier l'origine et l'intégrité.
3.6 Short-lived signed URLs
URLs pré-signées (S3, Cloud Storage) utilisent souvent des JWT pour prouver l'autorisation sur une durée limitée.
4. Claims - validation obligatoire
Un JWT valide techniquement (signature OK) n'est pas forcément utilisable. La validation des claims est un second filtre obligatoire.
4.1 Les 5 validations non-négociables
- Signature valide avec l'algorithme attendu côté serveur.
expnon dépassé (token pas expiré).nbfatteint si présent (pas utilisé avant sa validité).isscorrespond à l'émetteur attendu (pas un JWT d'un autre IdP).audcontient l'audience attendue (pas un JWT destiné à une autre API).
4.2 Validations contextuelles
subexiste et correspond à un user valide dans votre système.scopecontient les scopes nécessaires pour l'endpoint.iatpas trop ancien (anti-replay renforcé).- Claims custom selon les besoins métier (org_id, tenant_id, etc.).
4.3 Exemple de validation Python
import jwt
from jwt import PyJWKClient
JWKS_URL = "https://auth.example.com/.well-known/jwks.json"
jwks_client = PyJWKClient(JWKS_URL)
def validate_token(token: str) -> dict:
# Récupération de la clé publique par kid
signing_key = jwks_client.get_signing_key_from_jwt(token)
# Décodage + validation de toutes les checks obligatoires
payload = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"], # ne pas accepter autre chose
audience="api.example.com", # valide aud
issuer="https://auth.example.com", # valide iss
options={
"verify_signature": True,
"verify_exp": True,
"verify_nbf": True,
"verify_iat": True,
"require": ["exp", "iss", "aud", "sub"],
}
)
return payloadPoint critique : algorithms=["RS256"] est une liste fixe côté serveur. Ne jamais lire l'algorithme du header pour choisir comment vérifier (voir §5.1).
5. Les 12 vulnérabilités JWT classiques
5.1 alg: none accepté
CVE-type : bibliothèques JWT anciennes qui honorent un alg: none dans le header, permettant à un attaquant de forger des tokens sans signature.
Attaque :
{"alg": "none", "typ": "JWT"}.{"sub": "admin", "role": "admin"}.
Sans signature, le token est accepté. L'attaquant devient admin.
Mitigation : lister explicitement les algorithmes acceptés côté serveur (algorithms=["RS256"]), jamais de None ou vide.
5.2 Confusion HMAC / RSA
CVE-type : bibliothèques qui utilisent verify(token, key) où key est la clé publique RSA (publique = connue). Si l'attaquant change alg de RS256 à HS256, la bibliothèque utilise la clé publique comme secret HMAC.
Attaque :
- Récupérer la clé publique (jwks.json).
- Forger un JWT avec
alg: HS256signé via HMAC avec cette clé publique comme secret. - Le serveur vérifie en HMAC avec la même clé publique = signature valide.
Mitigation : fixer l'algorithme accepté côté serveur. algorithms=["RS256"] n'acceptera jamais un HS256.
5.3 Secrets HMAC faibles
Un secret HS256 court ou prédictible (ex. secret, mysecretkey) peut être bruteforcé hors-ligne.
Outils publics : hashcat mode 16500 (JWT). Un secret 8 caractères alphanumériques se casse en heures sur un GPU moderne.
Mitigation : secret HS256 d'au moins 256 bits d'entropie aléatoire (32 octets = 64 hex). Généré via secrets.token_bytes(32) (Python) ou équivalent.
5.4 Pas de validation exp
Token sans expiration vérifiée = token utilisable à vie. Cas classique où la bibliothèque par défaut ne valide pas exp, ou où exp est manquant dans le payload.
Mitigation : exiger exp obligatoire, valider côté serveur. Refuser les tokens sans exp.
5.5 Pas de validation aud
Token émis pour une API A utilisé sur une API B. Si B ne valide pas aud, il accepte.
Cas historique : tokens d'OIDC utilisés pour authentifier vers des APIs qui n'étaient pas l'audience prévue.
Mitigation : chaque API valide aud strictement contre sa propre identité.
5.6 Pas de validation iss
Token d'un IdP attaquant accepté par votre application parce que vous ne vérifiez pas qu'il vient de votre IdP.
Mitigation : validation stricte de iss contre une liste d'émetteurs de confiance.
5.7 kid injection / path traversal
Le header contient un champ kid (key ID). Certaines implémentations l'utilisent pour charger la clé depuis un fichier ou une URL.
Attaque :
{
"alg": "HS256",
"kid": "../../../etc/passwd"
}Si le serveur lit le fichier pointé par kid comme secret HMAC, l'attaquant peut pointer vers un fichier dont il connaît le contenu (ex. un fichier vide, /dev/null) et forger un token avec le même contenu comme secret.
Ou encore : kid pointant vers une URL contrôlée par l'attaquant pour servir une clé arbitraire.
Mitigation : kid doit être validé contre une allowlist côté serveur, jamais utilisé comme path/URL directement.
5.8 JKU / X5U header injection
Headers jku (JWK Set URL) et x5u (X.509 URL) permettent au JWT de spécifier où trouver la clé de vérification. Bibliothèques naïves suivent l'URL, permettant à l'attaquant de pointer vers son propre JWKS.
Mitigation : ignorer ces headers côté serveur. Utiliser une source de vérité connue (JWKS URL configurée).
5.9 Algorithm confusion sur les courbes
Similaire à HMAC/RSA mais avec les variantes ECDSA : ES256 vs ES384, ou ES256 vs ES256K (Bitcoin curve) qui partagent des structures similaires avec vulnérabilités historiques.
Mitigation : algorithmes fixés strictement.
5.10 Replay de refresh tokens volés
Un refresh token volé peut être utilisé à vie si :
- Le refresh token n'expire pas ou expire trop tard.
- Pas de rotation : le refresh token reste valide même après émission d'un nouveau.
- Pas de détection d'anomalie (même refresh token utilisé depuis deux IPs différentes).
Mitigation :
- Refresh token rotation : chaque usage d'un refresh token émet un nouveau refresh token, l'ancien devient invalide.
- Detection de replay : si un refresh token déjà rotaté est réutilisé, révoquer toute la chaîne (compromission détectée).
- Lifetime borné : rotation max 90 jours, après quoi re-authentification forcée.
5.11 Pas de révocation
Un JWT est auto-contenu : sa validité ne dépend que de sa signature et de ses claims. Pas de DB consultée à la validation. Conséquence : impossible de révoquer avant expiration.
Conséquence pratique : un JWT volé reste utilisable jusqu'à expiration. Si exp = 24h, compromission exploitable 24h.
Mitigations :
- Access tokens courts : 15 minutes max. Compromission limitée.
- Blacklist de jti pour révocation d'urgence : serveur consulte une liste de jti bannis à la validation. Requête DB/cache, perd le bénéfice "stateless" mais indispensable pour scénarios critiques.
- Session ID en JWT : le JWT contient un session_id, le serveur vérifie que la session est active en DB. Hybride entre JWT et session classique.
5.12 Stockage côté client mal choisi
Où stocker le JWT côté client ?
| Emplacement | Risques |
|---|---|
| localStorage | Accessible par tout JS de la page → XSS vole le token |
| sessionStorage | Idem localStorage, persistance plus courte |
| Cookie sans HttpOnly | Accessible par JS → XSS le vole |
| Cookie HttpOnly + Secure + SameSite | Meilleur : pas accessible par JS, protégé en transit, protégé CSRF |
| Variable mémoire JS | OK si durée très courte et pas de reload |
| IndexedDB | Accessible par JS → XSS |
| Mobile Keychain / Keystore | Sécurisé sur mobile natif |
Pour une web app : cookie HttpOnly Secure SameSite=Strict est le meilleur compromis. Le cookie protège contre XSS. Combiné avec CSRF tokens ou SameSite, protège contre CSRF.
localStorage est une mauvaise pratique sécurité malgré sa popularité. Toute XSS vole le token.
6. Bonnes pratiques 2026
6.1 Access token courte durée + refresh token
- Access token : 15 minutes.
- Refresh token : quelques jours (7 à 30), avec rotation.
- Re-authentification forcée après 90 jours maximum.
6.2 Algorithmes fixés côté serveur
algorithms=["RS256"] # ou ["EdDSA"] pour nouveaux projetsJamais d'algorithme lu du header pour choisir la vérification.
6.3 Claims minimal dans le payload
Ne pas stocker en clair :
- Mots de passe, hashes.
- Numéros de carte, SSN.
- Données personnelles sensibles.
- Secrets ou tokens tiers.
Le JWT est lisible par tout le monde.
6.4 JWKS avec rotation
- Serveur émetteur publie ses clés publiques dans un JWKS URL.
- Chaque clé a un
kidunique. - Rotation périodique (trimestrielle typiquement).
- Anciennes clés conservées jusqu'à expiration des derniers tokens signés avec.
Côté validateur : caching du JWKS avec TTL court (quelques minutes), re-fetch à la rotation détectée via changement de kid.
6.5 Validation stricte de tous les claims obligatoires
exp, iss, aud, sub = strict par défaut. nbf si utilisé.
6.6 Détection d'anomalies
- Même JWT utilisé depuis deux IPs géographiquement éloignées en quelques minutes.
- Volume d'API anormalement élevé sur un token.
- Refresh token réutilisé après rotation = alarme rouge.
6.7 mTLS sur les APIs sensibles
Pour les communications inter-services critiques, combiner JWT + mTLS : le JWT prouve l'identité du sujet, le certificat client prouve l'identité du service appelant.
6.8 Short-lived et rotation pour M2M
Pour OAuth Client Credentials, préférer :
- Tokens courte durée émis à la demande.
- Client secret rotaté régulièrement.
- Ou mieux : mTLS client authentication à la place du client_secret.
6.9 Observabilité
Logs structurés de chaque validation JWT :
- Succès / échec.
- Cause d'échec (signature invalide, expiré, audience incorrecte, kid inconnu).
- iss, sub, jti (pas le token complet).
Export vers SIEM, alertes sur patterns anormaux.
7. Bibliothèques recommandées 2026
| Langage | Bibliothèque | Notes |
|---|---|---|
| Python | pyjwt, python-jose, authlib | PyJWT stable, Authlib plus complet |
| Node.js | jose, jsonwebtoken | jose plus moderne et sécurisé |
| Go | golang-jwt/jwt/v5, lestrrat-go/jwx | jwx riche, v5 stable |
| Java | java-jwt (Auth0), nimbus-jose-jwt | Nimbus complet |
| Ruby | ruby-jwt | Maintenu |
| .NET | System.IdentityModel.Tokens.Jwt, Microsoft.AspNetCore.Authentication.JwtBearer | Natif Microsoft |
| Rust | jsonwebtoken, jwt-simple | Deux options solides |
Critères de choix :
- Maintenu activement (dernière release moins d'un an).
- CVE historique faible.
- Support des algorithmes modernes (EdDSA).
- Validation stricte par défaut.
7.1 Bibliothèques à éviter
- node-jsonwebtoken < 9.0.0 : nombreuses CVE historiques, privilégier
josepour nouveaux projets. - jwt-decode utilisé seul pour valider : ne valide pas la signature malgré son nom trompeur. Décode uniquement la payload. Bug fréquent chez les débutants.
8. Alternatives à JWT
8.1 Opaque tokens + session store
Token = string aléatoire. Serveur stocke les claims en DB/cache (Redis). Validation = lookup du token.
Avantages :
- Révocation instantanée (suppression du store).
- Taille petite (token = 64 chars, pas 500-2000 comme JWT).
- Pas de risque cryptographique lié au choix d'algorithme.
- Audit facile par DB lookups.
Inconvénients :
- Lookup DB à chaque requête (mitigable via cache Redis, latence sous la milliseconde).
- Pas stateless (dépendance infra validateur-store).
Pour APIs à fort trafic et exigences de révocation : opaque tokens restent excellents.
8.2 PASETO (Platform-Agnostic Security Tokens)
Alternative moderne à JWT créée en 2018 par Scott Arciszewski. Principes :
- Algorithmes versionnés (v1, v2, v3, v4) au lieu de négociables.
- Pas de choix d'algorithme côté attaquant : un PASETO v4 est toujours Ed25519.
- Distinction nette entre local (symétrique chiffré) et public (signé).
- Moins de vulnérabilités possibles par design.
Adoption croissante, bibliothèques dans la plupart des langages.
8.3 Macaroons
Tokens avec caveats (conditions supplémentaires que le détenteur peut ajouter mais pas retirer). Permet délégation fine (ex. "voici mon token, mais restreint à l'endpoint X seulement").
Utilisé par Google pour certains services internes. Adoption limitée dans l'industrie.
8.4 Biscuit tokens
Tokens authorization-focused avec Datalog pour exprimer les permissions. Supportent offline attenuation (on peut réduire les privilèges d'un token sans serveur). Niche mais intéressant.
9. Pentest et tests de sécurité JWT
9.1 Outils manuels
- jwt.io : décoder et inspecter visuellement un JWT.
- jwt_tool (ticarpi) : outil offensif complet, teste alg:none, HMAC confusion, kid injection, bruteforce.
- Burp Suite JWT extension : intercepter et manipuler JWT dans les requêtes.
9.2 Tests automatisés en CI
- Vérifier que l'API rejette un JWT avec
alg: none. - Vérifier que l'API rejette un JWT signé avec la clé publique (HMAC confusion).
- Vérifier que l'API rejette un JWT expiré.
- Vérifier que l'API rejette un JWT d'un autre issuer.
- Vérifier que l'API rejette un JWT d'une autre audience.
Ces tests appartiennent à la suite de sécurité automatisée et tournent à chaque PR.
9.3 Scanner offensif
jwt_tool en mode playbook attaque :
python3 jwt_tool.py <JWT> -M pbTeste automatiquement toutes les vulnérabilités courantes et rapporte celles exploitables.
10. Décisions d'architecture
10.1 Quand utiliser JWT
- Fédération d'identité OIDC entre IdP et SPs (standard).
- Access tokens OAuth 2.0 courte durée.
- M2M entre services de confiance avec rotation (JWT OAuth Client Credentials).
- Tokens de contexte dans systèmes distribués où stateless est critique.
10.2 Quand privilégier autre chose
- Sessions web classiques : cookie session + session store Redis = plus simple, plus sûr.
- Révocation critique (bancaire, médical) : opaque tokens + session store.
- Durée de vie très longue : jamais en JWT. Token courte durée + refresh.
- Données sensibles dans le token : JWT = lisible. Chiffrement nécessaire (JWE ou stockage externe).
10.3 Pattern hybride recommandé 2026
Pour une application web moderne :
- Authentication : IdP externe (Okta, Entra ID, Auth0) émet ID token JWT (OIDC).
- Session web : cookie HttpOnly qui réfère à une session server-side (Redis).
- API access : access token JWT courte durée (15 min), obtenu et renouvelé côté serveur.
- Service-to-service : mTLS + JWT Client Credentials courte durée.
- Refresh tokens : opaques avec rotation, stockés en DB avec possibilité de révocation.
Combine les avantages JWT (standard, interopérable, stateless) et session classique (révocable, sécurisée).
11. Erreurs fréquentes
11.1 Utiliser JWT comme session cookie
JWT stocké en localStorage avec durée 24h = tout le code JS a accès. Une XSS = vol de la session. Préférer cookie HttpOnly avec session ID référençant un store server-side.
11.2 Décoder sans vérifier
const payload = JSON.parse(atob(token.split('.')[1]));
if (payload.role === 'admin') { ... }Code qui lit le payload sans vérifier la signature. L'attaquant forge n'importe quel payload. Bug fréquent avec jwt-decode.
11.3 Confiance aux claims custom non signés
Si une partie du contexte vient du JWT, tout doit être signé par l'émetteur de confiance. Pas de mix "certaines claims signées, d'autres modifiables".
11.4 Payload trop gros
Un JWT ne devrait pas dépasser 8 KB (limite de certains proxies et cookies). Payload lourd = latence, mauvaise expérience.
11.5 Secret en dur dans le code
Secret HS256 committé dans Git = compromission permanente dès que quelqu'un accède au repo. Mitigation : secret depuis Vault ou variables d'environnement.
11.6 Clock skew non géré
Deux serveurs dont les horloges diffèrent de quelques secondes peuvent rejeter des JWT valides (exp légèrement dépassé). Tolérance typique : 30 secondes dans la plupart des libs. À configurer.
11.7 Logging du token complet
Loguer Authorization: Bearer eyJ... dans les access logs expose les tokens à quiconque accède aux logs (y compris admins cloud, SIEM operators). Mitigation : masking du header Authorization en log pipeline.
12. Verdict et posture Zeroday
Les JWT sont omniprésents et incontournables en 2026, mais leur apparente simplicité cache une surface de vulnérabilités réelle. La majorité des bugs JWT ne sont pas des zero-days exotiques : ce sont des erreurs de configuration ou d'implémentation récurrentes documentées depuis 10 ans.
Pour un développeur : ne jamais implémenter sa propre vérification JWT. Utiliser une bibliothèque mature, la configurer strictement (algorithms fixés, claims validés), tester les cas d'attaque courants. La compétence différenciante est de comprendre ce que la bibliothèque valide et ce qu'elle ne valide pas par défaut.
Pour un AppSec : la revue JWT est une activité à inclure dans tout audit d'API. Checklist simple mais implacable : algorithme fixé, claims tous validés, durée courte, stockage client approprié, révocation possible.
Pour une organisation : l'investissement en JWT security tools (scanners, tests CI, formation) est faible au regard du coût d'un incident. Beaucoup de fuites massives API démarrent par un JWT mal vérifié.
Pour approfondir : OWASP API Security Top 10 expliqué pour voir JWT dans le contexte API2 Broken Authentication, autorisation d'API : les bases pour la couche authorization qui suit la validation JWT, gestion de session sécurisée pour l'alternative session classique, SSO : définition pour le contexte OIDC qui émet les JWT.







