Zum Inhalt springen

JWT-Best-Practices und häufige Fallstricke

JWTs wirken einfach, doch der Schein trügt — so vermeidest du die Sicherheitslücken, Performance-Fallen und Architekturfehler der meisten Umsetzungen.

3 Min. Lesezeit
Aufbau eines JWT-Tokens mit den Abschnitten Header, Payload und Signatur

JSON Web Tokens sind allgegenwärtig — bei der Authentifizierung, der API-Autorisierung, der Sitzungsverwaltung. Ihre Einfachheit wirkt verlockend: ein in sich geschlossener, signierter Payload, der Benutzerinformationen kodiert. Doch diese Einfachheit verbirgt ein Minenfeld aus Sicherheitslücken und Architekturentscheidungen, die in den meisten Tutorials unter den Tisch fallen.

Was ein JWT wirklich ist

Ein JWT besteht aus drei base64url-kodierten JSON-Objekten, die durch Punkte getrennt sind: Header, Payload und Signatur.

tstypescript
// Header: algorithm and token type
{ "alg": "RS256", "typ": "JWT" }
 
// Payload: claims (data)
{
  "sub": "user_123",
  "name": "Alice",
  "role": "admin",
  "iat": 1587654321,
  "exp": 1587657921
}
 
// Signature: cryptographic proof that the header and payload are unmodified
// RSASHA256(base64url(header) + "." + base64url(payload), privateKey)

Die Signatur garantiert die Integrität — ändert jemand den Payload, stimmt die Signatur nicht mehr überein. Verschlüsselt wird dabei aber nichts. Den Payload kann jeder lesen, der im Besitz des Tokens ist.

Algorithmus-Verwechslungsangriffe

Die gefährlichste Schwachstelle bei JWT ist die Manipulation des alg-Headers. Manche Bibliotheken vertrauen dem im Token angegebenen Algorithmus, anstatt serverseitig einen festen Algorithmus zu erzwingen.

tstypescript
// ❌ Trusting the algorithm from the token header
const payload = jwt.verify(token, publicKey);
// An attacker can set alg: "none" and skip the signature entirely
// Or set alg: "HS256" and sign with the public key as the HMAC secret
 
// ✅ Always specify the expected algorithm
const payload = jwt.verify(token, publicKey, {
  algorithms: ["RS256"], // Only accept RS256
});

Lege die akzeptierten Algorithmen immer fest. Erlaube niemals "none". Wer asymmetrisch signiert (RS256), sollte niemals symmetrische Signaturen (HS256) akzeptieren.

Symmetrische vs. asymmetrische Signaturen

AlgorithmusSignierenVerifizierenAnwendungsfall
HS256Gemeinsames GeheimnisDasselbe gemeinsame GeheimnisEinzelner Dienst
RS256Privater SchlüsselÖffentlicher SchlüsselMicroservices, Verifizierung durch Dritte
ES256Privater SchlüsselÖffentlicher SchlüsselWie RS256, aber mit kleineren Signaturen
tstypescript
// HS256: same secret signs and verifies
// ❌ Every service that verifies tokens knows the signing secret
const token = jwt.sign(payload, "shared-secret", { algorithm: "HS256" });
jwt.verify(token, "shared-secret", { algorithms: ["HS256"] });
 
// RS256: only the auth service has the private key
// ✅ Other services verify with the public key — can't forge tokens
const token = jwt.sign(payload, privateKey, { algorithm: "RS256" });
jwt.verify(token, publicKey, { algorithms: ["RS256"] });

Verwende RS256 oder ES256, sobald mehrere Dienste Tokens verifizieren müssen. Bei HS256 kann jeder, der verifizieren kann, auch Tokens fälschen.

Payloads klein halten

JWTs werden bei jeder Anfrage mitgeschickt — in Headern, Cookies oder URL-Parametern. Ein aufgeblähter Payload verschwendet Bandbreite und kann an die Größenlimits für Header stoßen.

tstypescript
// ❌ Stuffing everything into the JWT
const token = jwt.sign({
  sub: "user_123",
  name: "Alice Johnson",
  email: "alice@example.com",
  role: "admin",
  permissions: ["read", "write", "delete", "manage-users", "view-analytics"],
  organization: { id: "org_456", name: "Acme Corp", plan: "enterprise" },
  preferences: { theme: "dark", language: "en", timezone: "UTC" },
}, key);
// This token is 500+ bytes — sent with EVERY request
 
