Authentication

JWT

Ein JSON Web Token ist ein signierter, URL-sicherer String, der Claims über einen Benutzer transportiert. Kompakt und eigenständig, lässt er sich hervorragend skalieren, ist aber gleichzeitig anfällig für Implementierungsfehler.

intermediate15 min readUpdated 16. Sept. 2026
verify.ts
ts
// verify.ts
import { jwtVerify, type JWTPayload } from "jose";

const secret = new TextEncoder().encode(process.env.JWT_SECRET);

export async function verify(token: string): Promise<JWTPayload> {
  const { payload } = await jwtVerify(token, secret, {
    issuer: "https://auth.example.com",
    audience: "api.example.com",
    algorithms: ["HS256"],
  });
  return payload;
}
Erste Spec
2015
Encoding
Base64url
Standard-Algorithmus
HS256
Asymmetrische Option
RS256 / ES256
Verschlüsselte Variante
JWE

Warum es wichtig ist

Die Vorteile von JWT

Eigenständige Anmeldedaten

Jeder Claim, den ein Service benötigt, reist im Token mit. Die Validierung ist somit eine lokale Operation ohne Datenbank-Roundtrip im kritischen Pfad.

Manipulationssicher durch Design

Die Signatur umfasst Header und Payload. Die Änderung eines einzigen Zeichens macht das Token ungültig und die Verifizierung schlägt sofort fehl.

Ideal für Rotation

Die Kombination aus kurzlebigen Access Tokens und rotierenden Refresh Tokens begrenzt den Schaden bei einem Leak und ermöglicht einen Weg zur Revocation.

Das Gesamtbild

Die drei Ebenen eines Tokens

Ein Header benennt den Algorithmus, ein Payload transportiert Claims und eine Signatur versiegelt beides gegen Manipulationen.

Header

Deklarieren

Ein kleines JSON-Objekt, das den Signier-Algorithmus und die Key-ID benennt, damit Verifizierer während der Rotation den richtigen Schlüssel finden.

Payload

Transportieren

Ein JSON-Objekt mit registrierten und benutzerdefinierten Claims wie sub, exp, iss, aud, scope und role.

Signature

Versiegeln

Ein kryptografischer MAC oder eine Signatur über dem kodierten Header und Payload, die das Token manipulationssicher macht.

HTML5 auf einen Blick

Ein Blick ins Token

Header

alg, typ und kid beschreiben, wie das Token signiert wurde.

Claims

Registrierte Namen wie iss, sub, aud und exp sowie eigene Felder.

Signature

HMAC- oder RSA/ECDSA-Ausgabe, die bei der Verifizierung neu berechnet und verglichen wird.

Expiry

Der exp-Claim begrenzt das Zeitfenster, in dem ein gestohlenes Token nützlich ist.

Rotation

Refresh Tokens sind Einweg-Tokens und werden bei jedem Austausch ersetzt.

Verification

Den Algorithmus festlegen (pinning) und iss, aud sowie exp prüfen, bevor man dem Token vertraut.

Ablauf

Wie ein JWT verifiziert wird

Jede geschützte Anfrage durchläuft denselben Pfad. Jeder fehlgeschlagene Check endet in einem 401.

  1. 1

    Token extrahieren

    Den Authorization-Header auslesen und das Bearer-Schema voraussetzen. Die Anfrage ablehnen, wenn kein Token vorhanden ist.

  2. 2

    Token aufteilen

    Den String an den Punkten in Header, Payload und Signatur zerlegen. Ein Token ohne genau drei Teile ist malformiert.

  3. 3

    Signatur verifizieren

    Die Signatur über den ersten beiden Teilen mit dem Schlüssel und dem festgelegten Algorithmus neu berechnen. Eine Abweichung bedeutet, dass das Token manipuliert wurde.

  4. 4

    Claims validieren

    exp und nbf für das Timing, iss für den erwarteten Aussteller und aud für die beabsichtigte Zielgruppe prüfen. Ein Staging-Token darf in der Produktion nicht akzeptiert werden.

  5. 5

    Principal zuordnen

    Bei Erfolg Subject und Scope an die Anfrage hängen, damit nachfolgende Handler autorisieren können, ohne das Token erneut zu parsen.

  6. 6

    Mit 401 ablehnen

    Wenn ein Schritt fehlschlägt, 401 mit einem generischen Fehler zurückgeben. Nicht erklären, welcher Check genau fehlgeschlagen ist.

Eine kurze Geschichte

