🛠️ Module 4 — Signer et vérifier un JWT en Python
Bonne lecture et bon apprentissage ! Junior TSAFACK – 12/09/2026 ⏱️ Temps de lecture estimé : 10 minutes
Place à la pratique ! 🧪 Tout le code de ce module a été réellement exécuté (environnement Anaconda dédié, pip install pyjwt cryptography) — les sorties affichées sont les sorties obtenues, pas des exemples inventés.
conda create -n jwt-course python=3.11conda activate jwt-coursepip install pyjwt cryptography1️⃣ HS256 : un secret partagé
Section titled “1️⃣ HS256 : un secret partagé”HS256 (HMAC + SHA-256) est un algorithme symétrique : la même clé sert à signer et à vérifier. Simple à mettre en place, mais tout service qui doit vérifier un token doit connaître ce secret — donc peut aussi en fabriquer de faux.
import jwt
SECRET = "un-secret-de-demonstration-ne-jamais-utiliser-en-production"
payload = {"sub": "user-42", "role": "formateur"}
token = jwt.encode(payload, SECRET, algorithm="HS256")print("Token signe (HS256) :")print(token)print()
decoded = jwt.decode(token, SECRET, algorithms=["HS256"])print("Verification avec le bon secret :")print(decoded)print()
print("Verification avec un MAUVAIS secret :")try: jwt.decode(token, "un-secret-invente-par-un-attaquant", algorithms=["HS256"])except jwt.InvalidSignatureError as exc: print(f"Rejete comme attendu -> {type(exc).__name__}: {exc}")Sortie réellement obtenue :
Token signe (HS256) :eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyLTQyIiwicm9sZSI6ImZvcm1hdGV1ciJ9.1ddCAHW2GwbW3ZpkKQJpflDLv4bBbElAaGTEgt71uS0
Verification avec le bon secret :{'sub': 'user-42', 'role': 'formateur'}
Verification avec un MAUVAIS secret :Rejete comme attendu -> InvalidSignatureError: Signature verification failed2️⃣ RS256 : une paire de clés
Section titled “2️⃣ RS256 : une paire de clés”RS256 est asymétrique : une clé privée signe, une clé publique (partageable sans risque) vérifie. Un service qui n’a que la clé publique peut valider des tokens mais ne peut jamais en forger. C’est le choix par défaut recommandé dès que plusieurs services doivent vérifier des tokens émis par un seul serveur d’authentification.
import jwtfrom cryptography.hazmat.primitives import serializationfrom cryptography.hazmat.primitives.asymmetric import rsa
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)public_key = private_key.public_key()
private_pem = private_key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption(),)public_pem = public_key.public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo,)
payload = {"sub": "user-42", "role": "formateur"}token = jwt.encode(payload, private_pem, algorithm="RS256")print("Token signe avec la cle PRIVEE (RS256) :")print(token)print()
decoded = jwt.decode(token, public_pem, algorithms=["RS256"])print("Verifie avec la cle PUBLIQUE uniquement :")print(decoded)Sortie réellement obtenue (le token est tronqué ici pour la lisibilité, il fait ~300 caractères) :
Token signe avec la cle PRIVEE (RS256) :eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyLTQyIiwicm9sZSI6ImZvcm1hdGV1ciJ9.ElCFsqe-zhN4FWFRx9T1993scXMM4Nrpvvwcr9H7bVhs1lEYnGyGDSr2Jg7YAGcacWX1Kjn8bBYXscWN9CNy6Rk2Fis20uSJWMfCgmXzIM8K62QCDVugZwCGgLBGMXifl59-5BNhyfgrONlf1EyIL6gR884pb5Rqp48rQ-ubhKOwpULQchtDc2BrkXkHUuOk23J7tjzMbUaCRHoOSQT3bHEz5qodUnmo9WZ0ZBOaAvfKXij146hAaq3r1pP4GN23TbMWDBAbx1-Onv00zf2eEwAYdWhulZ20S0gwCDooBm5yBBzPSrxE73vbxR-u7BPSk0zeBN3G8lqpYKRCPERt7Q
Verifie avec la cle PUBLIQUE uniquement :{'sub': 'user-42', 'role': 'formateur'}3️⃣ Falsifier un token : la vérification échoue 🚫
Section titled “3️⃣ Falsifier un token : la vérification échoue 🚫”Que se passe-t-il si on modifie le payload d’un token sans connaître le secret de signature ? Reprenons un token, changeons "role": "utilisateur" en "role": "admin" directement dans le texte décodé, et recomposons le token :
import base64import json
def b64url_decode(s): return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))
def b64url_encode(data): return base64.urlsafe_b64encode(data).rstrip(b"=").decode()
token = jwt.encode({"sub": "user-42", "role": "utilisateur"}, SECRET, algorithm="HS256")header_b64, payload_b64, signature_b64 = token.split(".")
payload = json.loads(b64url_decode(payload_b64))print("Payload original :", payload)
payload["role"] = "admin" # un attaquant tente de s'auto-promouvoirforged_payload_b64 = b64url_encode(json.dumps(payload).encode())forged_token = f"{header_b64}.{forged_payload_b64}.{signature_b64}" # signature INCHANGEE
print("Payload falsifie :", payload)print()print("Verification du token falsifie (signature inchangee) :")try: jwt.decode(forged_token, SECRET, algorithms=["HS256"]) print("PROBLEME : le token falsifie a ete accepte !")except jwt.InvalidSignatureError as exc: print(f"Rejete comme attendu -> {type(exc).__name__}: {exc}")Sortie réellement obtenue :
Payload original : {'sub': 'user-42', 'role': 'utilisateur'}Payload falsifie : {'sub': 'user-42', 'role': 'admin'}
Verification du token falsifie (signature inchangee) :Rejete comme attendu -> InvalidSignatureError: Signature verification failed🛡️ Voilà exactement à quoi sert la signature : la moindre modification d’un seul caractère du payload rend la signature (calculée sur l’ancien contenu) invalide pour le nouveau contenu.
4️⃣ L’expiration en action ⏰
Section titled “4️⃣ L’expiration en action ⏰”import time
expired_token = jwt.encode( {"sub": "user-42", "exp": int(time.time()) - 10}, # expire il y a 10 secondes SECRET, algorithm="HS256",)try: jwt.decode(expired_token, SECRET, algorithms=["HS256"]) print("PROBLEME : un token expire a ete accepte !")except jwt.ExpiredSignatureError as exc: print(f"Rejete comme attendu -> {type(exc).__name__}: {exc}")Rejete comme attendu -> ExpiredSignatureError: Signature has expired5️⃣ La faille historique alg: none 🕳️
Section titled “5️⃣ La faille historique alg: none 🕳️”Voici une vraie faille qui a touché plusieurs bibliothèques JWT par le passé : le champ alg du header est… choisi par l’émetteur du token. Certaines implémentations naïves faisaient confiance à cette valeur en lisant l’algorithme depuis le token lui-même, plutôt que d’imposer un algorithme attendu côté serveur. Un attaquant n’avait alors qu’à écrire "alg": "none" et laisser la signature vide.
none_header_b64 = b64url_encode(json.dumps({"alg": "none", "typ": "JWT"}).encode())none_payload_b64 = b64url_encode(json.dumps({"sub": "user-42", "role": "admin"}).encode())none_token = f"{none_header_b64}.{none_payload_b64}." # signature vide !
print("Token 'alg: none' (signature vide) :", none_token)print()print("Verification en exigeant explicitement HS256 :")try: jwt.decode(none_token, SECRET, algorithms=["HS256"]) print("PROBLEME : le token 'none' a ete accepte !")except jwt.exceptions.InvalidAlgorithmError as exc: print(f"Rejete comme attendu -> {type(exc).__name__}: {exc}")Token 'alg: none' (signature vide) : eyJhbGciOiAibm9uZSIsICJ0eXAiOiAiSldUIn0.eyJzdWIiOiAidXNlci00MiIsICJyb2xlIjogImFkbWluIn0.
Verification en exigeant explicitement HS256 :Rejete comme attendu -> InvalidAlgorithmError: The specified alg value is not allowedPyJWT nous protège parce qu’on lui a explicitement dit algorithms=["HS256"]. C’est la règle d’or que détaille le Module 5 : ne jamais laisser le token dicter l’algorithme utilisé pour sa propre vérification.
6️⃣ Découverte en bonus : PyJWT bloque aussi la confusion RS256 ↔ HS256 🔍
Section titled “6️⃣ Découverte en bonus : PyJWT bloque aussi la confusion RS256 ↔ HS256 🔍”Une autre attaque classique : puisqu’une clé publique RS256 est… publique, un attaquant qui la connaît peut tenter de l’utiliser comme secret HMAC pour forger un token HS256, en espérant qu’un serveur mal configuré accepte les deux algorithmes indifféremment.
import hashlibimport hmac
# Le serveur genere sa paire de cles RS256 comme d'habitude (voir section 2).# La cle publique est... publique par definition.
header_b64 = b64url_encode(json.dumps({"alg": "HS256", "typ": "JWT"}).encode())payload_b64 = b64url_encode(json.dumps({"sub": "user-42", "role": "admin"}).encode())signing_input = f"{header_b64}.{payload_b64}".encode()signature = hmac.new(public_pem, signing_input, hashlib.sha256).digest()forged_token = f"{header_b64}.{payload_b64}.{b64url_encode(signature)}"
print("--- Serveur qui accepte HS256 ET RS256 sans distinction (config a risque) ---")try: jwt.decode(forged_token, public_pem, algorithms=["HS256", "RS256"])except jwt.exceptions.InvalidKeyError as exc: print(f"Rejete par PyJWT lui-meme -> {type(exc).__name__}: {exc}")En testant cette attaque, une découverte intéressante : PyJWT refuse par construction d’utiliser une clé qui a la forme d’une clé asymétrique (PEM) comme secret HMAC — une protection intégrée à la bibliothèque elle-même, indépendante du code applicatif :
--- Serveur qui accepte HS256 ET RS256 sans distinction (config a risque) ---Rejete par PyJWT lui-meme -> InvalidKeyError: The specified key is an asymmetric key or x509 certificate and should not be used as an HMAC secret.Et même en restreignant explicitement à algorithms=["RS256"] (la bonne pratique), le token forgé est de toute façon rejeté :
--- Serveur qui n'accepte QUE RS256 pour ses tokens (bonne pratique) ---Rejete comme attendu -> InvalidAlgorithmError: The specified alg value is not allowedDeux couches de défense pour le prix d’une : la bibliothèque elle-même, et la configuration explicite de l’algorithme attendu. C’est exactement ce genre de « défense en profondeur » qu’on souhaite retrouver dans n’importe quel système d’authentification.
🧠 À retenir
Section titled “🧠 À retenir”- HS256 = un secret partagé (simple, mais tout vérificateur peut aussi signer). RS256 = clé privée/publique (le vérificateur ne peut pas forger).
- Modifier un seul caractère du payload invalide la signature — c’est tout le principe.
expest vérifié automatiquement par les bibliothèques sérieuses.- Toujours préciser explicitement les algorithmes acceptés (
algorithms=[...]) à la vérification, jamais faire confiance aualgdu token.
Prochaine étape
Section titled “Prochaine étape”👉 Module 5 : Sécurité — pièges classiques et bonnes pratiques