Strategie für Refresh-Tokens in Authentifizierungssystemen von Node.js
Ermitteln, wie man in Node.js Refresh-Tokens entwerfen, drehen, widerrufen und sicher speichern kann, damit der Diebstahl von Tokens sowie das Abmelden tatsächlich wie erwartet funktionieren.
Sobald in einem Node.js-Projekt erstmals ein Anmeldefluss implementiert wird, scheint es, als wäre die Arbeit bereits nach kurzer Zeit erledigt.
Das Muster erscheint recht einfach: Der Benutzer gibt E-Mail-Adresse und Passwort ein, der Server überprüft die Angaben in der Datenbank, und falls sie übereinstimmen, wird ein JWT signiert und an den Client zurückgesendet. Dieses Token wird anschließend mit jeder nachfolgenden Anfrage übertragen. Auf den ersten Blick wirkt das wie ein vollständiges Authentifizierungssystem.
Doch bei genauerer Betrachtung taucht eine lange Liste von Fragen auf, die dieser einfache Ablauf nie beantwortet:
- Was passiert, wenn das Token abläuft?
- Soll der Benutzer jedes Mal von vorne anmelden?
- Falls ein Token gestohlen wird, wie lange kann der Angreifer es nutzen?
- Wie funktioniert das Abmelden eines Benutzers in der Praxis tatsächlich?
- Gibt es eine Möglichkeit, ein bereits ausgestelltes Token zu widerrufen?
„Ein JWT signieren und es später validieren“ beantwortet keine dieser Fragen. In der Regel ist das der Punkt, an dem man aufhören muss, einfach nur Tutorials zu kopieren und stattdessen wirklich verstehen muss, wie Refresh-Tokens funktionieren.
Warum Zugriffstoken nicht lange halten
Eine kurze Gültigkeitsdauer von Zugriffstoken ist keine Standardeinstellung, die jemand vergessen hat zu ändern – es handelt sich dabei um eine bewusste Designentscheidung.
Betrachten Sie, was passiert, wenn ein Zugriffstoken 30 Tage lang gültig bleibt und in die falschen Hände fällt: Der Angreifer hat dann einen ganzen Monat lang Zugriff auf das Konto dieses Benutzers. Das ist ein inakzeptabler Kompromiss. Deshalb sind Zugriffstoken in der Regel nur auf wenige Minuten und nicht auf Wochen begrenzt – eine kurze Lebensdauer verringert den Schadensradius, falls das Token jemals durchsickert.
Diese Gestaltungsoption bringt jedoch ihr eigenes Problem mit sich. Wenn ein Token alle 15 Minuten abläuft, muss der Benutzer dann jedes Mal sein Passwort erneut eingeben? Das ist offensichtlich nicht praktikabel. Refresh-Tokens existieren genau dazu, diese Lücke zu schließen.
Was macht ein Refresh-Token eigentlich?
Auf den ersten Blick sieht ein Refresh-Token aus wie jedes andere Token, doch seine Rolle im System unterscheidet sich völlig von der eines Zugriffstokens.
Der Ablauf funktioniert im Allgemeinen wie folgt:
- Der Benutzer melden sich an.
- Der Server gibt sowohl ein Zugriffstoken als auch ein Refresh-Token zurück.
- Das Zugriffstoken wird zur Abwicklung von API-Anfragen verwendet.
- Irgendwann läuft das Zugriffstoken ab.
- Der Client sendet das Refresh-Token an einen speziellen Refresh-Endpunkt.
- Falls der Server es erfolgreich validiert, gibt er ein völlig neues Zugriffstoken aus.
Das Konzept, das man sich hier einprägen sollte, ist, dass ein Refresh-Token niemals dazu dient, direkt die API-Endpunkte aufzurufen. Seine einzige Aufgabe besteht darin, ein neues Access-Token zu erhalten. Sobald man diese beiden Aufgaben mental voneinander trennt, fängt der Rest des Designs an, logisch zusammenzupassen.
Access-Token gegenüber Refresh-Token – nebeneinander
Ein Access-Token wird verwendet, um auf geschützte APIs zuzugreifen, während ein Refresh-Token ausschließlich dazu dient, ein neues Access-Token zu erhalten. Ein Access-Token hat eine kurze Gültigkeitsdauer; ein Refresh-Token hingegen eine längere. Ein Access-Token wird in nahezu jeder Anfrage mitgesendet, während ein Refresh-Token nur gelegentlich verwendet wird. Wenn ein Access-Token durchsickert, ist das problematisch, doch wenn ein Refresh-Token durchsickert, ist es noch schlimmer, da damit weiterhin neue Access-Tokens erstellt werden können. Ein Access-Token wird bei jeder API-Aufruf überprüft, während ein Refresh-Token nur durch spezielle Refresh- oder Session-Logik geprüft wird.
Eine häufig genannte Einteilung sieht vor, dass ein Zugriffstoken eine Gültigkeitsdauer von etwa 15 Minuten und ein Erneuerungstoken eine Gültigkeitsdauer von 7 Tagen hat, doch an diesen Zahlen ist nichts Magisches. Die richtigen Werte hängen vollständig davon ab, was Ihre Anwendung sowie Ihre Nutzer ertragen können.
Warum Erneuerungstokens mehr Respekt verdienen, als es auf den ersten Blick scheint
Ein Erneuerungstoken kann eine Sitzung so lange am Leben erhalten, wie es gültig ist. Wenn ein Angreifer eines in die Hände bekommt, erhält er nicht nur einen einzigen Zugriff – er kann weiterhin neue Zugriffstoken erstellen, bis dieses Erneuerungstoken abläuft oder widerrufen wird.
Diese Realität verändert die Art und Weise, wie mit dem Token umgegangen werden sollte. Es handelt sich dabei nicht einfach um weitere Anwendungsdaten; es verhält sich eher wie ein physischer Hausschlüssel. So zu vorgehen bedeutet, folgende Aspekte ernsthaft zu berücksichtigen:
- Ablauf der Gültigkeit
- Widerruf
- Austausch
Typischerweise ist dies der Punkt, an dem eine scheinbar einfache JWT-Einrichtung ihre Schwachstellen zeigt.
Rotation: Verhindern, dass ein Token ewig gültig bleibt
Ein Konzept, das die Gestaltung dieses Systems verändert, ist die Rotation von Refresh-Tokens. Anstatt ein einzelnes Refresh-Token unbegrenzt wiederverwenden zu können, gibt der Server jedes Mal ein neues aus, wenn das aktuelle erfolgreich verwendet wurde, und das alte wird sofort außer Gebrauch genommen.
Anstelle eines langfristig gültigen Zugangsschlüssels, der ewig in Umlauf ist, entsteht so eine Kette von Tokens, wobei jedes das vorherige ersetzt:
Token A wird verwendet, wodurch Token B ausgegeben wird, während A zurückgezogen wird. Anschließend wird Token B verwendet, wodurch Token C ausgegeben wird, während B zurückgezogen wird. Dieses Muster setzt sich endlos fort, wobei nur das neueste Token in der Kette jemals gültig ist.
Warum das tatsächlich hilft
Betrachten Sie einen Szenario, in dem ein Angreifer irgendwie eine Kopie von Token A erhält, während der legitime Benutzer es noch besitzt.
Falls der echte Benutzer es zufällig zuerst verwendet, wird Token A als bereits verwendet markiert und faktisch ungültig, wodurch an seiner Stelle Token B ausgegeben wird. Wenn der Angreifer später versucht, dasselbe Token A zu verwenden, erkennt der Server es als ein bereits verbrauchtes Token – was ein offensichtliches Warnsignal darstellt. Je strenger das System konfiguriert ist, kann diese Erkennung zur Aufhebung der gesamten Token-Kette, die mit dieser Sitzung verbunden ist, führen – und nicht nur des einzelnen kompromittierten Tokens.
Dies setzt Sie in eine weitaus stärkere Position als in einer Konfiguration, bei der derjenige, der den Token zuerst ergreift, faktisch dauerhaft die Kontrolle erhält.
Außer Kraft Setzen – weil das Ausloggen tatsächlich etwas bedeuten sollte
JWTs werden häufig als zustandslos beschrieben, und technisch gesehen stimmt das. Doch jedes reale System benötigt zumindest einen gewissen Zustand, und das Ausloggen ist der deutlichste Punkt, an dem dieses Bedürfnis zum Vorschein kommt.
Falls ein Benutzer auf „Ausloggen“ klickt und dadurch lediglich der Token vom Frontend gelöscht wird, bleibt der Token selbst weiterhin vollständig gültig, bis er von selbst abläuft. Der Server hat keinerlei Kenntnis davon, dass der Benutzer im eigentlichen Sinne „ausgeloggt“ wurde, weshalb er denselben Token weiterhin akzeptieren wird, falls dieser vor Ablauf der Gültigkeit erneut vorgelegt wird.
Refresh-Tokens bieten ein echtes Verfahren, um Sessions vom Server aus zu verfolgen und zu beenden. Vor einem Abmeldevorgang befindet sich das Refresh-Token einer Session im aktiven Zustand. Sobald die Abmeldung erfolgt ist, sollte dieses Token auf „revoked“ gesetzt werden, sodass alle nachfolgenden Versuche, es zur Erneuerung zu verwenden, fehlschlagen. So sieht eine echte Abmeldung aus – und nicht nur eine rein kosmetische.
Die Abmeldung sollte im Backend stattfinden, nicht nur im Frontend
Die vereinfachte Version der Abmeldung, die Anfänger oft umsetzen, besteht lediglich darin, das Token aus dem lokalen Speicher oder von dort zu löschen, wo der Client es gespeichert hat, und anschließend weiterzumachen.
Ein vollständigerer und ehrlicherer Abmeldeablauf folgt in der Regel einer Sequenz, die dieser ähnelt:
- Der Client sendet eine
POST /auth/logout-Anfrage - Der Server bestimmt, zu welcher Session diese Anfrage gehört
Der entscheidende Punkt, den hier hervorgehoben werden sollte, ist, dass das Abmelden im Grunde ein Sicherheitsereignis auf Serverseite ist. Wenn Ihre Implementierung des Abmeldens nur das bearbeitet, was beim Client gespeichert ist, haben Sie den Benutzer eigentlich gar nicht abgemeldet – Sie haben lediglich dafür gesorgt, dass Ihre Benutzeroberfläche vergisst, dass dieser Benutzer existiert hat.
Entscheidung darüber, wo dieser Token tatsächlich gespeichert werden soll
Die Wahl des Speicherorts hat größere Bedeutung, als es auf den ersten Blick scheint. Für browserbasierte Anwendungen ist ein gängiger Ansatz, den Refresh-Token in eine Cookie zu platzieren, die mit einigen spezifischen Attributen konfiguriert ist:
HttpOnly, das verhindert, dass clientseitiges JavaScript ihn direkt lesen kannSecure, das es beschränkt, sodass er nur über HTTPS übertragen werden kann
SameSite, das die Anfälligkeit für bestimmte Arten von Cross-Site-Angriffen verringertAber keine dieser Einstellungen macht ein Cookie automatisch unangreifbar. Sie müssen weiterhin die CSRF-Schutzmaßnahmen, die Abgrenzung durch Domain und Pfad, die Handhabung des Session-Ablaufs sowie die Wechselwirkung beim Abmelden berücksichtigen. Es gibt kein einziges Speichermuster, das Sie aus einem Tutorial übernehmen und ohne Anpassung an die Architektur Ihres eigenen Systems verwenden können.
Was passiert, wenn der Refresh-Token dennoch durchsickert
Genau dieses Szenario hat die Notwendigkeit der Regelmäßigen Erneuerung deutlich gemacht.
Falls sowohl der legitime Benutzer als auch ein Angreifer irgendwie in den Besitz desselben Erneuerungstokens gelangen und Ihr System es zulässt, dass dieses Token unbegrenzt wiederverwendet wird, gibt es tatsächlich keine Möglichkeit, zwischen den beiden Parteien zu unterscheiden. Aus Sicht des Servers erscheinen beide Anfragen gleichermaßen legitim.
Durch die Implementierung einer Rotation wird das Token nach seiner ersten Verwendung sofort aus dem Umlauf genommen. Wenn daher derselbe Token ein zweites Mal vorgelegt wird, handelt es sich um verhaltensmäßig Abweichendes, das als Signal fungiert. Ein richtig konzipiertes System kann diese wiederholte Verwendung als Warnsignal werten und entsprechend reagieren – sei es durch die Aufhebung der Sitzung, das Markieren des Kontos oder die Anwendung einer Strategie, die zu Ihrer Risikotoleranz passt.
Dies ist tatsächlich der grundlegende Grund, warum Erneuerungstokens als Sicherheitsproblem im Design betrachtet werden sollten und nicht einfach durch die Erstellung eines weiteren JWT gelöst werden können.
Auch Refresh-Tokens müssen ablaufen
Das wird leicht übersehen, aber auch Refresh-Tokens sollten nicht dauerhaft gültig sein. Ohne Ablaufdatum wird ein gestohlenes Refresh-Token faktisch zu einer Hintertür, die niemals geschlossen wird.
Ein gängiger Ausgangspunkt sind beispielsweise 15 Minuten für Access-Tokens in Kombination mit 7 Tagen für Refresh-Tokens, wobei die genauen Werte, die Sie wählen, Ihrer eigenen Risikotoleranz entsprechen sollten und nicht den Zahlen aus dem ersten Tutorial, das Ihnen unterkommt.
Es lohnt sich, eine klare Unterscheidung zwischen der Lebensdauer der Tokens und der Lebensdauer der Sitzungen im Kopf zu behalten, da es sich um unterschiedliche Konzepte handelt. Eine Sitzung kann durch wiederholte Rotationsschleifen über einen längeren Zeitraum aktiv bleiben, obwohl jedes einzelne Token nur für eine kurze Zeitspanne gültig ist.
Fehler, die man vermeiden oder beachten sollte
- Zugriffstoken, die zu lange gültig sind, was den Schaden erhöht, falls sie kompromittiert werden
- Auffrischungstoken ohne jeglichen Ablaufzeitpunkt, was eine dauerhafte Zugangsmöglichkeit schafft
- Vollständiges Auslassen der Rotation, wodurch Diebstähle viel schwerer aufzudecken sind
- Fehlen eines Widerrufsmechanismus, der es nicht ermöglicht, eine Sitzung vor ihrem natürlichen Ablauf zu beenden
- Das Abmelden als etwas betrachten, das nur auf der Frontend-Seite stattfindet
- Nachlässigkeit bei sensiblen Daten, wodurch Token in Logs, URLs oder im Client-Speicher landen, wo sie nicht hingehören
- Ignorieren der Erkennung von Wiederverwendung, da das Rotieren von Token ohne Überprüfung auf Wiederverwendung keinen nennenswerten Schutz bietet
Der Auffrischungsprozess wirklich testen
Die Authentifizierung verdient genauso umfassende Tests wie jeder andere kritische Ablauf in Ihrer Anwendung. Hier ist eine grundlegende Abfolge, die man durchführen sollte:
- Melden Sie sich an und überprüfen Sie, ob Sie sowohl ein Zugriffstoken als auch ein Erneuerungstoken erhalten
- Rufen Sie eine geschützte Route mit einem gültigen Zugriffstoken auf und überprüfen Sie, ob Sie eine
200 OK-Antwort erhalten - Rufen Sie eine geschützte Route mit einem abgelaufenen Zugriffstoken auf und überprüfen Sie, ob Sie eine
401-Antwort erhalten - Rufen Sie den Erneuerungspunkt auf und überprüfen Sie, ob Sie ein neues Zugriffstoken erhalten (sowie ein neues Erneuerungstoken, falls die Rotation aktiviert ist)
- Versuchen Sie, das bereits erneuerte alte Erneuerungstoken erneut zu verwenden und überprüfen Sie, ob es abgelehnt wird
- Melden Sie sich aus und versuchen Sie anschließend die Erneuerung mit dieser nun ungültigen Sitzung und überprüfen Sie, ob auch dies abgelehnt wird
Das Aufspüren von Problemen in dieser Phase ist weitaus kostengünstiger als deren Entdeckung nach dem Veröffentlichungszeitpunkt der Anwendung.
Das Gesamtbild
Login
↓
Access Token + Refresh Token issued
↓
API requests using Access Token
↓
Access Token expires
↓
Refresh Token sent to refresh endpoint
↓
Server validates session
↓
Refresh Token rotated
↓
New Access Token issued
↓
API requests continue
Falls die Validierung zu irgendeinem Zeitpunkt in dieser Erneuerungssequenz fehlschlägt – das Token ist abgelaufen, widerrufen oder einfach ungültig – erfolgt die Antwort mit dem Code 401, und der Benutzer muss von vorne wieder einloggen. Diese Trennung zwischen „kurzfristiger Berechtigung zum Aufruf der API“ und „längerfristiger Berechtigung, eingeloggt zu bleiben“, ist im Grunde die Kernidee hinter dieser gesamten Konfiguration, zusammengefasst in einem Satz.
Prülliste vor der Produktion
Token-Design
- Ablaufen Zugriffstoken tatsächlich schnell?
- Haben Erneuerungstoken ihre eigene Ablaufzeit?
- Sind Erneuerungstoken nur für den spezifischen Erneuerungs-Endpunkt zugelassen und nicht für beliebige Routen einsetzbar?
- Vermeiden Token-Payloads es, mehr Informationen zu enthalten, als unbedingt notwendig ist?
Sicherheit
- Ist HTTPS überall erforderlich?
Sitzungsbearbeitung
- Kann ein Refresh-Token auf Anfrage widerrufen werden?
- Findet das Abmelden auf dem Server statt und nicht nur beim Client?
- Ist tatsächlich eine Token-Rotation implementiert?
- Kann man erkennen, wenn ein Token erneut verwendet wird?
Testabdeckung
- Ein gültiger Refresh-Token führt zum Erfolg
- Ein abgelaufener Refresh-Token führt zum Scheitern
- Ein widerrufener Refresh-Token führt zum Scheitern
- Ein fehlerhaft formatierter oder ungültiger Refresh-Token führt zum Scheitern
- Ein zuvor ausgetauschter Token führt zum Scheitern
- Ein korrektes Abmelden beendet die richtige Sitzung
Was letztendlich zählt
Die Authentifizierung geht nicht nur darum, die Identität einer Person zum Zeitpunkt des Anmeldens zu überprüfen. Es geht vielmehr darum, festzulegen – und anschließend durchzusetzen –, wie lange dieses Vertrauen danach bestehen soll.
JWTs machen den Teil „Überprüfen, wer Sie sind“ einfach. Was sie jedoch nicht kostenlos bieten, sind Session-Management, Revokation, ein ordnungsgemäßes Abmelden, Schutz vor gestohlenen Tokens, Erkennung von Wiederverwendungen, sicheres Speichern oder angemessene Ablaufzeiten. Jeder dieser Aspekte ist eine bewusste Entscheidung, die Sie selbst treffen müssen. Je größer und beliebter Ihre Anwendung wird, desto wichtiger werden diese Entscheidungen tatsächlich.
Was kommt als Nächstes
Nun, da Zugriffs- und Erneuerungstokens endlich sinnvoll sind, ist der nächste Bereich, den es zu untersuchen gilt, das Session-Management in größerem Maßstab:
- Wie soll mit einem Benutzer umgegangen werden, der sich auf fünf verschiedenen Geräten gleichzeitig angemeldet hat?
Die Erstellung der Anmelderoute selbst kann zehn Zeilen Code erfordern. Die Entwicklung einer Authentifizierung, der man wirklich vertrauen kann, erfordert deutlich mehr Aufwand.
Verwandte Literatur
- Der wahre Engpass in einem langsamen Node.js-Endpunkt finden — Lernen Sie eine systematische Methode, um die Latenz im Backend entlang des Anfragenpfades zu verfolgen – vom Node.js-Code bis hin zu Datenbankabfragen – unter Verwendung von Zeitmessung und EXPLAIN ANALYZE.
- Produktionsreife Fehlerbehandlung in Node.js-Anwendungen entwickeln — Erfahren Sie, wie Sie Node.js-Fehler kategorisieren, eine benutzerdefinierte Fehlerhierarchie entwerfen, die asynchrone Fehlerbehandlung zentralisieren und Stacktraces schützen, um die Zuverlässigkeit in der Produktion zu gewährleisten.