Wie JWT zum Standard wurde

  1. 2011

    Der JWT-Entwurf erscheint

    Die OAuth-Arbeitsgruppe schlägt ein kompaktes Token-Format vor, um Claims zwischen Services zu transportieren.

    11
  2. 2015

    RFC 7519 standardisiert JWT

    JWT wird zusammen mit JWS, JWE, JWK und JWA veröffentlicht und gibt dem Format eine stabile Spezifikation.

    15
  3. 2015

    OpenID Connect übernimmt es

    ID-Tokens werden als JWTs definiert, was das Format zum Standard für Identity Provider macht.

    15
  4. 2015

    Die klassischen Angriffe

    Forscher dokumentieren "alg: none" und HS/RS-Confusion, was Verifizierer dazu zwingt, Algorithmen festzulegen.

    15
  5. 2020

    Rotation wird zur Norm

    Kurze Access Tokens und rotierende Refresh Tokens ersetzen langlebige Tokens in der gängigen Praxis.

    20

Der vollständige Leitfaden

JWT: Alles was Sie wissen müssen

Was ein JWT eigentlich ist

Ein JSON Web Token ist ein kompakter, URL-sicherer String, der eine Reihe von Claims zusammen mit einer Signatur über diese kodiert. Er ist durch RFC 7519 definiert und basiert auf zwei ergänzenden Spezifikationen: JSON Web Signature (JWS) für die Signierung und JSON Web Encryption (JWE) für die Vertraulichkeit. Fast jeder JWT, dem man in der Praxis begegnet, ist ein JWS.

Der Token besteht aus drei Base64url-kodierten Teilen, die durch Punkte getrennt sind: Header, Payload und Signatur. Er ist self-contained, was bedeutet, dass alles, was ein Server für eine Entscheidung benötigt, mit der Anfrage übermittelt wird. Die Validierung erfordert keinen Datenbank-Lookup, keinen gemeinsamen Session-Store und keinen Aufruf beim Issuer.

Anatomy of a JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzQyIiwic2NvcGUiOiJyZWFkOnBvc3RzIiwiZXhwIjoxNzYwMDAwMDAwfQ.3vJ1o0Q8m4nQ7bKc9xP2sT6wYqL0dRfH8uZgWmA1eJk
headerthe algorithm and token type, Base64url
payloadthe claims, also Base64url and readable by anyone
signatureHMAC over the first two parts, proving they were not changed

Hierbei ist es wichtig, präzise zu sein, was genau dadurch bewiesen wird. Ein JWT belegt, dass die Instanz, die ihn ausgestellt hat, genau diese Payload signiert hat. Er beweist nicht, dass der Token für Sie bestimmt war, es sei denn, Sie prüfen das Audience-Feld. Er beweist nicht, dass der Benutzer immer noch berechtigt ist, eine Aktion auszuführen, es sei denn, Sie prüfen Scopes und Rollen. Und er verbirgt keinerlei Informationen, es sei denn, Sie verwenden JWE. Jede Sicherheitseigenschaft, die für Sie relevant ist, muss explizit geprüft werden.

Die drei Teile entschlüsselt

Der Header ist ein kleines JSON-Objekt, das den Signieralgorithmus und den Token-Typ benennt. Das Feld alg ist dabei das entscheidende. typ ist fast immer JWT, und kid gibt an, welcher Schlüssel verwendet wurde, damit Verifizierer bei einer Schlüsselrotation den richtigen finden können.

{ "alg": "HS256", "typ": "JWT", "kid": "2026-09" }

Der Payload ist ein JSON-Objekt aus Claims. Registrierte Claims haben standardisierte Namen, die in der Spezifikation definiert sind: iss für den Issuer, sub für das Subject, aud für die Audience, exp für das Ablaufdatum (Expiration), nbf für „Not Before“, iat für den Ausstellungszeitpunkt (Issued At) und jti für eine eindeutige Token-ID. Alles andere sind benutzerdefinierte Claims, die ihr selbst festlegt.

{
  "iss": "https://auth.example.com",
  "sub": "user_42",
  "aud": "api.example.com",
  "exp": 1760000000,
  "iat": 1759999100,
  "scope": "read:posts write:posts",
  "role": "editor"
}

Die Signatur wird über das Base64url-Format des Headers und des Payloads berechnet. Ändert man auch nur ein einziges Zeichen in einem der beiden Teile, stimmt die neu berechnete Signatur nicht mehr überein. Diese Eigenschaft macht ein JWT sicher für die Übergabe an einen nicht vertrauenswürdigen Client und ist das einzige Hindernis zwischen einem legitimen Claim und einer Fälschung.

Signiert, nicht verschlüsselt

Das häufigste Missverständnis bei JWTs ist, dass sie geheim seien. Das sind sie nicht. Der Payload ist Base64url-kodiert – das ist eine Kodierung, keine Verschlüsselung. Jeder, der den Token besitzt, kann jeden Claim mit einer einzigen Zeile Code dekodieren.

