Ton API sert des données à des dizaines de clients. Comment savoir que la requête qui arrive vient bien de Marie, et pas d'un script qui a deviné un identifiant ? C'est tout le boulot d'un JSON Web Token, le fameux JWT. Le principe tient en une phrase : ton serveur signe un petit document (l'utilisateur, son rôle, une date d'expiration) avec une clé secrète, et ce badge signé voyage avec chaque requête. Impossible à falsifier sans la clé, trivial à vérifier. PyJWT est la bibliothèque Python de référence pour créer et contrôler ces tokens. En dix lignes, tu peux verrouiller une route. On y va.
Avant de coder, une image pour bien comprendre. Un JWT est une chaîne coupée en trois morceaux par des points : l'en-tête, qui indique l'algorithme utilisé, la charge utile, qui porte tes données, et la signature, le sceau qui prouve que rien n'a bougé. Les deux premières parties sont simplement du base64 : tout le monde peut les lire. La troisième est calculée avec ta clé secrète, et c'est elle qui fait toute la sécurité. Si un seul octet change, la signature ne correspond plus et le token est rejeté.
1. Installer PyJWT
Rien de sorcier : une seule dépendance, 100 % Python, sans serveur ni base à configurer.
pip install pyjwt2. Signer ton premier token
Un token se fabrique avec trois ingrédients : une charge utile (le payload), une clé secrète, et un algorithme de signature. On part sur HS256, le classique HMAC-SHA256, qui n'exige qu'une seule clé partagée entre celui qui signe et celui qui vérifie.
import jwt
import datetime as dt
SECRET = "cette-cle-doit-faire-au-moins-32-octets-et-rester-secrete"
payload = {
"sub": "alice",
"role": "admin",
"exp": dt.datetime.now(dt.timezone.utc) + dt.timedelta(minutes=30),
}
token = jwt.encode(payload, SECRET, algorithm="HS256")
print(token)Le champ sub identifie l'utilisateur (subject), role porte ses droits, et exp fixe l'expiration : passé ce délai, le token ne vaut plus rien. À toi de décider ce que tu mets dedans. jwt.encode te renvoie une longue chaîne du genre eyJhbGciOiJIUzI1NiJ9... C'est ton badge. Note au passage que la clé doit être longue : en dessous de 32 octets, PyJWT te le reproche, et pour cause, une clé courte se devine par force brute.
3. Vérifier et décoder
Côté serveur, on décode et surtout on vérifie la signature. Si la clé ne colle pas, le token est rejeté.
decoded = jwt.decode(token, SECRET, algorithms=["HS256"])
print(decoded["sub"]) # alice
print(decoded["role"]) # adminLe paramètre algorithms est obligatoire depuis la version 2 : tu déclares explicitement quels algorithmes tu acceptes. C'est voulu, ça bloque toute une famille d'attaques par confusion d'algorithmes, où un attaquant tente de faire vérifier un token HS256 comme s'il était signé avec une clé publique. En listant ce que tu acceptes, tu fermes la porte.
4. Refuser les tokens trafiqués et expirés
Le vrai intérêt d'un JWT, c'est de détecter la fraude. Modifie un seul caractère du token, ou signe avec la mauvaise clé, et PyJWT lève une exception.
try:
jwt.decode(token, "mauvaise-cle", algorithms=["HS256"])
except jwt.InvalidSignatureError:
print("Signature invalide : token trafiqué ou mauvaise clé.")Même logique pour l'expiration : un token périmé doit être refusé, jamais toléré.
expired_payload = {
"sub": "bob",
"exp": dt.datetime.now(dt.timezone.utc) - dt.timedelta(seconds=1),
}
expired = jwt.encode(expired_payload, SECRET, algorithm="HS256")
try:
jwt.decode(expired, SECRET, algorithms=["HS256"])
except jwt.ExpiredSignatureError:
print("Token expiré, refusé.")ExpiredSignatureError et InvalidSignatureError héritent toutes deux de jwt.InvalidTokenError. En pratique, tu peux donc attraper InvalidTokenError seul pour refuser d'un coup tout ce qui est faux, malformé ou périmé. C'est exactement ce qu'on fait à l'étape suivante.
5. Brancher ça sur une API
Dans la vraie vie, tu n'écris pas un try/except à chaque route. Tu regroupes tout dans une petite fonction de vérification, puis tu l'appelles en tête de tes endpoints. Avec FastAPI, une simple dépendance fait le travail.
def verifier(token_brut: str) -> dict | None:
try:
return jwt.decode(token_brut, SECRET, algorithms=["HS256"])
except jwt.ExpiredSignatureError:
return None # expiré
except jwt.InvalidTokenError:
return None # forgé ou malformé
# En pseudo-code, côté FastAPI :
# @app.get("/admin")
# def admin(identite=Depends(verifier)):
# if identite is None or identite["role"] != "admin":
# raise HTTPException(401, "Accès refusé")6. Les règles d'or
Trois réflexes à garder. D'abord, la clé ne sort jamais de ton serveur et ne se commit jamais : génère-la avec secrets.token_hex(32) et range-la dans une variable d'environnement. Ensuite, fixe toujours une expiration courte, quinze ou trente minutes, et renouvelle via un refresh token si besoin. Enfin, n'embarque jamais de donnée sensible (mot de passe, email, numéro de carte) dans le payload : la signature garantit l'intégrité, pas la confidentialité. N'importe qui peut décoder le contenu en base64.
Pourquoi pas une bonne vieille session ?
La question revient souvent. Une session classique stocke l'état côté serveur : à chaque requête, il faut aller consulter la base pour savoir qui est connecté. Un JWT, lui, est sans état. Le serveur n'a rien à stocker ni à interroger : il recalcule la signature et c'est réglé. Sur une API répartie sur plusieurs machines derrière un load balancer, c'est un avantage énorme : pas de session partagée à synchroniser, pas de base à solliciter à chaque appel. Le prix à payer, c'est la révocation : impossible d'annuler un token déjà émis sans mécanisme supplémentaire. D'où l'expiration courte et le refresh token.
PyJWT ne fait qu'une chose, mais il la fait très bien : signer et vérifier des tokens. C'est la brique invisible qui se cache derrière l'authentification de la plupart des API modernes, des applications mobiles aux microservices. Maintenant que tu sais la poser, tes routes ne dépendent plus d'une session en mémoire, et ton API sait dire non à un badge falsifié. La suite logique, c'est d'ajouter un refresh token, une liste noire pour la révocation, et pourquoi pas de la signature asymétrique RS256. Mais tu as déjà le socle, et il tient en dix lignes.






