Django signing, max_age les pièges et mise en œuvre de jetons à usage unique
django.core.signing est pratique. Cependant, si vous vous fiez uniquement à max_age pour émettre des jetons, vous pourriez avoir de graves failles de sécurité.
🚫 max_age n'est pas "à usage unique"
C'est le malentendu le plus courant. TimestampSigner ne valide que la période de validité.
loads(token, max_age=600)Cet jeton est valable pendant 10 minutes. Pendant ces 10 minutes, il réussit à loads 100 et 1000 fois.
C'est parce que le module signing ne conserve pas d'état.
Que se passe-t-il si un lien de réinitialisation de mot de passe ou un lien de vérification par e-mail est utilisé plusieurs fois ? C'est une terrible situation.
La fonctionnalité "à usage unique" doit être mise en œuvre manuellement. Je vais introduire deux méthodes courantes : DB et Cache.
1. Méthode DB (la méthode la plus sûre)
Clé : gérer l'utilisation du jeton par un drapeau DB (used).
Fortement recommandé pour les réinitialisations de mot de passe, l'activation de compte, l'approbation de paiement, etc.
1. Conception du modèle
Vous aurez besoin d'un modèle pour stocker l'état du jeton. Utilisez UUID comme identifiant unique (Nonce).
# models.py
import uuid
from django.db import models
from django.conf import settings
class OneTimeToken(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
nonce = models.UUIDField(default=uuid.uuid4, unique=True, editable=False)
created_at = models.DateTimeField(auto_now_add=True)
used = models.BooleanField(default=False)
2. Génération du jeton
Créez d'abord un enregistrement "avant utilisation" dans la DB. Ensuite, signez la valeur nonce de cet enregistrement.
from django.core.signing import TimestampSigner
from .models import OneTimeToken
def create_one_time_link(user):
# 1. Créer un enregistrement de jeton "avant utilisation"
token_record = OneTimeToken.objects.create(user=user)
# 2. Signer l'identifiant unique (nonce)
signer = TimestampSigner()
signed_nonce = signer.dumps(str(token_record.nonce))
return f"https://example.com/verify?token={signed_nonce}"
3. Vérification du jeton
Cette logique est essentielle.
from django.core.signing import TimestampSigner, BadSignature, SignatureExpired
from .models import OneTimeToken
def verify_one_time_token(signed_nonce, max_age_seconds=1800): # 30 minutes
signer = TimestampSigner()
try:
# 1. Vérification de la signature + du temps d'expiration
nonce_str = signer.loads(signed_nonce, max_age=max_age_seconds)
# 2. Récupérer le Nonce depuis la DB
token_record = OneTimeToken.objects.get(nonce=nonce_str)
# 3. ★★★ Vérifiez s'il a déjà été utilisé ★★★
if token_record.used:
print("Ce jeton a déjà été utilisé.")
return None
# 4. ★★★ Changer l'état en "utilisé" ★★★ (transaction DB recommandée)
token_record.used = True
token_record.save()
# 5. Succès : retourner l'utilisateur
return token_record.user
except SignatureExpired:
print("Le jeton a expiré.")
return None
except (BadSignature, OneTimeToken.DoesNotExist):
print("Ce jeton est invalide.")
return None
- Avantages : très sûr et clair. L'historique d'utilisation est conservé dans la DB pour un suivi.
- Inconvénients : il y a des I/O de la DB. Vous devez organiser régulièrement la suppression des anciens enregistrements de jetons.
2. Méthode Cache (une méthode plus rapide)
Clé : enregistrez les jetons utilisés dans la cache (par exemple, Redis) comme une "liste noire".
Convient pour des liens "à usage unique" ou la prévention des appels API en double là où la vitesse est primordiale et le risque de sécurité est relativement faible.
1. Génération du jeton
Contrairement à la méthode DB, aucune action supplémentaire n'est nécessaire lors de sa création. Il suffit de signer les données.
from django.core.signing import TimestampSigner
signer = TimestampSigner()
token = signer.dumps({"user_id": 123})
2. Vérification du jeton (utilisation de la liste noire)
Après un succès avec loads, vérifiez dans la cache si "ce jeton a déjà été utilisé".
from django.core.cache import cache
from django.core.signing import TimestampSigner, BadSignature, SignatureExpired
import hashlib
def verify_one_time_token_with_cache(token, max_age_seconds=1800):
signer = TimestampSigner()
try:
# 1. Vérification de la signature + du temps d'expiration
data = signer.loads(token, max_age=max_age_seconds)
# 2. Création de la clé de cache (jeton peut être long donc hash)
token_hash = hashlib.sha256(token.encode()).hexdigest()
cache_key = f"used_token:{token_hash}"
# 3. ★★★ Vérifiez si dans la cache (liste noire) ★★★
if cache.get(cache_key):
print("Ce jeton a déjà été utilisé.")
return None
# 4. ★★★ Enregistrez-le comme "utilisé" dans la cache ★★★
# Enregistrez uniquement selon le max_age d'origine du jeton
cache.set(cache_key, 'used', timeout=max_age_seconds)
# 5. Succès : retournez les données
return data
except SignatureExpired:
print("Le jeton a expiré.")
return None
except BadSignature:
print("Ce jeton est invalide.")
return None
- Avantages : beaucoup plus rapide que la DB. Grâce à TTL du cache, les jetons expirés seront automatiquement nettoyés.
- Inconvénients : si le cache (quelconque, comme Redis) tombe en panne ou perd des données, les jetons non expirés peuvent être réutilisés. (moins de stabilité que la DB)
Conclusion
| Méthode | Situations recommandées | Avantages | Inconvénients |
|---|---|---|---|
| DB | Réinitialisation de mot de passe, paiement, authentification de compte | Sûr, enregistrement permanent | Charge DB, nettoyage nécessaire |
| Cache | Liens "à usage unique", prévention de duplication des API | Rapide, nettoyage automatique (TTL) | Risque de réutilisation en cas de perte de cache |
django.core.signing ne valide que l'intégrité et l'expiration. L'état d'utilisation revient au développeur. Choisissez une méthode appropriée selon votre situation.
Aucun commentaire.