DevToolBoxFREE
BlogAdvertise

JWT-Sicherheit Best Practices: Algorithmus, Ablauf, Rotation und Angriffsprävention

14 minvon DevToolBox

JWT-Sicherheits-Best-Practices: Algorithmuswahl, Ablauf, Rotation und Angriffsabwehr

JSON Web Tokens sind allgegenwärtig — aber die meisten Implementierungen haben kritische Sicherheitslücken. Dieser Leitfaden behandelt Algorithmusauswahl, Token-Ablauf und Rotation, Refresh-Token-Muster und die Abwehr der wichtigsten JWT-Angriffe.

Was ist ein JWT und wie funktioniert es?

Ein JWT ist ein Base64URL-kodierter String mit drei durch Punkte getrennten Teilen: Header (Algorithmus + Typ), Payload (Claims) und Signatur.

Algorithmusauswahl: HS256 vs RS256 vs ES256

Der häufigste JWT-Sicherheitsfehler ist die Verwendung von HS256 in einem verteilten System. RS256 und ES256 verwenden asymmetrische Schlüssel.

// Anatomy of a JWT — Header.Payload.Signature

// 1. Header (Base64URL decoded)
const header = {
  alg: "RS256",   // Algorithm: RS256 (asymmetric) preferred over HS256 (symmetric)
  typ: "JWT"
};

// 2. Payload (Base64URL decoded) — the claims
const payload = {
  iss: "https://auth.example.com",         // Issuer
  sub: "user_01HXYZ123",                  // Subject (user ID)
  aud: "https://api.example.com",          // Audience (target service)
  exp: 1735689600,                         // Expiry: Unix timestamp (15 min from now)
  iat: 1735688700,                         // Issued at
  jti: "8f14e0f9-21af-4c1e-8f26-3b4a5c6d7e8f", // Unique token ID (for revocation)
  roles: ["user"],                         // Custom claims
  email: "alice@example.com"
};

// 3. Signature (server-side only)
// RS256: sign(base64url(header) + '.' + base64url(payload), PRIVATE_KEY)
// Verification: verify(token, PUBLIC_KEY) — other services only need the public key

// --- Generating a JWT with jsonwebtoken (Node.js) ---
import jwt from 'jsonwebtoken';
import { readFileSync } from 'fs';
import { randomUUID } from 'crypto';

const privateKey = readFileSync('./private.pem');   // RSA private key
const publicKey  = readFileSync('./public.pem');    // RSA public key (shared with all services)

function issueAccessToken(userId: string, roles: string[]): string {
  return jwt.sign(
    {
      sub: userId,
      aud: 'https://api.example.com',
      roles,
      jti: randomUUID(),
    },
    privateKey,
    {
      algorithm: 'RS256',
      issuer: 'https://auth.example.com',
      expiresIn: '15m',  // Short-lived access token
    }
  );
}

function verifyAccessToken(token: string) {
  return jwt.verify(token, publicKey, {
    algorithms: ['RS256'],           // CRITICAL: whitelist algorithms — never allow 'none'
    issuer: 'https://auth.example.com',
    audience: 'https://api.example.com',
  });
}

Token-Ablauf- und Rotationsstrategie

Kurzlebige Access-Tokens (15 Minuten) gepaart mit langlebigen Refresh-Tokens (7–30 Tage) ist der Industriestandard.

// Refresh Token Pattern — Stateless Access + Stateful Refresh

import jwt from 'jsonwebtoken';
import { randomBytes } from 'crypto';

// ---- Token issuance ----
async function issueTokenPair(userId: string) {
  const accessToken = jwt.sign(
    { sub: userId, jti: randomUUID() },
    process.env.JWT_PRIVATE_KEY!,
    { algorithm: 'RS256', expiresIn: '15m' }
  );

  // Refresh token: random bytes stored in DB — NOT a JWT
  const refreshToken = randomBytes(64).toString('hex');
  const refreshExpiry = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days

  // Store hashed refresh token in DB (never store plaintext)
  await db.refreshTokens.create({
    tokenHash: sha256(refreshToken),
    userId,
    expiresAt: refreshExpiry,
    createdAt: new Date(),
  });

  return { accessToken, refreshToken, refreshExpiry };
}

// ---- Token refresh endpoint ----
app.post('/auth/refresh', async (req, res) => {
  const { refreshToken } = req.cookies; // httpOnly cookie
  if (!refreshToken) return res.status(401).json({ error: 'No refresh token' });

  const stored = await db.refreshTokens.findOne({
    tokenHash: sha256(refreshToken),
    expiresAt: { >: new Date() }, // Not expired
    revokedAt: null,                  // Not revoked
  });

  if (!stored) return res.status(401).json({ error: 'Invalid or expired refresh token' });

  // Rotate: invalidate old, issue new pair (refresh token rotation)
  await db.refreshTokens.update(
    { id: stored.id },
    { revokedAt: new Date() }         // Revoke old refresh token
  );

  const newPair = await issueTokenPair(stored.userId);

  // Set new refresh token as httpOnly cookie
  res.cookie('refreshToken', newPair.refreshToken, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    expires: newPair.refreshExpiry,
    path: '/auth/refresh',            // Scope cookie to refresh endpoint only
  });

  return res.json({ accessToken: newPair.accessToken });
});

// ---- Logout: revoke refresh token ----
app.post('/auth/logout', async (req, res) => {
  const { refreshToken } = req.cookies;
  if (refreshToken) {
    await db.refreshTokens.update(
      { tokenHash: sha256(refreshToken) },
      { revokedAt: new Date() }
    );
  }
  res.clearCookie('refreshToken', { path: '/auth/refresh' });
  return res.json({ success: true });
});

Häufige JWT-Angriffe und Verteidigungsmaßnahmen

Die gefährlichsten JWT-Schwachstellen sind: Algorithmusverwirrung, RS256→HS256-Verwirrung, schwache Secrets und fehlende exp-Validierung.

// JWT Attack Defenses — Code Patterns

// ---- ATTACK 1: Algorithm confusion ("none" bypass) ----
// VULNERABLE: never use verify() without specifying allowed algorithms
const VULNERABLE = jwt.verify(token, secret); // attacker can set alg: "none"

// SAFE: always whitelist algorithms
const SAFE = jwt.verify(token, publicKey, {
  algorithms: ['RS256'], // Never include 'none', 'HS256' if you're using RS256
});

// ---- ATTACK 2: RS256 > HS256 confusion ----
// Attacker changes alg from RS256 to HS256 and signs with the PUBLIC key as the secret
// Defense: always specify algorithm on verify — never infer from the token header
// VULNERABLE:
const header = JSON.parse(atob(token.split('.')[0]));
jwt.verify(token, publicKey, { algorithms: [header.alg] }); // attacker controls this!

// SAFE:
jwt.verify(token, publicKey, { algorithms: ['RS256'] }); // hardcoded

// ---- ATTACK 3: Weak HS256 secrets (brute-force) ----
// VULNERABLE: short or predictable secrets
const BAD_SECRET = 'secret';
const BAD_SECRET2 = 'mysupersecretkey';

// SAFE: 256-bit cryptographically random secret
import { randomBytes } from 'crypto';
const GOOD_SECRET = randomBytes(32).toString('hex'); // 256-bit
// Store in environment variable, rotate annually

// ---- ATTACK 4: Missing expiry validation ----
// Some older libraries skip exp check by default
// SAFE: always validate exp (jsonwebtoken does this by default)
try {
  const decoded = jwt.verify(token, publicKey, {
    algorithms: ['RS256'],
    clockTolerance: 30, // Allow 30 seconds clock skew max
    ignoreExpiration: false, // NEVER set this to true in production
  });
} catch (err) {
  if (err instanceof jwt.TokenExpiredError) {
    return res.status(401).json({ error: 'Token expired', code: 'TOKEN_EXPIRED' });
  }
  return res.status(401).json({ error: 'Invalid token' });
}

// ---- Token blacklist (for early revocation via jti) ----
import { createClient } from 'redis';
const redis = createClient();

async function revokeToken(jti: string, exp: number) {
  const ttl = exp - Math.floor(Date.now() / 1000); // seconds until natural expiry
  if (ttl > 0) {
    await redis.set(`jwt:revoked:${jti}`, '1', { EX: ttl });
  }
}

async function isTokenRevoked(jti: string): Promise<boolean> {
  return (await redis.get(`jwt:revoked:${jti}`)) !== null;
}

// Middleware: check blacklist
app.use(async (req, res, next) => {
  const decoded = jwt.verify(req.headers.authorization?.split(' ')[1] ?? '', publicKey, {
    algorithms: ['RS256'],
  });
  if (await isTokenRevoked((decoded as jwt.JwtPayload).jti ?? '')) {
    return res.status(401).json({ error: 'Token revoked' });
  }
  next();
});

Sichere Token-Speicherung

Access-Tokens: im Speicher speichern. Refresh-Tokens: in httpOnly, Secure, SameSite=Strict-Cookies speichern.

Häufig gestellte Fragen

Soll ich JWT oder opake Session-Tokens verwenden?

Verwenden Sie opake Session-Tokens für serverseitig gerenderte Apps. Verwenden Sie JWTs für Microservices oder APIs, wo zustandslose Verifikation benötigt wird.

Wie widerrufe ich ein JWT vor dessen Ablauf?

Lösungen: kurze Ablaufzeit (15 Min), Token-Blacklist in Redis, oder Token-Version im Benutzerdatensatz.

Welche Claims sollte jedes JWT enthalten?

Pflicht: iss, sub, aud, exp, iat, jti. Validieren Sie immer iss und aud auf dem empfangenden Service.

Wie lang sollten JWT-Secrets und private Schlüssel sein?

HS256-Secrets: mindestens 256 Bits. RS256: mindestens 2048-Bit-RSA-Schlüssel. ES256: 256-Bit-Elliptische-Kurven-Schlüssel.

Verwandte Tools

War das hilfreich?

Stay Updated

Get weekly dev tips and new tool announcements.

No spam. Unsubscribe anytime.

Partner Picks

Sponsor this article

Place your product next to this developer topic with tracked clicks.

Ask about article sponsorship

Verwandte Artikel

JWT-Authentifizierung: Vollstaendiger Implementierungsguide

JWT-Authentifizierung von Grund auf implementieren. Token-Struktur, Access- und Refresh-Tokens, Node.js-Implementierung, Client-seitige Verwaltung, Sicherheits-Best-Practices und Next.js-Middleware.

HTTP-Header Komplett-Guide

Vollständiger Leitfaden für HTTP-Header: Anfrage, Antwort und Sicherheits-Header.

REST-API-Design-Guide: Best Practices für 2026

Robuste REST-APIs entwerfen: Ressourcenbenennung, HTTP-Methoden, Paginierung, Fehlerbehandlung, Versionierung und Authentifizierung.

Online JWT Decoder: JSON Web Tokens Dekodieren, Inspizieren und Debuggen (2026 Guide)

Verwenden Sie unseren kostenlosen Online-JWT-Decoder, um JWT-Header, Payloads und Claims sofort zu inspizieren. Abdeckung von JWT-Struktur, Standardclaims, Dekodierung in JavaScript/Python/Go/Java, Signaturalgorithmen und Sicherheits-Best-Practices.

This site uses cookies for analytics and to display ads. By continuing to browse, you agree. Privacy Policy