const [, payload] = token.split(".");
console.log(JSON.parse(atob(payload)));
// { sub: "user_42", role: "editor", scope: "read:posts" }

Das hat zwei Konsequenzen. Erstens: Packen Sie niemals Geheimnisse, Passwörter oder personenbezogene Daten in einen JWT, die Sie dem Client nicht gerne zeigen würden. Zweitens: Behandeln Sie den Token selbst als Anmeldedaten (Credential): Der Besitz des Tokens reicht aus, um als das entsprechende Subject aufzutreten. Genau deshalb sind die Speicherung und der Transport später in diesem Guide so wichtig.

Wenn Sie den Payload tatsächlich vor dem Client verbergen müssen, verwenden Sie JSON Web Encryption und eine entsprechende Library. Für die überwältigende Mehrheit der Systeme ist ein signierter JWS über TLS die richtige Lösung; eine zusätzliche Verschlüsselung würde lediglich die Komplexität erhöhen.

Signieralgorithmen: HS256 vs. RS256

In realen Deployments dominieren zwei Algorithmen-Familien.

HS256 ist HMAC mit SHA-256. Dasselbe Secret wird sowohl zum Signieren als auch zum Verifizieren verwendet. Er ist schnell, einfach und ideal, wenn ein einziger Service seine eigenen Token sowohl ausstellt als auch verifiziert.

import { SignJWT, jwtVerify } from "jose";

const secret = new TextEncoder().encode(process.env.JWT_SECRET);

const token = await new SignJWT({ role: "editor" })
  .setProtectedHeader({ alg: "HS256" })
  .setSubject("user_42")
  .setExpirationTime("15m")
  .sign(secret);

await jwtVerify(token, secret, { algorithms: ["HS256"] });

RS256 ist RSA mit SHA-256 und ES256 ist ECDSA über einer Prime Curve. Der Issuer besitzt einen privaten Schlüssel und signiert damit; alle anderen besitzen den öffentlichen Schlüssel und verifizieren die Signatur. Diese Asymmetrie ist der Grund, warum große Systeme diesen Ansatz bevorzugen: Ein Resource Server kann Token verifizieren, ohne selbst in der Lage zu sein, sie zu erstellen. ES256 bietet die gleiche Garantie bei wesentlich kleineren Schlüsseln und Signaturen.

import { importPKCS8, importSPKI, SignJWT, jwtVerify } from "jose";

const privateKey = await importPKCS8(process.env.JWT_PRIVATE_KEY!, "RS256");
const publicKey = await importSPKI(process.env.JWT_PUBLIC_KEY!, "RS256");

const token = await new SignJWT({ role: "editor" })
  .setProtectedHeader({ alg: "RS256", kid: "2026-09" })
  .setSubject("user_42")
  .setExpirationTime("15m")
  .sign(privateKey);

await jwtVerify(token, publicKey, {
  algorithms: ["RS256"],
  issuer: "https://auth.example.com",
  audience: "api.example.com",
});

Die wichtigste Regel, unabhängig von der Wahl des Algorithmus: Der Verifizierer muss den erwarteten Algorithmus fest vorgeben (pinning). Lassen Sie niemals den Header des Tokens selbst entscheiden, wie die Prüfung durchgeführt wird.

Claims: registriert und benutzerdefiniert

Registrierte Claims sind durch die Spezifikation reserviert und werden von jeder ernsthaften Library unterstützt.

  • iss — wer das Token ausgestellt hat. Prüfen Sie dies, um Tokens aus einer anderen Umgebung abzulehnen.
  • sub — auf wen sich das Token bezieht, normalerweise die User-ID.
  • aud — für wen das Token bestimmt ist. Ein Token, das für Ihre öffentliche API erstellt wurde, sollte nicht von Ihrer Admin-API akzeptiert werden.
  • exp — wann das Token abläuft, als Unix-Timestamp. Setzen Sie diesen Wert immer.
  • nbf — nicht vor diesem Zeitpunkt gültig. Selten benötigt, aber nützlich für gestaffelte Rollouts.
  • iat — wann es ausgestellt wurde. Praktisch für Prüfungen des maximalen Alters und zum Debugging.
  • jti — eine eindeutige ID für dieses Token, verwendet zur Erkennung von Replay-Attacken und für Deny-Lists.

Benutzerdefinierte Claims enthalten Anwendungsdaten: scope, role, tenant_id, email. Halten Sie diese klein und nicht-sensitiv. Da ein Token bei jeder Anfrage mitgesendet wird, ist ein aufgeblähter Payload eine dauerhafte Belastung für Bandbreite und Latenz.

