Was in einem JWT steckt — und was es nicht schützt
Veröffentlicht am 6.7.2026 · 14 Min. Lesezeit · Entwickler-Tools
Daniel Okonkwo — Front-end-Entwickler und Tech-Redakteur bei Allin
Web-Performance · Dateiformate
Anhand von 6 Quellen geprüft
Ein JSON Web Token besteht aus drei base64url-kodierten Zeichenketten, durch Punkte verbunden: Header, Payload, Signatur. Die Signatur beweist, dass die ersten beiden Teile von niemandem ohne den Schlüssel verändert wurden. Sie verbirgt sie nicht. Nimm dieses echte HS256-Token — eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFkYSBMb3ZlbGFjZSIsInJvbGUiOiJlZGl0b3IiLCJpYXQiOjE3ODY1NzkyMDAsImV4cCI6MTc4NjU4MjgwMCwiaXNzIjoiaHR0cHM6Ly9hbGxpbi5leGFtcGxlIiwianRpIjoiYTFiMmMzIn0.bu5qB6d_D3--WUj4b77tnh-wdH6FhKhLcbh2nvNmyl4 — und dekodiere den mittleren Abschnitt ganz ohne Schlüssel. Heraus kommt {"sub":"1234567890","name":"Ada Lovelace","role":"editor","iat":1786579200,"exp":1786582800,"iss":"https://allin.example","jti":"a1b2c3"}. Base64url ist eine Kodierung, keine Verschlüsselung. Der erste Teil nennt den Algorithmus, der dritte ist ein 43-Zeichen-Tag über die ersten beiden, und das ganze Token hat 264 Zeichen, die bei jeder Anfrage mitreisen. Alles, was du in eine JWT-Payload legst — E-Mail-Adressen, interne Kennungen, Berechtigungsflags — ist lesbar für den Browser, für jeden Proxy, der den Header protokolliert, und für jeden, der das Token von einem Bildschirm abliest. Daraus folgt zweierlei. Leg nichts Vertrauliches in eine Payload; nutze JWE, wenn der Inhalt wirklich verborgen sein muss — das ist eine andere Spezifikation. Und denk daran, dass die Signatur nicht widerrufen kann: Ein gestohlenes Token bleibt bis zu seinem exp gültig, weshalb kurzlebige Zugriffstoken mit einem widerrufbaren Refresh-Token gepaart werden.
Ein JWT ist signiert, nicht verschlüsselt. Wer das Token hat, kann die Nutzlast dekodieren und jeden Claim darin lesen. Hier ist ein echtes Token, ohne jeden Schlüssel dekodiert, dazu die drei Angriffe, die die Signatur abwehren soll, und das eine Problem, das sie nicht lösen kann.
Drei Teile, zwei Punkte
Jedes JWT hat dasselbe Gerüst: Header, Punkt, Payload, Punkt, Signatur. Der Header ist ein winziges JSON-Objekt, das den Algorithmus nennt — hier {"alg":"HS256","typ":"JWT"}, base64url-kodiert eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. Die Payload ist ein weiteres JSON-Objekt mit den Claims. Die Signatur wird über die ersten beiden kodierten Teile samt ihrem Punkt berechnet — deshalb darfst du sie nie umsortieren oder umformatieren: Genau diese Bytes wurden signiert.
RFC 7519 reserviert sieben Claim-Namen, und sie zu kennen erspart viel Neuerfindung. iss ist der Aussteller, sub das Subjekt, aud das vorgesehene Publikum, exp der Ablauf, nbf der Zeitpunkt, vor dem es nicht gilt, iat der Ausstellungszeitpunkt und jti eine eindeutige Token-Kennung. Die drei Zeit-Claims sind NumericDate-Werte: schlichte Sekunden seit der Unix-Epoche, keine Millisekunden — eine verlässliche Quelle für Fehler um den Faktor 1000 in JavaScript. Im obigen Token ist iat 1786579200 und exp 1786582800: eine Differenz von 3 600, also eine Stunde Lebensdauer.
Die Größe wächst schneller als erwartet. Diese Payload sind 137 Byte JSON und werden kodiert zu 183 Zeichen, denn Base64 kostet stets vier Ausgabezeichen je drei Eingabebytes — ein Aufschlag von 33 %. Das ganze Token hat 264 Zeichen und reist bei jeder einzelnen Anfrage mit. Wechsle auf RS256 mit einem 2048-Bit-Schlüssel, und allein die Signatur springt von 32 Rohbytes auf 256, also 342 base64url-Zeichen: Das Token verdoppelt sich ungefähr. Ein Berechtigungs-Array in die Payload zu stopfen ist der Weg, auf dem Teams an Header-Limits von Proxys stoßen.
Dekodier es selbst: die Payload liegt im Klartext
Kopiere den mittleren Abschnitt des Tokens — die Zeichenfolge zwischen den beiden Punkten — und dekodiere sie base64url. Kein Schlüssel, keine Bibliothek, keine Berechtigung. Zurück kommt {"sub":"1234567890","name":"Ada Lovelace","role":"editor","iat":1786579200,"exp":1786582800,"iss":"https://allin.example","jti":"a1b2c3"}. Jeder Claim im Klartext, samt Rolle. Wäre das eine E-Mail-Adresse, eine interne Kundennummer, eine Abostufe oder ein Feature-Flag gewesen, wäre es genauso sichtbar.
Die Verwirrung ist verständlich, denn das Token sieht aus wie Chiffrat. Ist es nicht. Base64url existiert, um beliebige Bytes durch Kanäle zu bringen, die nur einen eingeschränkten Zeichensatz dulden — URLs, Header, Dateinamen. Es gibt keinen Schlüssel, also nichts geheim zu halten und nichts zu brechen. Die Signatur schützt die Integrität: Ändert jemand die Payload, verifiziert der dritte Teil nicht mehr, und der Server weist das Token ab. Integrität ist nicht Vertraulichkeit, und ein JWT liefert nur Ersteres.
Die praktische Regel folgt unmittelbar. Behandle eine JWT-Payload wie ein öffentliches Schwarzes Brett, das zufällig manipulationssicher ist. Leg Kennungen hinein, keine Geheimnisse. Eine Benutzer-ID statt einer E-Mail; einen Rollennamen statt der Begründung dahinter; nichts, was du nicht außen auf einen Umschlag drucken würdest. Und speichere es entsprechend: Ein HttpOnly-, Secure-, SameSite-Cookie hält es von Seitenskripten fern, während localStorage es jedem Skript aushändigt, das es schafft, auf deiner Origin zu laufen.
Warum base64url und nicht base64
Standard-Base64 nutzt vierundsechzig Zeichen, die auf + und / enden, und füllt die Ausgabe mit = auf ein Vielfaches von vier auf. Alle drei sind URL- und Header-feindlich: + bedeutet in formularkodierten Daten ein Leerzeichen, / ist ein Pfadtrenner, und = ist in einem Querystring ein Schlüssel-Wert-Trenner. RFC 4648 definiert deshalb ein URL-sicheres Alphabet, das + durch -, / durch _ ersetzt und die Auffüllung ganz weglässt. Das ist base64url, und genau das nutzen alle Teile eines JWT.
Die Signatur im obigen Token zeigt es offen: bu5qB6d_D3--WUj4b77tnh-wdH6FhKhLcbh2nvNmyl4 enthält einen Unterstrich und drei Bindestriche. Zurück in Standard-Base64 lauten dieselben 32 Byte bu5qB6d/D3++WUj4b77tnh+wdH6FhKhLcbh2nvNmyl4=. Wenn du je einen JWT-Teil mit einer gewöhnlichen Base64-Funktion dekodierst und Müll bekommst, liegt es daran: Du musst erst - wieder zu + und _ wieder zu / machen und die Auffüllung ergänzen, falls der Dekoder darauf besteht.
Was die Signatur stoppt und was sie aushebelt
Ändere ein Zeichen der Payload, und die Signatur passt nicht mehr, denn HMAC-SHA-256 über die veränderte Zeichenkette header.payload liefert etwas völlig anderes. Die manipulierte Payload neu zu signieren erfordert das Geheimnis, das der Client nicht hat. So weit funktioniert es. Die historischen Fehlschläge sind keine Angriffe auf die Kryptografie, sondern auf den Prüfer, und beide Klassiker entspringen dem Vertrauen in den Header.
Der erste ist alg: none. RFC 7515 definiert ein ungesichertes JWS, dessen Header {"alg":"none"} lautet und dessen Signaturteil schlicht leer ist — das Token endet mit einem nackten Punkt. Bibliotheken, die alg aus dem Header lasen und danach verzweigten, akzeptierten ein solches Token und behandelten seine Claims als geprüft. Ein Angreifer schreibt die Payload nach Belieben um, setzt alg auf none, streicht die Signatur und spaziert hinein. Die Lösung ist nicht, die Zeichenkette none abzulehnen; sie ist, das Token gar nicht erst nach dem Algorithmus zu fragen. Der Prüfer weiß bereits, welchen Algorithmus und welchen Schlüssel er erwartet, und alles andere wird verworfen, bevor das Parsen weitergeht.
Der zweite ist die Verwechslung von HS256 und RS256, und er ist subtiler. Bei RS256 hält der Server einen privaten Schlüssel zum Signieren und veröffentlicht den passenden öffentlichen zum Prüfen. Nimmt ein Prüfer den Algorithmus aus dem Header, kann ein Angreifer alg auf HS256 setzen und das Token mit dem öffentlichen Schlüssel signieren, als wäre er ein HMAC-Geheimnis. Der Prüfer führt dann brav HMAC mit ebendiesem öffentlichen Schlüssel aus — den er hat, weil er öffentlich ist — und das Tag passt. Das funktioniert: HMAC-SHA-256 mit dem PEM-Text eines öffentlichen Schlüssels zu schlüsseln erzeugt ein Token, das ein naiver Prüfer akzeptiert. Den erwarteten Algorithmus festzunageln schließt beide Angriffe auf einmal, weshalb RFC 8725 das zur Kernempfehlung macht.
Ein dritter Fehler braucht überhaupt keine Kryptografie: die Signatur prüfen und dann vergessen, die Claims zu prüfen. Ein strukturell gültiges Token, dessen exp letzte Woche verstrichen ist, bleibt strukturell gültig. Ebenso eines von einem anderen Mandanten oder für ein anderes Publikum. Prüfe stets exp gegen die aktuelle Zeit mit einer kleinen Toleranz für Uhrenabweichung, iss gegen den erwarteten Aussteller und aud gegen deine eigene Kennung — die Signatur sagt, wer die Claims geschrieben hat, nicht ob sie noch für dich gelten.
Das Problem, das keine Signatur löst: der Widerruf
Der ganze Reiz eines JWT liegt darin, dass der Server nichts nachschlagen muss. Das Token trägt seine Claims und seinen Beweis selbst, jeder Knoten mit dem Schlüssel prüft es also in Mikrosekunden, ohne eine Datenbank anzufassen. Das ist zugleich seine strukturelle Schwäche, und beides ist untrennbar: Ein Server, der keinen Zustand konsultiert, kann nicht wissen, dass du den Nutzer vor fünf Minuten entlassen hast. Das Token bleibt gültig, bis exp erreicht ist, und die Spezifikation bietet keinen Weg, das zu verkürzen.
Die Standardantwort ist eine Aufteilung auf zwei Token. Das Zugriffstoken ist ein JWT mit bewusst kurzem exp — fünf bis fünfzehn Minuten ist die übliche Spanne — und wird bei jeder Anfrage zustandslos geprüft. Das Refresh-Token ist langlebig, undurchsichtig, serverseitig gespeichert und wird gegen ein neues Zugriffstoken getauscht, wenn das kurze abläuft. Der Widerruf greift am Refresh-Token, das zustandsbehaftet und damit annullierbar ist. Das Expositionsfenster schrumpft auf die Restlaufzeit des Zugriffstokens — genau das, was exp begrenzen soll.
Brauchst du schnelleren Widerruf, musst du Zustand wieder einführen — und zwar bewusst. Eine Sperrliste über den Claim jti erlaubt es, einzelne Token zu annullieren; die Einträge dürfen fallen, sobald das zugehörige exp verstrichen ist, die Liste bleibt also klein. Den Signaturschlüssel zu rotieren entwertet alle Token auf einen Schlag — das grobe Werkzeug bei Verdacht auf Schlüsselkompromittierung. Beides kostet dich einen Lookup, und an diesem Punkt darf man fragen, ob eine undurchsichtige Sitzungskennung in einem Cookie nicht von Anfang an einfacher gewesen wäre.
JWE ist die andere Spezifikation, keine JWT-Einstellung
Wenn die Claims wirklich verborgen sein müssen, lautet die Antwort JSON Web Encryption, definiert in RFC 7516. Ein JWE ist eine andere Serialisierung mit fünf statt drei Teilen — geschützter Header, verschlüsselter Schlüssel, Initialisierungsvektor, Chiffrat, Authentifizierungs-Tag — und liefert Vertraulichkeit und Integrität zusammen, weil es authentifizierte Verschlüsselung nutzt. Du kannst beides schachteln, ein JWT signieren und das Ergebnis verschlüsseln; RFC 7519 nennt das ein Nested JWT.
In der Praxis brauchen die meisten Teams kein JWE und sollten nicht zuerst danach greifen. Enthält die Payload etwas, das lieber niemand lesen soll, ist die übliche richtige Antwort, es aus der Payload zu nehmen. Ersetze den sensiblen Wert durch eine undurchsichtige Kennung, die der Ressourcenserver auflösen kann, und das Vertraulichkeitsproblem verschwindet mitsamt zusätzlicher Schlüsselverwaltung, zusätzlicher Bibliotheksfläche und zusätzlichen Fehlerarten. Greif zu JWE, wenn ein Token eine Partei durchqueren muss, die es weiterreicht, ohne es zu lesen — dafür wurde es entworfen.
Eine Checkliste für den Prüfer
Nagle den Algorithmus fest, bevor du überhaupt parst, und weise ein Token ab, dessen Header abweicht. Prüfe die Signatur mit einem zeitkonstanten Vergleich. Prüfe dann exp und, falls vorhanden, nbf gegen die aktuelle Zeit mit einer Abweichungstoleranz von höchstens einer Minute. Prüfe iss gegen genau den Aussteller, dem du vertraust, und aud gegen deine eigene Kennung. Erst danach solltest du die Anwendungs-Claims lesen — und selbst dann behandle role oder scope als Aussage des Ausstellers, nicht als letztes Wort: Der Ressourcenserver besitzt seine Autorisierungsentscheidungen weiterhin selbst.
| Teil | Inhalt | Größe in diesem Token | Ohne Schlüssel lesbar? | Von der Signatur abgedeckt? |
|---|---|---|---|---|
| Header | alg und typ — welcher Algorithmus signiert hat | 36 Zeichen | Ja, vollständig | Ja — der Prüfer darf alg aber nicht blind glauben |
| Payload | Die Claims: sub, iss, exp, iat, jti und was du ergänzt | 183 Zeichen für 137 Byte JSON | Ja — genau das übersehen die meisten | Ja — ohne Schlüssel nicht änderbar |
| Signatur | HMAC-SHA-256 über header.payload oder eine RSA/ECDSA-Signatur | 43 Zeichen für 32 Rohbytes | Ja, aber für sich genommen bedeutungslos | Sie ist die Signatur |
| Was fehlt | Vertraulichkeit und jede Möglichkeit, das Token vorzeitig zu annullieren | Dafür wird kein einziges Byte aufgewendet | Nicht zutreffend | Für Ersteres JWE, für Letzteres ein Refresh-Token oder eine Sperrliste |
Häufige Fragen
- Ist ein JWT verschlüsselt?
- Nein. Ein Standard-JWT ist signiert, was eine andere Garantie ist. Header und Payload sind base64url-kodiert, eine Kodierung ohne Schlüssel und ohne Geheimnis, wer das Token hat, kann sie also lesen. Die Payload des Beispieltokens in diesem Artikel ohne jeden Schlüssel zu dekodieren liefert {"sub":"1234567890","name":"Ada Lovelace","role":"editor","iat":1786579200,"exp":1786582800,"iss":"https://allin.example","jti":"a1b2c3"} — jeder Claim im Klartext. Was die Signatur dir bringt, ist Manipulationserkennung: Ändere ein Zeichen, und der dritte Teil verifiziert nicht mehr, der Server weist ab. Musst du den Inhalt wirklich verbergen, ist JSON Web Encryption (RFC 7516) die passende Spezifikation — ein eigenes Format mit fünf statt drei Teilen, kein Schalter an einem JWT. In den meisten Entwürfen ist es besser, vertrauliche Werte gar nicht in die Payload zu legen und stattdessen eine undurchsichtige Kennung zu transportieren.
- Kann jemand eine JWT-Payload ohne das Geheimnis ändern?
- Die Zeichen lassen sich ändern, aber das Ergebnis verifiziert nicht — sofern dein Prüfer korrekt geschrieben ist. Die Signatur wird über die exakte Zeichenkette header.payload berechnet, jede Änderung erzeugt also eine Abweichung, und das Token wird abgewiesen. Zwei Prüferfehler heben diesen Schutz auf. Der erste ist das Vertrauen in das Feld alg im Header: Ein Token mit {"alg":"none"} und leerem Signaturteil wurde historisch von Bibliotheken akzeptiert, die anhand des Headers verzweigten, sodass ein Angreifer die Payload nach Belieben umschreiben konnte. Der zweite ist die HS256/RS256-Verwechslung, bei der ein Angreifer ein asymmetrisches Token auf HMAC umstellt und es mit dem eigenen öffentlichen Schlüssel des Servers signiert, den der Prüfer dann als HMAC-Geheimnis verwendet; das erzeugt tatsächlich ein passendes Tag. Beides schließt dieselbe Maßnahme: Lege Algorithmus und Schlüssel im eigenen Code vor dem Parsen fest und weise alles Abweichende ab. RFC 8725 nennt dies als vorrangige Empfehlung.
- Wie melde ich einen Nutzer ab, wenn ein JWT nicht widerrufbar ist?
- Das Token auf dem Client zu löschen beendet die Sitzung für diesen Browser, und für viele Produkte reicht das wirklich. Es reicht nicht, wenn das Token kopiert worden sein könnte, denn ein signiertes Token bleibt bis zu seinem exp gültig, was der Client auch tut. Die Standardstruktur ist ein kurzlebiges Zugriffs-JWT — fünf bis fünfzehn Minuten — gepaart mit einem langlebigen, undurchsichtigen Refresh-Token auf dem Server. Die Abmeldung löscht das Refresh-Token, die Sitzung lässt sich also nicht erneuern und stirbt innerhalb der Restlaufzeit des Zugriffstokens. Brauchst du es schneller, füge bewusst Zustand hinzu: Eine Sperrliste über jti annulliert einzelne Token, und Einträge lassen sich löschen, sobald das zugehörige exp verstrichen ist, sie wächst also nie unbegrenzt. Den Signaturschlüssel zu rotieren tötet alle ausstehenden Token auf einmal und ist die richtige Antwort auf einen vermuteten Schlüsselverlust. Jede dieser Optionen bringt einen Lookup zurück — das ist der Preis des Widerrufs, den du wolltest.
- Wo soll ich ein JWT im Browser speichern?
- In einem Cookie mit HttpOnly, Secure und SameSite — in fast jedem Fall. HttpOnly hält das Token von JavaScript fern, eine Cross-Site-Scripting-Lücke irgendwo auf deiner Origin kann es also weder lesen noch fortschicken; Secure hält es von Klartextverbindungen fern; SameSite blockiert die Cross-Site Request Forgery, der Cookies dich sonst aussetzen. localStorage ist die übliche und schwächere Alternative, denn jedes Skript auf deiner Seite — auch eines aus einer kompromittierten Abhängigkeit — kann jeden Schlüssel darin lesen. Das Argument für localStorage lautet meist, das Token müsse bei Cross-Origin-API-Aufrufen von Hand angehängt werden, was Cookies umständlich machen; das ist eine echte Einschränkung, aber sie löst man besser mit einem Same-Origin-Proxy als damit, das Token skriptlesbar zu machen. Was du auch wählst: Halte die Payload frei von Sensiblem, denn die Ablage entscheidet, wer das Token stehlen kann, nicht wer es nach dem Diebstahl lesen kann.
- Wie lange sollte ein JWT gelten?
- Kurz genug, dass die nicht annullierbare Laufzeit eine akzeptable Exposition ist. Da ein signiertes Token bis exp gültig bleibt, egal was auf deiner Seite passiert, ist exp die volle Breite des Fensters, in dem ein gestohlenes Token noch funktioniert. Fünf bis fünfzehn Minuten sind die übliche Spanne für Zugriffstoken, und das Beispieltoken in diesem Artikel nutzt eine Stunde — iat 1786579200, exp 1786582800, eine Differenz von genau 3 600 Sekunden. Alles, was in Tagen gemessen wird, ist faktisch ein dauerhaftes Zugangsmittel mit angehängter Signatur. Die lange Laufzeit trägt stattdessen das Refresh-Token, und es darf lang sein, gerade weil es undurchsichtig, serverseitig gespeichert und widerrufbar ist. Zwei Umsetzungsdetails zählen. NumericDate-Werte sind Sekunden, keine Millisekunden — exp in JavaScript mit Date.now() zu vergleichen, ohne durch 1000 zu teilen, ist der klassische Fehler. Und sieh eine kleine Toleranz für Uhrenabweichung vor, in der Größenordnung von dreißig bis sechzig Sekunden, sonst weist ein Server, dessen Uhr dem Aussteller etwas vorausläuft, gelegentlich gültige Token ab.
- Sollte ich ein JWT oder ein einfaches Sitzungscookie nehmen?
- Für eine einzelne Anwendung mit einer Datenbank ist eine schlichte Sitzungskennung im Cookie meist die einfachere und stärkere Wahl. Sie ist eine zufällige, undurchsichtige Zeichenkette, verrät nichts, und Abmelden ist eine Zeilenlöschung mit sofortiger Wirkung. Das JWT verdient seine Komplexität, wenn die Prüfung dort stattfinden muss, wo der Sitzungsspeicher nicht ist — mehrere Dienste hinter einem Gateway, eine Fremd-API, die deine Identitätsaussagen annimmt, eine Edge-Funktion, die sich keinen Datenbank-Roundtrip leisten kann. Das ist ein echter architektonischer Vorteil und der Existenzgrund von JWTs. Nimm keines für einen Monolithen, der schon eine Sitzungstabelle hat, und sei ehrlich über den Tausch: Zustandslosigkeit kauft dir schnelle verteilte Prüfung und kostet dich den sofortigen Widerruf; beides geht nicht. Teams, die jeder Anfrage einen Sperrlisten-Lookup hinzufügen, haben die Komplexität des JWT bezahlt und seinen einzigen strukturellen Vorteil zurückgegeben.
Artikel, die dich interessieren könnten
Alle Ratgeber →Ähnliche Tools
Quellen
Hast du einen Fehler in diesem Artikel entdeckt?