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.