Es ist verlockend, das gesamte Profil des Benutzers in das Token zu packen, um einen Datenbank-Read zu vermeiden. Widerstehen Sie diesem Impuls. Claims veralten in dem Moment, in dem sich eine Rolle ändert, und Sie können ein Token, das ein Client bereits besitzt, nicht nachträglich ungültig machen. Packen Sie nur das hinein, was der Verifizierer wirklich benötigt, und rufen Sie den Rest bei Bedarf ab.

Token erstellen und verifizieren

In Node ist jose die moderne Wahl. Es basiert auf Promises, funktioniert in jeder Runtime, einschließlich Cloudflare Workers und Deno, und bietet eine kleine, präzise API. jsonwebtoken ist die ältere Library mit Callback-Struktur und ist in bestehendem Code nach wie vor weit verbreitet.

import { SignJWT } from "jose";

export async function issueAccessToken(userId: string, scope: string) {
  return new SignJWT({ scope })
    .setProtectedHeader({ alg: "HS256", typ: "JWT" })
    .setSubject(userId)
    .setIssuer("https://auth.example.com")
    .setAudience("api.example.com")
    .setIssuedAt()
    .setExpirationTime("15m")
    .setJti(crypto.randomUUID())
    .sign(new TextEncoder().encode(process.env.JWT_SECRET));
}

Bei der Verifizierung entscheidet sich die Sicherheit. Ein Verifizierer muss die Signatur prüfen, den Algorithmus festlegen und exp, iss sowie aud validieren. Eine Library wird einen Token gerne dekodieren, ohne ihn zu verifizieren – und dieser dekodierte Payload ist dann eine vom Angreifer kontrollierte Eingabe.

import { jwtVerify, type JWTPayload } from "jose";

const secret = new TextEncoder().encode(process.env.JWT_SECRET);

export async function verifyAccessToken(token: string): Promise<JWTPayload> {
  const { payload } = await jwtVerify(token, secret, {
    algorithms: ["HS256"],
    issuer: "https://auth.example.com",
    audience: "api.example.com",
  });
  return payload;
}

Beachten Sie, dass jwtVerify exp und nbf automatisch erzwingt, aber iss und aud nur prüft, wenn Sie diese übergeben. Diese wegzulassen ist eine echte Sicherheitslücke: Ein für einen Staging-Service ausgestellter Token würde dann auch in der Produktion akzeptiert werden, ebenso wie ein Token, der für eine ganz andere Anwendung erstellt wurde.

Access- und Refresh-Token

Ein einzelnes, langlebiges Token ist zwar praktisch, aber gefährlich. Die Standardlösung ist ein Paar mit unterschiedlichen Laufzeiten und unterschiedlichen Zielgruppen (Audiences).

  • Das access token ist kurzlebig, typischerweise 5 bis 15 Minuten. Es wird bei jedem API-Aufruf mitgesendet und ist das einzige Token, das der Resource Server jemals sieht.
  • Das refresh token ist langlebig, von einigen Tagen bis hin zu Monaten. Es wird nur an den Authorization Server gesendet, um im Austausch ein neues access token zu erhalten.

Diese Aufteilung begrenzt den potenziellen Schaden. Wenn ein access token durchsickert, ist es innerhalb weniger Minuten wertlos. Das refresh token, welches wesentlich sensibler ist, wird niemals an Resource Server übertragen und kann rotiert oder widerrufen werden – hier liegt die eigentliche Kontrolle.

Refresh Token Rotation

Rotation bedeutet, dass bei jeder Aktualisierung ein neues Refresh Token ausgestellt und das alte ungültig gemacht wird. Wenn ein Angreifer ein Refresh Token stiehlt und verwendet, wird der legitime Client später ein bereits verwendetes Token vorlegen. Der Server kann diese Mehrfachverwendung erkennen und die gesamte Token-Familie widerrufen.

import { randomUUID } from "node:crypto";

export async function rotateRefreshToken(presented: string) {
  const stored = await db.refreshToken.findUnique({ where: { token: presented } });

  if (!stored || stored.revokedAt || stored.expiresAt < new Date()) {
    if (stored) await revokeFamily(stored.familyId);
    throw new Error("invalid_refresh_token");
  }

  await db.refreshToken.update({
    where: { id: stored.id },
    data: { revokedAt: new Date(), replacedBy: randomUUID() },
  });

  return issueTokenPair(stored.userId, stored.familyId);
}

Speichere Refresh Tokens gehasht, genau so, wie du Passwörter speichern würdest. Ein Datenbank-Leak sollte einem Angreifer keine funktionierenden Zugangsdaten liefern, denn ein Refresh Token ist im Grunde nichts anderes als ein Passwort, das das Login-Formular umgeht.