// ✅ Minimal payload — look up details when needed
const token = jwt.sign({
  sub: "user_123",
  role: "admin",
  org: "org_456",
}, key, { expiresIn: "15m" });
// ~200 bytes — essential claims only

Nimm die Benutzer-ID, die Rolle und alle Daten auf, die für Autorisierungsentscheidungen nötig sind. Alles andere lässt sich bei Bedarf aus einer Datenbank oder einem Cache nachladen.

Ablauf und Widerruf

JWTs sind zustandslos — einmal ausgestellt, kann der Server sie ohne zusätzliche Infrastruktur nicht widerrufen. Das ist der grundlegende Kompromiss.

tstypescript
// Short expiration limits the damage window
const accessToken = jwt.sign(
  { sub: userId, role: user.role },
  privateKey,
  {
    algorithm: "RS256",
    expiresIn: "15m", // Short-lived
    issuer: "https://auth.myapp.com",
    audience: "https://api.myapp.com",
  },
);
tstypescript
// For immediate revocation, you need a blocklist
const revokedTokens = new Set<string>();
 
function verifyToken(token: string) {
  const payload = jwt.verify(token, publicKey, {
    algorithms: ["RS256"],
    issuer: "https://auth.myapp.com",
    audience: "https://api.myapp.com",
  });
 
  // Check if this specific token has been revoked
  if (revokedTokens.has(payload.jti)) {
    throw new Error("Token revoked");
  }
 
  return payload;
}

Eine Sperrliste ist ein Kompromiss — man verliert die reine Zustandslosigkeit, gewinnt dafür aber die Möglichkeit zu widerrufen. Halte die Sperrliste in Redis vor, mit einer TTL, die der maximalen Lebensdauer des Tokens entspricht, damit Einträge automatisch ablaufen.

Alles validieren

tstypescript
// ❌ Minimal verification — many attack vectors remain
const payload = jwt.verify(token, key);
 
// ✅ Full verification — close every loophole
const payload = jwt.verify(token, publicKey, {
  algorithms: ["RS256"],      // Prevent algorithm confusion
  issuer: "https://auth.myapp.com",  // Reject tokens from other issuers
  audience: "https://api.myapp.com", // Reject tokens meant for other services
  clockTolerance: 30,         // Allow 30s clock skew
  maxAge: "1h",               // Reject tokens older than 1 hour
});
 
// Additional checks
if (!payload.sub) throw new Error("Missing subject claim");
if (!payload.role) throw new Error("Missing role claim");

Wann man JWTs besser nicht einsetzt

JWTs sind nicht immer die richtige Wahl. Läuft deine Anwendung auf einem einzigen Server und muss sie keine dienstübergreifende Token-Verifizierung durchführen, sind serverseitige Sitzungen einfacher und sicherer.

SzenarioJWTServerseitige Sitzung
Einzelner ServerUnnötige Komplexität✅ Einfacher
Microservices✅ Kein gemeinsamer Session-Speicher nötigErfordert gemeinsamen Speicher
Sofortiger Widerruf nötigErfordert Sperrliste✅ Sitzung einfach löschen
Domänenübergreifende Auth✅ In sich geschlossenErfordert gemeinsames Cookie-Handling

Die wichtigsten Erkenntnisse

  1. Gib beim Verifizieren immer den Algorithmus explizit an — vertraue niemals dem alg-Header des Tokens
  2. Setze bei Multi-Service-Architekturen auf RS256/ES256 — bei HS256 kann jeder Verifizierer auch Tokens fälschen
  3. Halte Payloads minimal — nur Benutzer-ID, Rolle und die für die Autorisierung wirklich nötigen Claims
  4. Eine kurze Gültigkeitsdauer (15 Minuten) plus Refresh-Tokens begrenzt den Schaden gestohlener Tokens
  5. JWTs lassen sich ohne Sperrliste nicht widerrufen — das ist der grundlegende Kompromiss der Zustandslosigkeit
  6. Prüfe bei jeder Verifizierung Aussteller, Zielgruppe und Algorithmus — nicht nur die Signatur
Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX