Django signing, max_age de valkuil en het implementeren van eenmalige tokens



django.core.signing is handig. Maar als je uitsluitend vertrouwt op max_age bij het uitgeven van tokens, kan dat ernstige beveiligingsproblemen opleveren.

🚫 max_age is geen "eenmalig"

Een van de meest voorkomende misverstanden. TimestampSigner's max_age controleert alleen de geldigheidstijd.

loads(token, max_age=600)

Dit token is 10 minuten geldig. Gedurende deze 10 minuten kan het 100 of 1000 keer succesvol worden geladen.

Dat komt omdat het signing-moduul geen status (state) opslaat.

Wat als de link voor het resetten van wachtwoorden of de e-mailbevestigingslink meerdere keren wordt gebruikt? Dat is verschrikkelijk.

De functie "eenmalig gebruik" moet door ons zelf worden geïmplementeerd. We presenteren de twee meest voorkomende methoden, DB en Cache.


1. DB-methode (de veiligste methode)

Kern: Beheer het gebruik van de token met een DB-vlag (used).

Sterk aanbevolen voor beveiligingsgevoelige acties zoals het resetten van wachtwoorden, het activeren van accounts en het goedkeuren van betalingen.

1. Modelontwerp

We hebben een model nodig om de status van de token op te slaan. We gebruiken een UUID als unieke identificator (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. Token genereren

Maak eerst een "voor gebruik" record in de DB. Onderteken vervolgens de nonce waarde van dat record.

from django.core.signing import TimestampSigner
from .models import OneTimeToken

def create_one_time_link(user):
    # 1. Genereer record voor "voor gebruik" token
    token_record = OneTimeToken.objects.create(user=user)

    # 2. Onderteken de unieke identificator (nonce)
    signer = TimestampSigner()
    signed_nonce = signer.dumps(str(token_record.nonce))

    return f"https://example.com/verify?token={signed_nonce}"

3. Token validatie

Deze logica is cruciaal.

from django.core.signing import TimestampSigner, BadSignature, SignatureExpired
from .models import OneTimeToken

def verify_one_time_token(signed_nonce, max_age_seconds=1800): # 30 minuten
    signer = TimestampSigner()

    try:
        # 1. Ondertekening + geldigheidstijd controleren
        nonce_str = signer.loads(signed_nonce, max_age=max_age_seconds)

        # 2. Zoek Nonce in de DB
        token_record = OneTimeToken.objects.get(nonce=nonce_str)

        # 3. ★★★ Controleer of het al is gebruikt ★★★
        if token_record.used:
            print("Dit token is al gebruikt.")
            return None

        # 4. ★★★ Wijzig de status naar "gebruikt" ★★★ (DB-transactie aanbevolen)
        token_record.used = True
        token_record.save()

        # 5. Succes: retourneer gebruiker
        return token_record.user

    except SignatureExpired:
        print("Token is verlopen.")
        return None
    except (BadSignature, OneTimeToken.DoesNotExist):
        print("Ongeldig token.")
        return None
  • Voordelen: Zeer veilig en transparant. Het gebruiksgeschiedenis wordt in de DB opgeslagen, wat tracking mogelijk maakt.
  • Nadelen: DB I/O vindt plaats. Verouderde tokenrecords moeten regelmatig worden opgeschoond.

2. Cache-methode (snellere methode)



Kern: Registreer gebruikte tokens in de cache (bijv. Redis) als "blacklist".

Geschikt voor "eenmalig bekijken" links of het voorkomen van dubbele API-aanroepen, waar snelheid belangrijk is en de beveiligingsrisico's relatief laag zijn.

1. Token genereren

In tegenstelling tot de DB-methode is er bij de creatie geen aparte handeling vereist. Gewoon de data ondertekenen.

from django.core.signing import TimestampSigner

signer = TimestampSigner()
token = signer.dumps({"user_id": 123})

2. Token validatie (gebruik van blacklist)

Na succesvolle loads, controleer in de cache of "dit token ooit is gebruikt".

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. Ondertekening + geldigheidstijd controleren
        data = signer.loads(token, max_age=max_age_seconds)

        # 2. Maak cache sleutel aan (omdat de token lang kan zijn, hash)
        token_hash = hashlib.sha256(token.encode()).hexdigest()
        cache_key = f"used_token:{token_hash}"

        # 3. ★★★ Controleer of het in de cache (blacklist) zit ★★★
        if cache.get(cache_key):
            print("Dit token is al gebruikt.")
            return None

        # 4. ★★★ Registreer in de cache als "gebruikt" ★★★
        #    Alleen voor de oorspronkelijke max_age van de token in de cache bewaren
        cache.set(cache_key, 'used', timeout=max_age_seconds)

        # 5. Succes: retourneer data
        return data

    except SignatureExpired:
        print("Token is verlopen.")
        return None
    except BadSignature:
        print("Ongeldig token.")
        return None
  • Voordelen: Veel sneller dan de DB. Door de TTL van de cache worden verlopen tokens automatisch opgeruimd.
  • Nadelen: Als de cache (zoals Redis) uitvalt of gegevens verliest, kan een niet-verlopen token hergebruikt worden. (minder stabiliteit dan de DB)

Conclusie

Methode Aangeraden situatie Voordelen Nadelen
DB Wachtwoord resetten, betalingen, account validatie Veilig, permanente registratie DB-belasting, schoonmaak nodig
Cache "Eenmalig bekijken" links, voorkomen van dubbele API-aanroepen Snel, automatische opruiming (TTL) Risico op hergebruik bij cache verlies

django.core.signing controleert alleen integriteit en vervaldatum. "Gebruik status" is aan de ontwikkelaar. Kies een verstandige methode die past bij de situatie.