Wo man Tokens im Browser speichert

Es gibt keinen perfekten Ort, sondern nur Kompromisse zwischen Cross-Site Scripting und Cross-Site Request Forgery.

  • localStorage kann von jedem Skript auf der Seite gelesen werden. Ein einziger XSS-Bug genügt, und der Angreifer exfiltriert jedes Token. Dies ist der Fehler, der immer wieder in Berichten über Datenlecks auftaucht.
  • In-memory, also als Variable in einem Modul, ist sicher vor persistentem XSS, geht aber beim Aktualisieren der Seite verloren. Daher wird dies normalerweise mit einem Refresh Token in einem httpOnly Cookie kombiniert.
  • Ein httpOnly, Secure, SameSite Cookie kann nicht von JavaScript gelesen werden, was den Diebstahl von Tokens via XSS neutralisiert. Dies führt jedoch CSRF wieder ein, was durch SameSite=Lax oder Strict zusammen mit einem CSRF-Token abgeschwächt wird.

Für eine Browser-Anwendung ist der pragmatische Standard ein kurzlebiges Access Token im Speicher und ein rotierender Refresh Token in einem httpOnly Cookie, der auf den Refresh-Endpunkt beschränkt ist. Native und serverseitige Clients haben diese Einschränkung nicht und können Tokens in einem sicheren Speicher oder einfach im Arbeitsspeicher halten.

Der Trade-off der Statelessness und das Revocation-Problem

Das Hauptargument für JWTs ist die Statelessness. Jeder Server kann einen Token verifizieren, ohne auf einen gemeinsamen State zugreifen zu müssen, was eine hervorragende Skalierung über verschiedene Regionen hinweg ermöglicht und das horizontale Scaling trivial macht. Der Preis dafür ist, dass das Revocation (Widerrufen von Token) ausgesprochen schwierig ist. Ein signierter Token ist bis zu seinem Ablaufdatum gültig – völlig unabhängig davon, ob Sie den Benutzer inzwischen gelöscht, seine Rolle geändert oder ihn ausgeloggt haben. Es gibt keinen zentralen Datensatz, den man löschen könnte.

Sie können jedoch einen Teil der Kontrolle zurückgewinnen:

  • Halten Sie Access Token kurz, sodass das Zeitfenster für das Revocation nur wenige Minuten statt Tage beträgt.
  • Führen Sie eine Deny-List von jti-Werten für seltene, sofortige Logouts, die bei jeder Anfrage geprüft wird. Dies führt den State wieder ein, daher sollte die Liste klein gehalten werden und Einträge automatisch ablaufen.
  • Fügen Sie einen pro Benutzer spezifischen token_version-Claim hinzu und lehnen Sie Token ab, deren Version veraltet ist. Eine Passwortänderung oder eine Rollenanpassung erhöht diese Version.

Hier ist die ehrliche Zusammenfassung: JWTs tauschen ein einfaches Revocation gegen eine einfache Skalierung. Wenn Sie eine sofortige Sperrung an allen Stellen benötigen, könnten Session-Cookies mit einem Store im Hintergrund besser geeignet sein, wie in Session Auth beschrieben.

Algorithm Confusion und alg none

Zwei Angriffsvektoren sind so alt, dass sie bereits in Lehrbüchern stehen, finden aber immer noch Opfer.

Der erste ist alg: none. Ein Angreifer ändert den Header auf {"alg":"none"} und entfernt die Signatur. Ein naiver Verifizierer, der dem Header vertraut, akzeptiert das gefälschte Token. Der zweite ist die HS/RS-Confusion. Ein Service, der RS256-Tokens erwartet, wird dazu verleitet, ein HS256-Token zu akzeptieren, das mit dem RSA-Public-Key signiert wurde – welcher per Definition öffentlich zugänglich ist.

Beide haben die gleiche Lösung: Der Verifizierer legt den Algorithmus fest, nicht das Token.

// Good: the verifier decides.
await jwtVerify(token, secret, { algorithms: ["HS256"] });

// Bad: the token decides.
const { header } = decodeProtectedHeader(token);
await jwtVerify(token, secret, { algorithms: [header.alg] });

Lehnen Sie alg: none konsequent ab, leiten Sie den Key niemals aus einer nicht vertrauenswürdigen Quelle ab und behandeln Sie den Header als Daten, nicht als Anweisungen.

Scopes und Autorisierung

Die Authentifizierung beantwortet die Frage, wer jemand ist, und Scopes beantworten die Frage, was diese Person tun darf. Ein scope Claim ist eine durch Leerzeichen getrennte Liste von Berechtigungen, die von einer middleware geprüft wird, bevor ein Handler ausgeführt wird.

