Les JWT : structure, forge de token, faille alg:none
12 min · Intermédiaire · 7 août 2026
Une alternative aux sessions côté serveur
Le chapitre précédent décrivait une session dont l’état (qui est connecté, avec quels droits) vit côté serveur, le cookie ne portant qu’un identifiant. Un JWT (JSON Web Token) inverse cette logique : l’état lui-même voyage dans le token, signé pour empêcher toute modification, mais jamais chiffré.
Structure : trois parties séparées par des points
entete.charge_utile.signature Chaque partie est encodée en Base64url (une variante de Base64 compatible avec les URL). L’entête précise l’algorithme de signature utilisé, la charge utile contient les données (identité, rôle, expiration), la signature garantit que ni l’entête ni la charge n’ont été modifiés depuis leur émission par le serveur.
Encoder, décoder : aucun secret nécessaire
import base64, json
def base64url_encode(data):
return base64.urlsafe_b64encode(data).rstrip(b'=').decode()
def base64url_decode(s):
s += '=' * (-len(s) % 4)
return base64.urlsafe_b64decode(s)
# Construire un JWT "normal", comme le ferait un vrai serveur apres connexion
entete = {"alg": "HS256"}
charge = {"utilisateur": "alice", "role": "user"}
jwt = base64url_encode(json.dumps(entete).encode()) + "." + base64url_encode(json.dumps(charge).encode()) + ".signature_valide_ici"
print("JWT :", jwt)
# Decoder : la charge utile n'est PAS chiffree, juste encodee
entete_b64, charge_b64, signature = jwt.split(".")
print("Entete decodee :", json.loads(base64url_decode(entete_b64)))
print("Charge decodee :", json.loads(base64url_decode(charge_b64))) Décoder l’entête et la charge ne demande aucun secret : seule la vérification de la signature en demande un. C’est exactement la même logique que le Base64 du cours sur la cryptographie, appliquée ici au format JWT.
La faille alg:none
Certaines premières bibliothèques JWT faisaient une erreur précise : elles faisaient confiance au champ alg du token lui-même pour savoir comment le vérifier, au lieu d’imposer l’algorithme attendu côté serveur. Un token annonçant alg: none était alors accepté sans aucune vérification de signature.
import base64, json
def base64url_encode(data):
return base64.urlsafe_b64encode(data).rstrip(b'=').decode()
def base64url_decode(s):
s += '=' * (-len(s) % 4)
return base64.urlsafe_b64decode(s)
# Forger un token avec alg:none et un role modifie, sans connaitre aucun secret
entete_forge = base64url_encode(json.dumps({"alg": "none"}).encode())
charge_forgee = base64url_encode(json.dumps({"utilisateur": "alice", "role": "admin"}).encode())
jwt_forge = entete_forge + "." + charge_forgee + "." # signature vide
print("Token force :", jwt_forge)
def verificateur_vulnerable(token):
entete_b64, charge_b64, signature = token.split(".")
entete = json.loads(base64url_decode(entete_b64))
if entete.get("alg") == "none":
# Bug : aucune signature n'est verifiee dans ce cas precis
return json.loads(base64url_decode(charge_b64))
return None # verification normale de la signature omise ici
resultat = verificateur_vulnerable(jwt_forge)
print("Le verificateur vulnerable accepte le token force avec :", resultat) Sans jamais connaître la clé secrète du serveur, la charge utile a été modifiée (role passe de user à admin), et le vérificateur vulnérable l’accepte quand même.
Astuce
La correction est simple une fois le problème identifié : le serveur doit imposer lui-même l’algorithme attendu (« je vérifie toujours en HS256 »), sans jamais faire confiance à ce que le token prétend être. Les bibliothèques JWT modernes appliquent cette correction par défaut.