Une comparaison == sur un hash ne suffit pas à choisir le bon mécanisme. sha256, HMAC, hash salé, chiffrement : chaque approche offre des garanties différentes. Comprendre lesquelles change concrètement la façon de stocker et vérifier un token en Django.
Hash simple
import hashlib
token_hash = hashlib.sha256(token.encode()).hexdigest()
Un hash simple est déterministe : la même entrée produit toujours la même sortie. Aucun secret serveur n’est impliqué. Il est impossible de retrouver le token original depuis le hash (sha256 est une fonction à sens unique). Mais si quelqu’un connaît ou devine le token, il peut recalculer le hash et comparer.
C’est le problème principal : si les tokens sont courts, prévisibles ou issus d’un espace limité, un attaquant qui dispose d’un dump de base de données peut tester des valeurs offline à très haute vitesse, sans jamais interroger le serveur.
Hash salé
import hashlib
import os
salt = os.urandom(16).hex() # généré et stocké par ligne
token_hash = hashlib.sha256((salt + token).encode()).hexdigest()
On ajoute un salt : une valeur aléatoire unique par entrée. Deux tokens identiques produiront des hashs différents si leurs salts diffèrent. Cela détruit l’efficacité des tables arc-en-ciel et force l’attaquant à traiter chaque ligne séparément.
Pour les mots de passe, les algorithmes dédiés comme bcrypt ou argon2 vont plus loin : ils intègrent le salt nativement et ajoutent un facteur de coût (lenteur intentionnelle) qui rend les attaques brute force sur GPU prohibitives. Un sha256 + salt manuel reste rapide à calculer, ce qui est une faiblesse pour les mots de passe.
Mais dans tous les cas, si le salt est stocké en clair dans la même table et qu’il n’y a pas de secret serveur, la protection reste limitée : un dump complet expose salt + hash, et un attaquant patient peut toujours tester des candidats offline.
HMAC
import hmac
import hashlib
mac = hmac.new(
key=server_secret.encode(),
msg=token.encode(),
digestmod=hashlib.sha256,
).hexdigest()
HMAC introduit un secret serveur. Sans ce secret, il est impossible de recalculer la valeur correcte. Un attaquant qui obtient le dump de la base ne peut pas retrouver le token ni vérifier des candidats : il lui manque le secret.
En Django, salted_hmac de django.utils.crypto s’appuie sur SECRET_KEY comme secret implicite :
from django.utils.crypto import salted_hmac
token_hmac = salted_hmac("auth-token-salt", token).hexdigest()
Par défaut, salted_hmac utilise SHA-1 (pour compatibilité historique). Depuis Django 3.1, un paramètre algorithm permet de spécifier SHA-256 :
token_hmac = salted_hmac("auth-token-salt", token, algorithm="sha256").hexdigest()
La vérification est simple : on recalcule le HMAC du token fourni par le client et on compare avec la valeur stockée, en temps constant avec constant_time_compare.
Chiffrement
from cryptography.fernet import Fernet
key = Fernet.generate_key()
f = Fernet(key)
encrypted = f.encrypt(token.encode())
decrypted = f.decrypt(encrypted) # retrouve le token original
Le chiffrement est réversible : le serveur peut retrouver la valeur originale. C’est utile quand on doit relire la donnée plus tard, par exemple une clé API à afficher une seule fois ou un token OAuth à rafraîchir.
Pour la vérification d’un token d’authentification, c’est inutilement risqué : on ne veut pas retrouver le token, on veut seulement confirmer qu’il est valide. Stocker une valeur déchiffrable en base agrandit la surface d’attaque sans aucun bénéfice.
Récapitulatif
| Mécanisme | Secret serveur | Réversible | Usage typique |
|---|---|---|---|
| Hash simple | Non | Non | Fingerprint, déduplication |
| Hash salé (sha256 + salt) | Non | Non | Stockage basique, non recommandé pour les mots de passe |
| bcrypt / argon2 | Non | Non | Mots de passe (lenteur intentionnelle) |
| HMAC | Oui | Non | Vérification de token |
| Chiffrement | Oui | Oui | Relecture de valeur nécessaire |
Pourquoi HMAC est le bon choix pour un token Django
Le schéma est le suivant : générer un token brut, l’envoyer au client, stocker son HMAC en base, puis vérifier à chaque requête en recalculant le HMAC du token fourni.
import secrets
from django.utils.crypto import constant_time_compare, salted_hmac
TOKEN_SALT = "auth-token-v1"
# Génération
raw_token = secrets.token_urlsafe(32)
stored_hash = salted_hmac(TOKEN_SALT, raw_token, algorithm="sha256").hexdigest()
# → stocker stored_hash en base, retourner raw_token au client
# Vérification
def verify_token(provided_token: str, stored_hash: str) -> bool:
expected = salted_hmac(TOKEN_SALT, provided_token, algorithm="sha256").hexdigest()
return constant_time_compare(expected, stored_hash)
Si la base est compromise, l’attaquant obtient des HMACs mais pas les tokens bruts ni le SECRET_KEY. Sans ce secret, ces valeurs ne lui permettent rien : il ne peut ni retrouver les tokens, ni en forger de nouveaux.
C’est précisément le cas d’usage de HMAC : générer un token brut, l’envoyer au client, ne pas le garder lisible en base, et pouvoir vérifier plus tard. Le chiffrement serait une garantie plus faible pour ce besoin, pas plus forte.