export function requireScope(required: string) {
  return (req, res, next) => {
    const granted = String(req.user.scope ?? "").split(" ");
    if (!granted.includes(required)) {
      return res.status(403).json({ error: "insufficient_scope" });
    }
    next();
  };
}

Halten Sie Scopes grobmaschig und stabil und erzwingen Sie diese auf dem Server. Ein Token ohne den richtigen Scope muss mit einem 403-Fehler fehlschlagen, nicht mit 401: Der Aufrufer ist authentifiziert, aber nicht berechtigt. Für komplexere Zugriffsmodelle, die auf Rollen und Attributen basieren, lesen Sie mehr über RBAC.

JWT vs. opaque tokens

Ein JWT ist ein Bearer Token, das seine eigene Validierung mitführt. Ein opaque token hingegen ist eine zufällige Zeichenfolge ohne inhärente Bedeutung, sodass der Server diesen nachschlagen muss, um Informationen zu erhalten.

Opaque tokens haben Vorteile bei der Widerrufbarkeit (Revocation) und dem Datenschutz. Sie können eine Session sofort löschen, und falls ein Token durchsickert, gibt es keinerlei Informationen preis. Der Preis dafür ist ein Datenbank- oder Cache-Roundtrip bei jeder Anfrage. JWTs punkten bei der Skalierbarkeit und Unabhängigkeit. Services können sie lokal verifizieren und benötigen keinen gemeinsamen Speicher, allerdings auf Kosten eines Zeitfensters, in dem ein widerrufener Token noch gültig bleibt.

Viele Produktionssysteme setzen auf beides: ein JWT access token für die Geschwindigkeit und ein opaque refresh token für die Kontrolle. Dieser Hybrid-Ansatz ist das, was die meisten Identity Provider heute anbieten, und ist ein guter Standard, wenn Sie sich nicht entscheiden können.

Schlüsselrotation mit kid

Signaturschlüssel sollten nicht ewig gültig sein. Eine Rotation begrenzt den Schaden eines kompromittierten Schlüssels, und das Header-Feld kid sorgt dafür, dass die Rotation für Clients unsichtbar bleibt: Der Verifizierer liest kid, wählt den passenden Schlüssel aus und prüft die Signatur.

import { SignJWT, jwtVerify, createLocalJWKSet } from "jose";

const jwks = createLocalJWKSet({
  keys: [{ kty: "oct", kid: "2026-09", k: process.env.JWT_SECRET }],
});

// The verifier resolves the key from the header's kid.
await jwtVerify(token, jwks, { algorithms: ["HS256"] });

Wenn Sie rotieren, veröffentlichen Sie den neuen Schlüssel neben dem alten, signieren Sie neue Tokens mit der neuen kid und verifizieren Sie den alten Schlüssel so lange weiter, bis jedes mit ihm signierte Token abgelaufen ist. Wenn Sie ihn zu früh entfernen, melden Sie alle aktiven Benutzer gleichzeitig ab. Veröffentlichen Sie bei asymmetrischen Schlüsseln ein JWKS-Dokument unter einer bekannten URL und lassen Sie es von den Resource Servern cachen.

Token-Lebensdauern in der Praxis

Die Ablaufzeit ist ein Balanceakt zwischen Sicherheit und Komfort. Es gibt keine universell richtige Antwort, aber die Struktur eines sinnvollen Standardwerts ist konsistent.

  • Access tokens: 5 bis 15 Minuten. Kurz genug, damit ein Leak schnell wertlos wird, aber lang genug, damit nicht bei jeder Anfrage ein Refresh nötig ist.
  • Refresh tokens: 7 bis 30 Tage, mit Rotation und einem Sliding Window. Langlebig genug, um Nutzer angemeldet zu lassen, aber kurz genug, damit ein vergessenes Token irgendwann abläuft.
  • Absolute Session-Limit: 30 bis 90 Tage. Ein maximales Alter, nach dem sich der Nutzer erneut authentifizieren muss, unabhängig davon, wie oft er den Refresh-Prozess durchlaufen hat.

Wenn sich Ihre Nutzer darüber beschweren, dass sie ausgeloggt werden, liegt die Lösung in einem reibungsloseren Silent Refresh, nicht in einem längeren Access token. Ein 24-stündiges Access token ist ein vorprogrammierter Ausfall bei der Token-Widerrufung (Revocation).

Einen Token debuggen, ohne ihm zu vertrauen

