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écanismeSecret serveurRéversibleUsage typique
Hash simpleNonNonFingerprint, déduplication
Hash salé (sha256 + salt)NonNonStockage basique, non recommandé pour les mots de passe
bcrypt / argon2NonNonMots de passe (lenteur intentionnelle)
HMACOuiNonVérification de token
ChiffrementOuiOuiRelecture 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.