JWT - Wie richtig implementieren

  • Allgemein

Es gibt 2 Antworten in diesem Thema. Der letzte Beitrag () ist von sandiro.

    JWT - Wie richtig implementieren

    Hallo zusammen,

    Ich beschäftige mich gerade mit JSON Web Token (JWT) in Webapplikationen (mit PHP, spielt hier aber keine Rolle), komme beim Konzept selber aber nicht weiter. Ich freue mich auf eure Meinungen :)
    Nachfolgend mal was ich weiss oder zu wissen glaube und welche Ansätze aber auch Probleme es gibt. Korrigiert mich, wo ich falsch liege.


    1. Ziel von JWT
    Grundsätzlich könnte man ja allbekannte Sessions - ein Token in der Datenbank und auf dem Client - einsetzen.
    Durch den Einsatz von JWT sollen Probleme beim Load Balancing verhindert werden (Sync-Probleme) und es muss nicht mehr bei jedem Request auf die DB zugegriffen werden (zumindest nicht für die Authentifizierung).

    2. Grundsätzlicher Workflow
    Die Umsetzung erfolgt mittels einem Access Token (AT) und einem Refresh Token (RT). Der Access Token ist nur eine kurze Zeit gültig, der Refresh Token länger (oder ewig). Läuft der Access Token ab, wird er mit dem Refresh Token erneuert.

    Grundsätzlich läuft der Prozess dann wie folgt ab:
    1. Der User loggt sich mit User + PW ein
    2. Der Server generiert einen Refresh und Access Token und sendet diese an den Client
    3. Der User arbeitet mit der Web-App und sendet somit Anfragen an den Server, zusammen mit dem Access Token
    4. Der Server prüft ob der Access Token gültig ist (mit dem Secret Key) und ob der nicht abgelaufen ist
    5. Solange der Access Token gültig ist, können die Anfragen direkt beantwortet werden, ohne auf die DB zugreifen zu müssen.
    6. Nach einiger Zeit macht der User eine Abfrage mit einem abgelaufenen Access Token, der Server merkt das und unterbindet die Anfrage
    7. Der Client bittet mit dem noch gültigen Refresh Token um einen neuen Access Token und erhält diesen.
    8. Der Client kann (ohne Unterbruch) direkt mit dem neuen Access Token weiterarbeiten.


    FESTSTELLUNG 1: Gültigkeitsdauer des Access Token
    Ein sehr kurzlebiger Access Token, z.B. eine Minute oder weniger, würde zu wesentlich mehr HTTP-Requests führen (Request mit falschem AT, Request für neuen AT, Request mit neuem AT).
    Insofern muss der Access Code zum Beispiel eine Viertelstunde oder noch besser eine Stunde gültig sein. Nur jede Stunde mehr Anfragen zu haben, sollte vertretbar sein (gegenüber jede Minute).


    FESTSTELLUNG 2: Kein Zugriff auf DB bei Access Token
    Ein Access Token muss durch sich selbst validiert werden können und somit ohne Datenbankzugriff auskommen, ansonsten hat man ein Ziel von JWT nicht erreicht.


    PROBLEM 1: Logout verhindern wenn User am Arbeiten ist
    Wenn der Access Token abläuft, kann dieser mit dem Refresh Token erneuert werden - soweit kein Problem. Wenn hingegen der Refresh Token ausläuft, muss sich der User neu einloggen.
    Nehmen wir an, der Access Token hat eine Laufzeit von 60 Minuten und der Refresh Token von einer Woche. 55 Minuten vor Ablauf des Refresh Tokens beginnt der User zu arbeiten.
    Der User kann mit dem RT einen neuen AT für eine Stunde generieren. Nach einer Stunde ist der AT abgelaufen und weil mittlerweile auch der RT abgelaufen ist, wird er einfach ausgeloggt.
    Aus UX-Sicht sehr suboptimal, da er gerade am arbeiten war.

    Wie kann dies einfach und sauber gelöst werden?


    PROBLEM 2: Von allen Geräten abmelden
    Aus Sicherheitsgründen möchte sich der User von allen Geräten abmelden (eine Funktion die viele Dienste anbieten). Nehmen wir an, ein Angreifer gelang in Besitz des AT (und vielleicht auch RT) des Users.

    Da der Access Token jedoch ohne DB-Zugriff validiert wird, kann der Server den Zugriff nicht direkt verhindern. In diesem Fall den AT auf eine Blacklist zu setzen, hilft nur dann, wenn wir die Blacklist bei jedem Aufruf prüfen.
    Damit verstossen wir jedoch gegen das Ziel, nicht auf die Datenbank zugreifen zu wollen. Somit kann der Angreifer 60 Minuten lang auf den Account zugreifen.

    Um zu verhindern, dass ein Angreifer neue AT generieren kann, könnte man für die RT eine Blacklist einführen. Dadurch muss zwar auch auf die DB zugegriffen werden, allerdings nur alle 60 Minuten.
    Somit kann verhindert werden, dass der Angreifer länger als 60 Minuten auf den Account zugreifen kann - was aber eben auch schon zu lange ist.

    Wie kann eine "Von allen Geräten abmelden"-Funktion angeboten werden (die quasi sofort wirkt), ohne jedes mal auf eine DB zugreifen zu müssen?


    PROBLEM 3: Logout verhindern (Fortsetzung Problem 1)
    Um zu verhindern, dass der User automatisch ausgeloggt wird, müsste er einen neuen Refresh Token haben, damit er wieder Access Tokens beantragen kann.
    Ohne die Logindaten erneut einzugeben, müsste es also möglich sein, mit einem bestehenden (gültigen) RT einen neuen RT zu beantragen, der dann wieder eine Woche (oder auch kürzer) gültig ist.

    Was ist aber nun, wenn ein Angreifer meinen Refresh Token geklaut hat und damit einen neuen Refresh Token generiert?
    Wenn ich eine Blacklist führe, kenne ich den neuen Token des Angreifers nicht und kann ich ihn somit nicht sperren. Daher müsste ich eigentlich pro Login eine ID speichern und im RT hinterlegen. Der neue RT erhält dann dieselbe Login ID.
    Ich als User sperre dann die Login ID und er kann somit keine neuen Refresh Tokens generieren. Dann habe ich aber eher eine Whitelist als eine Blacklist.

    Also kann ich ja gleich eine Whitelist führen und somit all meine Refresh Tokens abspeichern. Wenn der Angreifer nun einen neuen RT beantragt (mit dem geklauten), kann ich beide RT sperren.
    Beim Load Balancing könnte dies aber wieder zu einem Problem führen, wenn der Angreifer weiterhin neue Tokens generieren kann.


    Ich könnte ohne Probleme nochmals so viel Text schreiben, aber ich denke ihr habt genug :) Und ich lasse euch mal zu Wort kommen.

    Gruss
    Sandro
    JWT scheint (habe mich bis eben nie damit beschäftigt, von daher nicht alles für bare Münze nehmen) vom Grundgedanken ein Protokoll zum sicheren und schlanken Austausch von Informationen zu sein. Authentifizierung ist dann wohl der üblichste Anwendungsfall.

    Das Verfahren mit Access und Refresh Token baut dann letztlich nur auf JWT auf, ist aber nicht Bestandteil des Protokolls. (Ähnlich wie TCP prinzipiell nur auf IP aufbaut, aber IP durch ein anderes Protokoll ersetzt werden könnte.)

    Unabhängig davon vielleicht noch als Ergänzung zu deinen Feststellungen:
    Der Access-Token kann ohne zentrale Stelle verifiziert werden. Im einfachsten Szenario spart man sich also die Datenbankabfrage und bei komplexeren Szenarien kann man auf lästige Sessionssynchronisation über mehrere Datenbanken verzichten. Das Problem an der Stelle ist (wie du schon bemerkt hast), wie der Token ungültig gemacht und der User abmeldet wird. Da es bei dem System kein Single Point of Truth gibt (bzw. der gerade eingespart werden soll) ist es nicht möglich, ein AT gezielt zu "revoken".
    Folglich wählt man eine relativ kurze Lebensdauer. Entsprechend kann man auch etwas mehr auf Performance als auf die Komplexität der involvierten Crypto-Funktionen achten.
    Wie lang das Token gültig sein sollte hängt dann letztlich an den Daten und Funktionen, die man damit ausführen kann.

    Zu Problem 1)
    Drei mögliche Lösungsansätze, die mir spontan einfallen (und wie ich im Nachgang lese, du auch zum Teil erwähnt hast):
    a) Refresh-Token hat längere Lebensdauer (bis hin zu unbegrenzt aka "Dauerhaft angemeldet bleiben")
    b) Refresh Token wird automatisch verlängert. Beispiel: Jedes mal wenn User ein neues AT beantragt (und das RT noch gültig ist) prüft das System, ob der RT in x Tagen/Stunden ablaufen wird und stellt ggf. ein neues bereit.
    c) Falls das RT ungültig ist, poppt beim user ein Login-Fenster hoch, er meldet sich neu an, bekommt neue Tokens und der letzte Request wird wiederholt. Ist Client-seitig aufwendiger und setzt voraus, dass das Token nicht für andere Zwecke (außer Authentifizierung) verwendet wird.

    zu Problem 2)
    Den AT kann man per Design nicht revoken. Hier hilft eigentlich nur eine kurze Lebenszeit oder dann doch wieder ein irgendwie geartetes zentrales System zum Session-Management.
    Ein "richtiges" abmelden, mit sofortiger Wirkung (lies: AT revoken), ist damit ebenso nicht möglich. Natürlich kann man den Token Client-seitig verwerfen und anderen Geräten noch eine Push-Notification schicken (sofern möglich), das gleiche zu tun. Ist aber realtiv aufwendig und ist keine Schutzmaßnahme gegen Dritte, die irgendwie an ein Gerät/AT gelangt sind.

    Den RT würde ich anders implementieren als den AT, nämlich als klassische Session. Lies: Der RT kann sich nicht selbst validieren, sondern steht in einer Datenbank an zentraler Stelle. So kann er auch ohne Probleme revoked werden, auch Geräteübergreifend anhand der User-ID. Eine extra Blacklist braucht es hier nicht. Die Vergabe von ATs muss dann immer über die zentrale Stelle erfolgen, aber aufgrund des geringeren Anfragevolumens sollte das nicht das Problem sein.

    zu Problem 3)
    siehe tlw. 2) Die spannende Frage bei dem Problem ist: Soll es möglich sein, sich von mehreren Geräten gleichzeitig anzumelden und im gleichen Zustand weiter arbeiten zu können?
    Falls nicht, kann man alle RTs des Benutzers revoken, wenn ein neuer RT erzeugt wird. Damit würde ein Angreifer nur noch mit dem AT Unfug treiben können.
    Danke für dein Feedback.

    Also für die beiden Tokens gilt somit:
    • AT ist selbstvalidierend, kurzlebig und nicht widerrufbar
    • RT braucht Datenbank zur Validierung, lebt länger und kann durch die DB auch widerrufen werden

    Zu Problem 1:
    Variante a funktioniert nur, wenn der RT für immer gültig ist, sich der User also nie einloggen muss.
    Variante b ist sicher eine gute Möglichkeit.
    Variante c finde ich ebenfalls sinnvoll (warum habe ich da bloss nie daran gedacht :) ). Somit könnte ein Angreifer keinen neuen RT generieren und der User müsste nur nochmals sein PW (+ evtl. Email) eingeben und verliert den Kontext nicht.

    Zu Problem 3:
    Beim Revoken eines Refresh Tokens müssten eigentlich gleich alle gelöscht werden, inklusive dem eigenen:

    Wenn ich einen Angreifer rauswerfen möchte, muss ich meinen aktiven RT entfernen (da er den gleichen nutzt). Wenn für mich automatisch ein neuer RT generiert wird, ist dies aber ein Problem:
    Wenn der Angreifer sich einloggt und die RT entfernt, werde ich rausgeworfen und für ihn wird ein neuer RT angelegt. Wenn ich mich dann neu einloggen will (und er schon drin ist), kann er mich gleich wieder rauswerfen.
    Insofern habe ich dann keine Möglichkeit mehr, ihn rauszuwerfen.

    Gruss
    Sandro