Wenn eine Anfrage mit einem 401-Fehler fehlschlägt, möchten Sie sehen, welche Claims der Token enthält, ohne dabei die Verifizierung zu schwächen. Das Dekodieren ist sicher, solange Sie das Ergebnis als nicht vertrauenswürdige Daten behandeln.

import { decodeJwt, decodeProtectedHeader } from "jose";

const header = decodeProtectedHeader(token);
const claims = decodeJwt(token);

console.log({ alg: header.alg, kid: header.kid });
console.log({
  sub: claims.sub,
  iss: claims.iss,
  aud: claims.aud,
  exp: new Date((claims.exp ?? 0) * 1000).toISOString(),
  expired: (claims.exp ?? 0) * 1000 < Date.now(),
});

Die beiden häufigsten Fehler, auf die Sie stoßen werden, sind ein aud-Mismatch nach einer Umbenennung eines Dienstes und ein iss-Mismatch nach einem Wechsel zwischen Umgebungen. Beides sind Konfigurationsprobleme, die unsichtbar bleiben, bis Sie die Claims ausgeben.

Tokens testen

Du solltest in der Lage sein, eine authentifizierte Route zu testen, ohne einen Identity Provider bereitstellen zu müssen. Da ein JWT lediglich ein signierter String ist, genügt ein Test-Helper, der einen Token mit demselben Test-Secret signiert.

import { SignJWT } from "jose";
import request from "supertest";
import app from "../app.js";

const secret = new TextEncoder().encode("test-secret");

async function tokenFor(scope = "read:posts") {
  return new SignJWT({ scope })
    .setProtectedHeader({ alg: "HS256" })
    .setSubject("user_1")
    .setIssuer("https://auth.example.com")
    .setAudience("api.example.com")
    .setExpirationTime("5m")
    .sign(secret);
}

test("rejects a request without a token", async () => {
  await request(app).get("/posts").expect(401);
});

test("accepts a request with a valid token", async () => {
  const token = await tokenFor();
  await request(app)
    .get("/posts")
    .set("authorization", `Bearer ${token}`)
    .expect(200);
});

Teste zudem die Negativfälle: einen abgelaufenen Token, einen Token mit dem falschen Audience und einen Token, der mit einem anderen Key signiert wurde. Genau diese Prüfungen schützen deine Anwendung, daher verdienen sie ebenso viel Testabdeckung wie der Happy Path.

Best Practices

  • Setzen Sie immer exp; halten Sie die Gültigkeit von Access Tokens auf 15 Minuten oder weniger begrenzt.
  • Pinnen Sie den Algorithmus auf dem Verifier und lehnen Sie alg: none ab.
  • Validieren Sie iss, aud, exp und nbf; vertrauen Sie niemals einem Claim, den Sie nicht geprüft haben.
  • Halten Sie Secrets und private Keys aus der Versionsverwaltung fern und laden Sie diese aus einem Secret Manager.
  • Speichern Sie Refresh Tokens gehasht und rotieren Sie diese bei jeder Verwendung.
  • Halten Sie Claims klein und nicht-sensitiv; der Payload ist lesbar.
  • Bevorzugen Sie in Browsern httpOnly Cookies oder den Speicher (Memory) gegenüber dem localStorage.
  • Planen Sie den Widerruf (Revocation) durch kurze Laufzeiten, eine jti Deny-List oder eine Token-Version ein.
  • Verwenden Sie jose für neuen Code; es ist Promise-basiert und über verschiedene Runtimes hinweg portabel.

Häufige Fehler

  • Davon ausgehen, dass der Payload verschlüsselt ist, nur weil er wie Zeichensalat aussieht.
  • Claims mit decode auslesen und diese als verifiziert betrachten.
  • Den alg-Header des Tokens entscheiden lassen, welcher Verifizierungsalgorithmus genutzt wird.
  • Einen Token akzeptieren, ohne aud zu prüfen, sodass Staging-Tokens auch in der Production funktionieren.
  • Ein schwaches oder gemeinsam genutztes Secret verwenden oder dieses im Repository committen.
  • Die Ablaufzeit auf 30 Tage setzen, weil das Refreshing nervig ist.
  • Tokens im localStorage speichern und glauben, damit fertig zu sein.
  • Eine Rolle in den Token schreiben und diese nicht invalidieren, wenn sich die Rolle ändert.
  • Einen 401- und einen 403-Fehler als denselben Fehler behandeln.

Wie geht es weiter?

JWTs sind nur ein Werkzeug in einem größeren Toolkit für die Identitätsverwaltung. Der OAuth 2.0-Guide zeigt, wie Token tatsächlich über delegierte Autorisierung bezogen werden, Session Auth behandelt die cookie-basierte Alternative für Fälle, in denen ein sofortiger Widerruf (Revocation) erforderlich ist, und API Keys erklärt langlebige Anmeldedaten für Machine-Clients. Wenn Sie sehen möchten, wie dieser Verifizierungscode in einem echten Server implementiert wird, lesen Sie den Abschnitt zu Node.js.

