Sobald ein Browser eine Ribaunt-Challenge gelöst und dein Server die Lösung akzeptiert hat, sollte dieses Token nie wieder akzeptiert werden. Ohne Wiederholungsschutz könnte ein Angreifer, der ein gültiges { tokens, solutions }-Paar abfängt oder aufzeichnet, es beliebig oft erneut einreichen, bis das Token abläuft – und damit die gesamte Proof-of-Work-Anforderung umgehen. Ribaunt verhindert dies, indem es verbrauchte Token-IDs (jti-Werte) verfolgt und jedes Token ablehnt, das bereits gesehen wurde.
Modi zum Wiederholungsschutz
Ribaunt unterstützt drei Modi für den Wiederholungsschutz, die über die Option replayPrevention von verifySolution() konfiguriert werden.
| Modus | Verhalten | Wann verwenden |
|---|
local (Standard) | Verfolgt verwendete Tokens in einer In-Memory-Map innerhalb des aktuellen Node.js-Prozesses | Single-Process-Deployments |
remote | Delegiert an deinen eigenen verteilten Store über das ReplayStore-Interface | Serverless-Funktionen, horizontal skalierte Dienste, mehrere Node.js-Instanzen |
disabled | Überspringt Replay-Prüfungen vollständig | Nur wenn eine andere Schicht bereits Replays verhindert oder als Legacy-Opt-out während der Migration |
Lokaler Modus (Standard)
Der lokale Modus erfordert keine Konfiguration. Wenn du verifySolution() ohne die Option replayPrevention aufrufst, verwendet Ribaunt automatisch eine prozesslokale Map, um verbrauchte jti-Werte zu speichern. Jeder Versuch, dasselbe Token innerhalb desselben Prozesses zweimal einzureichen, wird abgelehnt.
// Default behavior — local replay protection is active
const valid = await verifySolution(tokens, solutions);
Dies ist die richtige Wahl für die meisten klassischen Node.js-Deployments, bei denen ein einzelner langlaufender Prozess alle Anfragen bearbeitet. Abgelaufene Token-IDs werden bei neuen Verifizierungen automatisch bereinigt, sodass der Speicherverbrauch begrenzt bleibt.
Du musst nichts zusätzlich konfigurieren, um vom lokalen Wiederholungsschutz zu profitieren – er ist standardmäßig aktiv.
Remote-Modus für verteilte Deployments
Wenn deine Anwendung über mehrere Node.js-Instanzen läuft, Serverless-Funktionen nutzt oder auf Edge-Workern bereitgestellt wird, hat jeder Prozess seinen eigenen Speicher. Der lokale Modus kann den Zustand nicht zwischen ihnen teilen, sodass ein von einer Instanz verbrauchtes Token gegen jede andere Instanz wiederholt werden kann. In diesen Fällen benötigst du den remote-Modus.
Im remote-Modus stellst du ein replayStore-Objekt bereit, das das ReplayStore-Interface implementiert. Ribaunt ruft replayStore.consume(jti, expiresAt) für jedes verifizierte Token auf. Deine Implementierung ist für die atomare Operation „als verwendet markieren” verantwortlich – das Standardmuster mit Redis oder Valkey ist SET NX EX (nur setzen, wenn nicht vorhanden, mit Ablauf).
const valid = await verifySolution(tokens, solutions, {
replayPrevention: 'remote',
replayStore: {
consume: async (jti, expiresAt) => {
// Returns true if consumed (first time), false if already seen.
// Implement with Redis/Valkey SET NX EX for atomic "set if not exists".
return true;
},
},
});
Der expiresAt-Wert ist ein Unix-Zeitstempel in Sekunden – gib ihn direkt als TTL für deinen Cache-Schlüssel weiter, damit Einträge automatisch ablaufen und sich keine veralteten Daten ansammeln.
Verwende das expiresAt-Argument, um die TTL für deinen Redis/Valkey-Schlüssel festzulegen. So bleibt dein Store ohne manuelles Aufräumen sauber.
Wiederholungsschutz deaktivieren
Deaktiviere den Wiederholungsschutz nur, wenn ein separater Mechanismus in deinem Stack bereits die Wiederverwendung von Tokens verhindert. Bei deaktiviertem Wiederholungsschutz kann eine gültige Lösung innerhalb des TTL-Fensters des Tokens mehrfach eingereicht werden.
Wenn du dich bisher auf das alte Verhalten verlassen hast, bei dem verifySolution() keine Replay-Prüfungen hatte, kannst du dieses Verhalten während der Migration explizit wiederherstellen:
const valid = await verifySolution(tokens, solutions, {
replayPrevention: 'disabled',
});
ReplayStore-Interface
Bei Verwendung des remote-Modus muss dein replayStore das folgende Interface erfüllen:
interface ReplayStore {
consume(jti: string, expiresAt: number): Promise<boolean>;
}
| Parameter | Typ | Beschreibung |
|---|
jti | string | Die eindeutige ID des zu verifizierenden Challenge-Tokens |
expiresAt | number | Unix-Zeitstempel in Sekunden, zu dem dieses Token abläuft |
Gib true zurück, um die Verifizierung fortzusetzen (dies ist das erste Mal, dass dieses Token gesehen wird). Gib false zurück, um es als Wiederholung abzulehnen. Die Operation muss atomar sein – verwende einen einzelnen SET NX EX-Befehl in Redis statt einer separaten Check-then-Set-Logik, um Race Conditions unter gleichzeitiger Last zu vermeiden.