Una comparación == sobre un hash no es suficiente para elegir el mecanismo correcto. sha256, HMAC, hash salado, cifrado: cada enfoque ofrece garantías distintas. Entender cuáles son cambia concretamente la forma de almacenar y verificar un token en Django.

Hash simple

import hashlib

token_hash = hashlib.sha256(token.encode()).hexdigest()

Un hash simple es determinista: la misma entrada siempre produce la misma salida. No interviene ningún secreto del servidor. Es imposible recuperar el token original a partir del hash (sha256 es una función unidireccional). Pero si alguien conoce o adivina el token, puede recalcular el hash y comparar.

Ese es el problema principal: si los tokens son cortos, predecibles o provienen de un espacio limitado, un atacante con una copia de la base de datos puede probar valores offline a gran velocidad, sin consultar nunca el servidor.

Hash salado

import hashlib
import os

salt = os.urandom(16).hex()  # generado y almacenado por fila
token_hash = hashlib.sha256((salt + token).encode()).hexdigest()

Se añade un salt: un valor aleatorio único por fila. Dos tokens idénticos producirán hashes distintos si sus salts difieren. Esto destruye la eficacia de las tablas arcoíris y obliga al atacante a tratar cada fila por separado.

Para contraseñas, los algoritmos dedicados como bcrypt o argon2 van más lejos: integran el salt de forma nativa y añaden un factor de coste (lentitud intencional) que hace prohibitivos los ataques de fuerza bruta por GPU. Un sha256 + salt manual sigue siendo rápido de calcular, lo que supone una debilidad para las contraseñas.

En cualquier caso, si el salt se almacena en texto plano en la misma tabla y no existe un secreto del servidor, la protección sigue siendo limitada: un volcado completo expone salt + hash, y un atacante con paciencia puede seguir probando candidatos offline.

HMAC

import hmac
import hashlib

mac = hmac.new(
    key=server_secret.encode(),
    msg=token.encode(),
    digestmod=hashlib.sha256,
).hexdigest()

HMAC introduce un secreto del servidor. Sin ese secreto, es imposible recalcular el valor correcto. Un atacante que obtenga el volcado de la base de datos no puede recuperar el token ni verificar candidatos: le falta el secreto.

En Django, salted_hmac de django.utils.crypto se apoya en SECRET_KEY como secreto implícito:

from django.utils.crypto import salted_hmac

token_hmac = salted_hmac("auth-token-salt", token).hexdigest()

Por defecto, salted_hmac usa SHA-1 (por compatibilidad histórica). Desde Django 3.1, un parámetro algorithm permite especificar SHA-256:

token_hmac = salted_hmac("auth-token-salt", token, algorithm="sha256").hexdigest()

La verificación es sencilla: se recalcula el HMAC del token proporcionado por el cliente y se compara con el valor almacenado, en tiempo constante con constant_time_compare.

Cifrado

from cryptography.fernet import Fernet

key = Fernet.generate_key()
f = Fernet(key)
encrypted = f.encrypt(token.encode())
decrypted = f.decrypt(encrypted)  # recupera el token original

El cifrado es reversible: el servidor puede recuperar el valor original. Es útil cuando hay que leer el dato más adelante, por ejemplo una clave API que se muestra una sola vez o un token OAuth que se debe renovar.

Para verificar un token de autenticación, esto es innecesariamente arriesgado: no se necesita recuperar el token, solo confirmar que es válido. Almacenar un valor descifrable en la base de datos amplía la superficie de ataque sin ningún beneficio.

Resumen

MecanismoSecreto del servidorReversibleUso típico
Hash simpleNoNoHuella digital, deduplicación
Hash salado (sha256 + salt)NoNoAlmacenamiento básico, no recomendado para contraseñas
bcrypt / argon2NoNoContraseñas (lentitud intencional)
HMACNoVerificación de token
CifradoValor que debe recuperarse después

Por qué HMAC es la elección correcta para un token Django

El esquema es el siguiente: generar un token bruto, enviarlo al cliente, almacenar su HMAC en la base de datos, y verificar en cada solicitud recalculando el HMAC del token proporcionado.

import secrets
from django.utils.crypto import constant_time_compare, salted_hmac

TOKEN_SALT = "auth-token-v1"

# Generación
raw_token = secrets.token_urlsafe(32)
stored_hash = salted_hmac(TOKEN_SALT, raw_token, algorithm="sha256").hexdigest()
# → almacenar stored_hash en la base de datos, devolver raw_token al cliente

# Verificación
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 de datos queda comprometida, el atacante obtiene HMACs pero no los tokens brutos ni el SECRET_KEY. Sin ese secreto, esos valores no le sirven de nada: no puede recuperar los tokens ni falsificar nuevos.

Este es exactamente el caso de uso de HMAC: generar un token bruto, enviarlo al cliente, no conservar nada legible en la base de datos, y verificar más adelante. El cifrado sería una garantía más débil para esta necesidad, no más fuerte.