JSON Web Tokens sind beliebt, weil eine API sie prüfen kann, ohne jemanden aufzurufen: Signatur verifizieren, Claims lesen, fertig. Genau diese Eigenschaft ist auch die Quelle der meisten JWT-Probleme. Ein Token, das du nicht nachschlagen kannst, ist ein Token, das du nicht zurückrufen kannst, und Teams merken das zum ersten Mal, wenn ein Laptop gestohlen wird oder jemand das Unternehmen verlässt.
Dieser Beitrag geht einen vollständigen JWT-Authentifizierungsablauf mit Access Tokens, Refresh Tokens und einem Sequenzdiagramm durch und behandelt dann die Prüfungen, die jede API braucht, wo Tokens im Browser liegen sollten, die Rotation von Refresh Tokens, Schlüsselrotation und die realistischen Optionen für den Widerruf.
Was ein JWT ist und was nicht
Ein JWT (RFC 7519) ist ein kompakter, URL-sicherer String aus drei base64url-Teilen, getrennt durch Punkte: ein Header, ein Payload mit Claims und eine Signatur. Dekodiert sieht ein Access Token so aus:
{ "alg": "RS256", "kid": "2026-10", "typ": "at+jwt" }
{
"iss": "https://auth.example.com",
"sub": "user_8412",
"aud": "orders-api",
"scope": "orders:read orders:write",
"iat": 1793606400,
"exp": 1793607300,
"jti": "a3f1c9e2"
}
Drei Punkte sorgen für die meisten Missverständnisse:
- Ein signiertes JWT ist nicht verschlüsselt. Jeder, der das Token hat, kann den Payload lesen. Pack keine Geheimnisse oder personenbezogenen Daten hinein, die der Inhaber nicht sehen soll.
- JWT ist ein Format, kein Protokoll. Wie ein Client ein Token bekommt, ist anderswo definiert, meist durch OAuth 2.0 und OpenID Connect. Das OAuth-2.0-Ablaufdiagramm behandelt diesen Teil.
- Nicht jedes Token in einem Ablauf ist ein JWT. Access Tokens sind es oft; Refresh Tokens sind oft opake Zufallsstrings, die nur der Auth-Server nachschlagen kann.
Die registrierten Claims, die dir in jedem Token begegnen:
| Claim | Bedeutung | Wer prüft ihn |
|---|---|---|
iss |
Issuer, der Auth-Server, der das Token signiert hat | API |
sub |
Subject, meist die Benutzer-ID | API, für die Autorisierung |
aud |
Audience, die API, für die das Token bestimmt ist | API |
exp, nbf, iat |
Ablauf, Gültigkeitsbeginn und Ausstellungszeit, in Sekunden seit der Epoche | API |
jti |
Eindeutige Token-ID, nützlich für Denylists | API oder Gateway |
Der Ablauf Schritt für Schritt
- Anmeldung. Der Benutzer authentifiziert sich beim Auth-Server, typischerweise über den OAuth-Authorization-Code-Flow mit PKCE.
- Token-Antwort. Der Auth-Server liefert einen kurzlebigen Access Token (ein JWT, sagen wir 15 Minuten) und einen langlebigen Refresh Token.
- API-Aufruf. Die App schickt bei jedem Request
Authorization: Bearer <access token>mit. - Lokale Prüfung. Die API verifiziert die Signatur mit dem öffentlichen Schlüssel des Auth-Servers, den sie einmal vom JWKS-Endpunkt geholt und gecacht hat, und prüft dann die Claims. Kein Aufruf des Auth-Servers pro Request.
- Ablauf. Nach 15 Minuten antwortet die API mit
401 Unauthorized. - Refresh. Die App schickt den Refresh Token an den Token-Endpunkt des Auth-Servers und bekommt einen neuen Access Token und, mit Rotation, einen neuen Refresh Token.
- Erneuter Versuch. Die App wiederholt den fehlgeschlagenen Request mit dem neuen Access Token.
Schritte 3 und 4 sind der Grund, warum Teams JWTs wählen: Tausende API-Requests und kein gemeinsamer Session-Store. Schritte 5 und 6 sind der Preis, und dort steckt der Großteil der Sicherheitsarbeit.
Ein Token prüfen: die Checkliste
Jede API, die JWTs akzeptiert, sollte dieselben Prüfungen in dieser Reihenfolge durchführen:
- Nur die erwarteten Algorithmen zulassen. Nimm den
alg-Header nie für bare Münze. RFC 8725 beschreibt die zwei klassischen Angriffe:algaufnonegesetzt und ein RS256-Token, das in HS256 umgeschrieben wird, damit eine naive Bibliothek es mit dem öffentlichen Schlüssel als HMAC-Geheimnis verifiziert. - Den Schlüssel per
kidaus dem gecachten JWKS wählen und das Set neu laden, wenn ein unbekannterkidauftaucht, höchstens einmal pro kurzem Intervall. - Die Signatur verifizieren.
issundaudprüfen. Ein Token, das für eine andere API ausgestellt wurde, muss hier scheitern.expundnbfprüfen, mit einer kleinen Uhrentoleranz wie 60 Sekunden.- Den Token-Typ prüfen. Access Tokens nach RFC 9068 tragen
typ: at+jwt, was verhindert, dass ein ID Token als Access Token wieder eingespielt wird. - Dann autorisieren: Scopes, Rollen und Eigentum an der Ressource.
Mit der Bibliothek jose in Node.js ist der Großteil der Liste ein einziger Aufruf:
import { createRemoteJWKSet, jwtVerify } from 'jose'
const JWKS = createRemoteJWKSet(new URL('https://auth.example.com/.well-known/jwks.json'))
export async function verifyAccessToken(token: string) {
const { payload } = await jwtVerify(token, JWKS, {
issuer: 'https://auth.example.com',
audience: 'orders-api',
algorithms: ['RS256'],
typ: 'at+jwt',
clockTolerance: 60,
})
return payload // scopes and sub are checked by the caller
}
Wie lange sollte ein Access Token leben?
Die Laufzeit ist das Fenster, in dem ein gestohlenes Token funktioniert und nicht zurückgerufen werden kann. Kürzer heißt sicherer und mehr Refresh-Traffic. Für einen Benutzer, der einen achtstündigen Arbeitstag lang aktiv ist:
| Laufzeit des Access Tokens | Gefährdung bei Diebstahl | Refreshes pro Benutzer und Tag |
|---|---|---|
| 5 Minuten | 5 Minuten | 96 |
| 15 Minuten | 15 Minuten | 32 |
| 1 Stunde | 1 Stunde | 8 |
| 24 Stunden | 24 Stunden | 1 |
Die meisten Teams landen bei Access Tokens zwischen 5 und 15 Minuten. Ein Access Token mit einem Tag Laufzeit ist ein Session-Cookie, das du nicht widerrufen kannst.
Rotation von Refresh Tokens
Ein Refresh Token lebt Tage oder Wochen, also ist ein geleaktes Refresh Token schlimmer als ein geleakter Access Token. RFC 9700, die Best Current Practice für OAuth-Sicherheit, verlangt für Refresh Tokens öffentlicher Clients wie Browser- und Mobile-Apps einen von zwei Schutzmechanismen:
- Absendergebundene Refresh Tokens, gebunden an einen Schlüssel, den der Client hält, per Mutual TLS oder DPoP.
- Rotation: Jeder Refresh liefert einen neuen Refresh Token und macht den alten ungültig, während sich der Auth-Server die Kette merkt. Nutzen ein Angreifer und der echte Client dasselbe Token, legt einer von beiden ein ungültiges Token vor, und der Server widerruft den aktiven Refresh Token, was die Kette für beide beendet.
Rotation macht aus einem gestohlenen Refresh Token statt eines wochenlangen Problems eines, das beim nächsten Refresh des legitimen Clients endet. Der Preis: Der Benutzer muss sich neu anmelden, und dein Token-Endpunkt muss zwei Tabs, die gleichzeitig refreshen, verkraften, ohne die Wiederverwendungserkennung auszulösen, meist mit einer kurzen Kulanzfrist.
Wo Tokens im Browser liegen sollten
| Speicher | Für JavaScript lesbar | Hauptrisiko | Hinweise |
|---|---|---|---|
localStorage |
Ja | Jedes XSS kann Tokens stehlen | Überlebt Reloads; für Refresh Tokens meiden |
| Nur im Speicher | Ja, solange die Seite lebt | XSS während der Session | Geht beim Reload verloren; beim Start refreshen |
HttpOnly-Cookie |
Nein | CSRF | Secure, SameSite und CSRF-Tokens für zustandsändernde Requests nutzen |
| Backend for Frontend | Nein; Tokens bleiben auf dem Server | Serverseitiges Session-Handling | Der Browser hält nur ein Session-Cookie |
Der IETF-Entwurf zu OAuth für browserbasierte Anwendungen beschreibt das Backend-for-Frontend-Muster als die Option, die Tokens vollständig von JavaScript fernhält. Wenn du Tokens im Browser hältst, leg den Access Token in den Speicher und den Refresh Token in ein HttpOnly-Cookie, das auf den Token-Endpunkt beschränkt ist.
Widerruf: was wirklich funktioniert
Du kannst ein JWT nicht entsignieren. Du kannst nur dafür sorgen, dass die API es nicht mehr akzeptiert, und jede Option kostet etwas Zustandslosigkeit:
| Strategie | Wie sie funktioniert | Kosten |
|---|---|---|
| Kurze Laufzeit | Tokens sterben in Minuten von selbst | Keine; das Gefährdungsfenster bleibt |
| Refresh Token widerrufen | Der nächste Refresh scheitert, der Zugriff endet beim nächsten Ablauf | Ein Lookup am Token-Endpunkt, der ohnehin Zustand hat |
Denylist per jti |
Gateways lehnen gelistete IDs bis zu ihrem exp ab |
Ein gemeinsamer Cache, bei jedem Request geprüft |
| Stichtag pro Benutzer | Tokens ablehnen, die vor einem gespeicherten Zeitstempel für diesen Benutzer ausgestellt wurden | Ein Lookup pro Benutzer, cachebar |
| Introspection | Die API fragt den Auth-Server zu jedem Token (RFC 7662) | Ein Netzwerkaufruf; nur für Hochrisiko-Endpunkte nutzen |
Eine praxistaugliche Kombination: Access Tokens von 15 Minuten oder weniger, rotierende Refresh Tokens, die bei Logout und Passwortänderung widerrufen werden, und ein Stichtag pro Benutzer für den seltenen Fall „überall sofort abmelden“.
Signaturschlüssel rotieren
Schlüssel leaken und Algorithmen altern, also plane die Rotation vom ersten Tag an. Veröffentliche den neuen öffentlichen Schlüssel etwa einen Tag, bevor du damit signierst, im JWKS, damit jeder API-Cache ihn hat. Stell das Signieren auf den neuen kid um. Behalte den alten Schlüssel im JWKS, bis das letzte damit signierte Token abgelaufen ist, und entferne ihn dann. Eine API, die bei einem unbekannten kid neu lädt, übersteht eine Notfallrotation ohne Neustart.
Das JWT-Ablaufdiagramm
Diagramminhalt
- Browser app
- Auth server
- Orders API
- access token expired
- 1. Sign in (OAuth code flow + PKCE)
- 2. Access token (JWT, 15 min) + refresh token
- 3. GET /orders, Authorization: Bearer JWT
- GET /.well-known/jwks.json (once, then cached)
- public keys
- 4. 200 OK
- 5. GET /orders (expired JWT)
- 6. 401 Unauthorized
- 7. POST /token (refresh token)
- 8. New token pair, old refresh token invalidated
- 9. Retry GET /orders with new JWT
- 10. 200 OK
So liest du es:
- Drei Lebenslinien. Die Browser-App links, der Auth-Server in der Mitte und die Orders-API rechts. Die Zeit läuft nach unten.
- Login und Token-Antwort oben: Die App bekommt einen Access Token und einen Refresh Token.
- Der Happy Path: der Bearer-Request an die API und die Antwort. Die API prüft das Token mit gecachten JWKS-Schlüsseln und ruft den Auth-Server nie auf.
- Das opt-Fragment deckt den Ablauf ab:
401, Refresh, ein neues Token-Paar mit ungültig gemachtem altem Refresh Token und der wiederholte Request. - Der Markdown-Block zeigt das dekodierte Token und die Prüfungen.
Auth-Abläufe zeichnen, die Reviewer prüfen können
- Benenne jedes Token an jedem Pfeil, mit Typ und Laufzeit: „Access Token (JWT, 15 min)“. Unklarheit darüber, welches Token wohin reist, versteckt echte Bugs.
- Zeig, wo geprüft wird. Setz eine Notiz an die Lebenslinie, die die Signatur verifiziert, und schreib dazu, woher ihre Schlüssel kommen.
- Zeichne die Fehlerpfade. Ablauf, Wiederverwendung von Refresh Tokens und Logout gehören in Fragmente; dort beginnen die meisten Incidents.
- Halte Geheimnisse aus dem Bild. Nutze Platzhalterwerte; Diagramme werden weiter geteilt, als du erwartest.
Das Sequenzdiagramm-Beispiel behandelt Lebenslinien und Fragmente im Detail, und das Zero-Trust-Architekturdiagramm zeigt, wo Token-Prüfungen in einem umfassenderen Zugriffsmodell sitzen.
In Archboard nachbauen
- Melde dich mit Google oder GitHub an und leg ein Diagramm an.
- Füge drei Lebenslinie-Nodes aus dem Sequenz-Pack hinzu: Browser app, Auth server und Orders API.
- Zeichne die Nachrichten als Kanten zwischen den Lebenslinien, von oben nach unten: Login, Token-Antwort, den Bearer-Request und seine Antwort. Beschrifte jede mit dem Token, das sie trägt.
- Füge ein Kombiniertes Fragment aus dem Sequenz-Pack mit dem Operator
optum den Ablaufpfad hinzu:401, Refresh-Request, neues Token-Paar, erneuter Versuch. - Füge einen Markdown-Block aus dem Basis-Pack mit dem dekodierten Token und der Checkliste hinzu.
Das Beispiel nutzt 5 Diagramm-Nodes und bleibt damit im Free-Plan.
FAQ
Sind JWTs verschlüsselt? Signierte JWTs nicht. Der Payload ist base64url-kodiert und für jeden lesbar, der das Token hat. Für verschlüsselte Tokens gibt es JSON Web Encryption, aber die meisten Access Tokens sind nur signiert.
Sollte ich ein JWT in localStorage speichern? Für alles Langlebige: nein. Jeder Cross-Site-Scripting-Bug kann localStorage lesen. Halte Access Tokens im Speicher und Refresh Tokens in einem HttpOnly-Cookie, oder halte die Tokens in einem Backend for Frontend.
Wie melde ich einen Benutzer ab, wenn JWTs zustandslos sind? Widerrufe den Refresh Token, damit keine neuen Access Tokens ausgestellt werden, und halte Access Tokens kurz. Für sofortige Wirkung ergänze einen Stichtag pro Benutzer oder eine jti-Denylist, die deine API prüft.
Was ist der Unterschied zwischen einem ID Token und einem Access Token? Ein ID Token sagt dem Client, wer sich angemeldet hat, und ist für den Client bestimmt. Ein Access Token ist für eine API bestimmt. APIs sollten ID Tokens ablehnen; die Prüfungen von typ und aud machen das explizit.
Sollte ich HS256 oder RS256 verwenden? HS256 nutzt ein gemeinsames Geheimnis, also kann jeder Service, der Tokens prüft, auch welche ausstellen. Mit RS256 oder ES256 prüfen APIs mit einem öffentlichen Schlüssel, während nur der Auth-Server den privaten Schlüssel hält; das ist der sicherere Standard für mehr als einen Service.