In der Praxis

Ausstellen, verifizieren, rotieren, inspizieren

Die vier Operationen, die Sie zuerst schreiben werden, unter Verwendung von jose.

tokens.ts
import { SignJWT } from "jose";

const secret = new TextEncoder().encode(process.env.JWT_SECRET);

export async function issueAccessToken(userId: string, scope: string) {
  return new SignJWT({ scope })
    .setProtectedHeader({ alg: "HS256", typ: "JWT" })
    .setSubject(userId)
    .setIssuer("https://auth.example.com")
    .setAudience("api.example.com")
    .setIssuedAt()
    .setExpirationTime("15m")
    .setJti(crypto.randomUUID())
    .sign(secret);
}

HS256 vs RS256

Symmetrisch ist einfacher, wenn ein einziger Service ausstellt und verifiziert. Asymmetrisch lohnt sich, sobald die Verifizierung über mehrere Services verteilt ist.

HS256
import { SignJWT, jwtVerify } from "jose";

const secret = new TextEncoder().encode(process.env.JWT_SECRET);

const token = await new SignJWT({ role: "editor" })
  .setProtectedHeader({ alg: "HS256" })
  .setSubject("user_42")
  .setExpirationTime("15m")
  .sign(secret);

await jwtVerify(token, secret, { algorithms: ["HS256"] });
RS256
import { importPKCS8, importSPKI, SignJWT, jwtVerify } from "jose";

const privateKey = await importPKCS8(process.env.JWT_PRIVATE_KEY!, "RS256");
const publicKey = await importSPKI(process.env.JWT_PUBLIC_KEY!, "RS256");

const token = await new SignJWT({ role: "editor" })
  .setProtectedHeader({ alg: "RS256", kid: "2026-09" })
  .setSubject("user_42")
  .setExpirationTime("15m")
  .sign(privateKey);

await jwtVerify(token, publicKey, { algorithms: ["RS256"] });

httpOnly Cookie vs localStorage

JavaScript kann ein httpOnly-Cookie nicht lesen, was den häufigsten Weg von einem XSS-Bug zur vollständigen Account-Übernahme blockiert.

Bevorzugen
Set-Cookie: access_token=eyJhbGciOi...;
  HttpOnly;
  Secure;
  SameSite=Lax;
  Path=/;
  Max-Age=900
Vermeiden
localStorage.setItem("access_token", token);

// Any injected script can now read the token and
// exfiltrate it. Persistent XSS becomes account takeover.

Abwägungen

Lohnt sich zustandslose Authentifizierung?

JWTs tauschen eine einfache Revocation gegen einfache Skalierbarkeit. Entscheiden Sie, was Ihr Produkt tatsächlich benötigt.

Strengths

  • Kein gemeinsamer Session-Store

    Jede Instanz kann ein Token mit einem bereits vorhandenen Schlüssel verifizieren, sodass horizontales Scaling und Multi-Region-Deployments simpel bleiben.

  • Kompakt und portabel

    Ein einziger Header transportiert Identität, Scopes und Ablaufdatum über Services, Sprachen und Runtimes hinweg ohne Übersetzungsschicht.

  • Ideal für Service-to-Service

    Unabhängige Services können Tokens lokal verifizieren, was eine synchrone Abhängigkeit vom Authorization-Server entfernt.

Trade-offs

  • Revocation ist der schwierige Teil

    Ein signiertes Token ist gültig, bis es abläuft. Ein Benutzer-Logout oder das Entziehen einer Rolle erreicht ein Token, das sich bereits beim Client befindet, nicht.

  • Der Payload ist öffentlich

    Base64url ist keine Verschlüsselung. Alles, was Sie in die Claims schreiben, ist für den Inhaber und jeden, der es abfängt, lesbar.

  • Kleine Fehler haben schwere Folgen

    Dem Algorithmus im Header zu vertrauen, den Audience-Check zu überspringen oder Tokens in localStorage zu speichern, macht aus einem Komfort-Feature eine Sicherheitslücke.

Häufig gestellte Fragen

Häufig gestellte Fragen

Keep learning

Related topics from the roadmap.

$ Lernen Sie jetzt

Bereit, JWT (JSON Web Tokens) zu lernen?

Unser interaktives Tutorial führt Sie Schritt für Schritt durch JWT (JSON Web Tokens) — mit Quizzen und echtem Code, den Sie im Browser ausführen können.