Use Cases (Aktualisiert: 2.6.2026)

JWT-Authentifizierung mit Claude Code: Claims, Cookies, Rotation und Schlüssel

JWT mit Claude Code sicher bauen: Claims, Cookies, Refresh-Rotation, Widerruf, Schlüsselrotation und sichere Prompts.

JWT-Authentifizierung mit Claude Code: Claims, Cookies, Rotation und Schlüssel

JWT-Authentifizierung wirkt in Demos sehr einfach. Man signiert ein Payload, gibt ein Token zurück und prüft es in Middleware. In Produktion reichen diese drei Schritte nicht. Eine fehlende aud-Prüfung, ein zu langlebiges Refresh Token, localStorage als Ablage oder eine hastige Schlüsselrotation können echte Konten gefährden.

Dieser Leitfaden erklärt JWT zuerst für Einsteiger und macht daraus anschließend einen klaren Auftrag für Claude Code. Behandelt werden Claim-Design, Signatur versus Verschlüsselung, Cookie- und Session-Ablage, Refresh-Token-Rotation, Widerruf, Schlüsselrotation, typische Sicherheitsfehler und sichere Prompts. Für den gesamten Login-Kontext lesen Sie auch Authentifizierung implementieren, Cookie-Management und RBAC mit Claude Code.

Die fachliche Basis sollten Primärquellen sein: RFC 7519 für JWT, RFC 8725 für sichere JWT-Nutzung, RFC 9700 für Refresh-Token-Replay, das OWASP JWT Cheat Sheet, MDN Set-Cookie, jose und Claude Code settings.

JWT-Grundlagen für Einsteiger

Ein JWT besteht aus drei Teilen: header.payload.signature. Der Header nennt Typ und Algorithmus. Das Payload enthält Claims, also Aussagen über das Token. Die Signatur hilft, Manipulationen zu erkennen. Häufige Claims sind sub für die Benutzer-ID, iss für den Aussteller, aud für die Zielgruppe, exp für das Ablaufdatum und jti für die Token-ID.

Der wichtigste Punkt: Ein normales JWT ist signiert, nicht verschlüsselt. Die Signatur verbirgt das Payload nicht. Wer das Token hat, kann das Payload dekodieren. Legen Sie dort keine API Keys, Adressen, Zahlungsdaten, internen Notizen oder unnötigen personenbezogenen Daten ab. Wenn eine Information geheim bleiben muss, gehört sie auf den Server. Das JWT sollte nur minimale Referenzen enthalten.

Claude Code sollte diesen Rahmen zuerst bekommen.

Implementiere JWT-Authentifizierung.
Regeln:
- JWT-Payloads sind signiert, nicht verschlüsselt. Keine Secrets darin.
- Access Token maximal 15 Minuten.
- Refresh Token maximal 7 Tage.
- iss, aud, sub, exp, iat und jti validieren.
- alg none und unerwartete Algorithmen ablehnen.
- Refresh-Token-Rotation und Wiederverwendungs-Erkennung implementieren.
- Cookie, CSRF, XSS, Widerruf und Schlüsselrotation prüfen.

Claims minimal entwerfen

Ein JWT ist kein Benutzerprofil-Cache. Das Access Token sollte nur Informationen enthalten, die die API am Eingang benötigt: stabile Benutzer-ID, Session-ID, Tenant-ID, grobe Rolle und Token-ID. Werte wie Tarif, Sperrstatus, feine Berechtigungen oder Nutzungslimits ändern sich und sollten serverseitig geprüft werden.

ClaimZweckVorsicht
subBenutzer-IDInterne ID statt E-Mail verwenden
issAusstellerAuf Auth-Server festlegen
audZielgruppeTokens für andere APIs ablehnen
expAblaufzeitAccess Tokens kurz halten
jtiToken-IDWiderruf und Audit
sidSession-IDGeräte-Logout und Token-Familien
roleGrobe RolleFeine Rechte serverseitig prüfen

Ein häufiger Fehler ist plan: "pro" oderdisabled: false im Token. Ändert sich der Tarif oder wird ein Konto gesperrt, bleibt der alte Claim bis zum Ablauf bestehen. Authentifizierung beantwortet “wer ist das?”. Autorisierung beantwortet “darf diese Person das jetzt?”. Diese Entscheidungen sollten getrennt bleiben.

Ausführbares TypeScript mit jose

Das folgende Demo signiert und prüft Access Tokens, speichert nur Hashes der Refresh Tokens, rotiert Refresh Tokens und erkennt Wiederverwendung. In Produktion ersetzen Sie Map durch Redis oder Datenbank und ergänzen HTTPS, Rate Limit, Audit Logs und CSRF-Schutz.

mkdir jwt-lab
cd jwt-lab
npm init -y
npm install jose
npm install -D tsx typescript @types/node
// auth-demo.ts
import { createHash, createSecretKey, randomUUID } from "node:crypto";
import { SignJWT, jwtVerify } from "jose";

const ISSUER = "https://auth.example.com";
const AUDIENCE = "claudecodelab-api";
const ACCESS_TTL = "15m";
const REFRESH_TTL_SECONDS = 60 * 60 * 24 * 7;

const accessKey = createSecretKey(
  Buffer.from(
    process.env.JWT_ACCESS_SECRET ??
      "dev-only-secret-change-me-32-bytes-minimum"
  )
);

const refreshKey = createSecretKey(
  Buffer.from(
    process.env.JWT_REFRESH_SECRET ??
      "dev-only-refresh-secret-change-me-32-bytes"
  )
);

type Role = "admin" | "user" | "viewer";
type User = { id: string; role: Role; tenantId: string };
type VerifiedAccess = {
  userId: string;
  role: Role;
  tenantId: string;
  sessionId: string;
  tokenId: string;
};

type RefreshRecord = {
  userId: string;
  sessionId: string;
  tokenHash: string;
  expiresAt: number;
  revokedAt?: number;
};

const refreshStore = new Map<string, RefreshRecord>();
const revokedAccessTokenIds = new Set<string>();

function sha256(value: string) {
  return createHash("sha256").update(value).digest("hex");
}

function assertRole(value: unknown): asserts value is Role {
  if (!["admin", "user", "viewer"].includes(String(value))) {
    throw new Error("invalid role claim");
  }
}

async function signAccessToken(user: User, sessionId: string) {
  const tokenId = randomUUID();

  return new SignJWT({ role: user.role, tid: user.tenantId, sid: sessionId })
    .setProtectedHeader({ alg: "HS256", typ: "JWT" })
    .setIssuer(ISSUER)
    .setAudience(AUDIENCE)
    .setSubject(user.id)
    .setIssuedAt()
    .setExpirationTime(ACCESS_TTL)
    .setJti(tokenId)
    .sign(accessKey);
}

async function verifyAccessToken(token: string): Promise<VerifiedAccess> {
  const { payload } = await jwtVerify(token, accessKey, {
    issuer: ISSUER,
    audience: AUDIENCE,
    algorithms: ["HS256"],
  });

  assertRole(payload.role);

  if (
    typeof payload.sub !== "string" ||
    typeof payload.tid !== "string" ||
    typeof payload.sid !== "string" ||
    typeof payload.jti !== "string"
  ) {
    throw new Error("missing required claim");
  }

  if (revokedAccessTokenIds.has(payload.jti)) {
    throw new Error("access token revoked");
  }

  return {
    userId: payload.sub,
    role: payload.role,
    tenantId: payload.tid,
    sessionId: payload.sid,
    tokenId: payload.jti,
  };
}

async function signRefreshToken(user: User, sessionId: string) {
  const tokenId = randomUUID();
  const token = await new SignJWT({ sid: sessionId, kind: "refresh" })
    .setProtectedHeader({ alg: "HS256", typ: "JWT" })
    .setIssuer(ISSUER)
    .setAudience("claudecodelab-refresh")
    .setSubject(user.id)
    .setIssuedAt()
    .setExpirationTime("7d")
    .setJti(tokenId)
    .sign(refreshKey);

  refreshStore.set(tokenId, {
    userId: user.id,
    sessionId,
    tokenHash: sha256(token),
    expiresAt: Date.now() + REFRESH_TTL_SECONDS * 1000,
  });

  return token;
}

async function rotateRefreshToken(refreshToken: string, user: User) {
  const { payload } = await jwtVerify(refreshToken, refreshKey, {
    issuer: ISSUER,
    audience: "claudecodelab-refresh",
    algorithms: ["HS256"],
  });

  if (
    typeof payload.jti !== "string" ||
    typeof payload.sid !== "string" ||
    typeof payload.sub !== "string"
  ) {
    throw new Error("invalid refresh token claims");
  }

  const record = refreshStore.get(payload.jti);
  const presentedHash = sha256(refreshToken);

  if (!record || record.revokedAt || record.tokenHash !== presentedHash) {
    for (const item of refreshStore.values()) {
      if (item.sessionId === payload.sid) item.revokedAt = Date.now();
    }
    throw new Error("refresh token reuse detected");
  }

  if (record.expiresAt < Date.now()) {
    throw new Error("refresh token expired");
  }

  record.revokedAt = Date.now();

  return {
    accessToken: await signAccessToken(user, payload.sid),
    refreshToken: await signRefreshToken(user, payload.sid),
  };
}

async function main() {
  const user: User = {
    id: "user_123",
    role: "admin",
    tenantId: "tenant_a",
  };
  const sessionId = randomUUID();
  const accessToken = await signAccessToken(user, sessionId);
  const refreshToken = await signRefreshToken(user, sessionId);
  const verified = await verifyAccessToken(accessToken);
  const rotated = await rotateRefreshToken(refreshToken, user);

  console.log({ verified, rotatedRefreshLength: rotated.refreshToken.length });
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});
npx tsx auth-demo.ts

Wichtig ist, dass das Refresh Token selbst nicht gespeichert wird. Gespeichert wird nur sein Hash. Nach Nutzung wird der alte Eintrag widerrufen und ein neues Paar ausgegeben. Taucht das alte Token erneut auf, wird die gesamte Familie für diese sid widerrufen.

Cookies, Session und Ablage

In Browser-Anwendungen gehört das Refresh Token meist in ein HttpOnly, Secure, SameSite Cookie. HttpOnly verhindert, dass JavaScript den Wert direkt liest. Das reduziert Token-Diebstahl bei XSS, ersetzt aber keine XSS-Abwehr. Weil Cookies automatisch gesendet werden, brauchen Refresh-, Logout- und Änderungsrouten CSRF-Schutz.

const refreshCookieOptions = {
  httpOnly: true,
  secure: true,
  sameSite: "lax" as const,
  path: "/api/auth/refresh",
  maxAge: 60 * 60 * 24 * 7,
};

const clearRefreshCookieOptions = {
  ...refreshCookieOptions,
  maxAge: 0,
};

Ein Access Token kann in einer SPA im Speicher liegen, verschwindet dann aber beim Reload. Mit BFF oder Next.js Route Handlers kann der Server API-Aufrufe übernehmen, ohne das Access Token an den Browser zu geben. localStorage ist bequem, aber bei XSS lesbar. Lange Bearer Tokens gehören dort nicht hin.

Widerruf und Schlüsselrotation

JWTs bleiben ohne Zusatzkontrolle bis exp gültig. Nutzen Sie kurze Access Tokens, eine jti-Widerrufsliste für kritische Ereignisse, sid-basierten Session-Widerruf und Refresh-Token-Rotation. Logout widerruft den Refresh-Eintrag. Passwortänderung, Kontosperre und Kompromittierungsverdacht widerrufen alle Sessions des Kontos.

Schlüsselrotation braucht eine Überlappungsphase. HS256 ist einfach, doch jeder Verifier kennt das gemeinsame Secret. Bei mehreren Services sind RS256 oder ES256 mit JWKS leichter zu betreiben, weil Verifier nur öffentliche Schlüssel benötigen.

import { createRemoteJWKSet, jwtVerify } from "jose";

const JWKS = createRemoteJWKSet(
  new URL("https://auth.example.com/.well-known/jwks.json")
);

export async function verifyWithRotatingKeys(token: string) {
  return jwtVerify(token, JWKS, {
    issuer: "https://auth.example.com",
    audience: "claudecodelab-api",
    algorithms: ["RS256", "ES256"],
  });
}
{
  "rotationPlan": {
    "step1": "Neuen Schlüssel erzeugen und in JWKS veröffentlichen",
    "step2": "Neue Tokens mit neuer kid signieren",
    "step3": "Alten Public Key bis zum Ablauf alter Tokens behalten",
    "step4": "Logs prüfen und alten Schlüssel entfernen"
  }
}

Use Cases und sichere Prompts

flowchart LR
  Login["Login"] --> Access["Kurzes Access Token"]
  Login --> Refresh["HttpOnly Refresh Cookie"]
  Access --> API["API prüft iss/aud/exp/jti"]
  Refresh --> Rotate["Rotation beim Refresh"]
  Rotate --> Store["DB/Redis speichert Hash und sid"]
  Store --> Reuse["Reuse: Token-Familie widerrufen"]

Use Case 1 ist ein SaaS-Adminbereich. tenantId kann Kontext liefern, aber Datenbankabfragen müssen zusätzlich nach Tenant filtern. Admin-Rechte, Sperrstatus und Abrechnung werden vor gefährlichen Aktionen serverseitig neu geprüft.

Use Case 2 sind bezahlte Inhalte oder Kurse. Access Tokens bleiben kurz, Refresh läuft leise im Hintergrund. Wenn Werbung, Analytics und Kauf-CTA auf derselben Site liegen, verbinden Sie das mit Web-Security-Headern und Cookie-Consent.

Use Case 3 sind Mobile- und Desktop-Apps. Hier nutzt man eher den sicheren Speicher des Betriebssystems statt Browser-Cookies. sid bleibt wichtig, damit verlorene Geräte widerrufen werden können.

Use Case 4 sind Microservices. Verteilen Sie kein symmetrisches Secret an alle Services. Prüfen Sie Public-Key-Verifikation, Gateway-Prüfung oder Token Exchange. Jeder Service validiert aud.

Entwirf und implementiere JWT-Authentifizierung in diesem Repository.
Vor Änderungen bitte eine Tabelle erstellen:
- Framework, User Model, Session/Cookie-Code, Auth Middleware
- bestehende Authorization Checks, CSRF, CSP und Rate Limit
- aktueller Token-Speicherort und XSS/CSRF-Risiko

Regeln:
- jose verwenden. Nicht zu jsonwebtoken zurückwechseln.
- Access Token 15 Minuten. Refresh Token 7 Tage.
- iss, aud, sub, exp, iat, jti und sid validieren.
- Refresh Token nur als Hash in DB/Redis speichern.
- Rotation umsetzen und bei Reuse die sid-Familie widerrufen.
- Keine Secrets, .env oder Produktionstokens ausgeben.
- Am Ende Test- oder curl-Nachweis liefern.

Fehler, Prüfung und CTA

Typische Fehler sind: Algorithmus nicht festlegen, sensible Daten ins Payload schreiben, aud oderiss nicht prüfen, Refresh Tokens wiederverwendbar lassen, Logout nur im Browser durchführen, alte Schlüssel sofort löschen und echte Secrets in Claude Code einfügen. Gegenmaßnahmen sind Algorithmus-Allowlist, minimale Claims, Rotation, sid-Widerruf, JWKS-Überlappung und Prompts ohne Secrets.

curl -i -X POST https://example.com/api/auth/login \
  -H "content-type: application/json" \
  -d '{"email":"demo@example.com","password":"correct horse"}'

npm test -- --runInBand auth

Testen Sie abgelaufene Tokens, manipulierte Signaturen, falsche Audience, widerrufene jti, wiederverwendete Refresh Tokens, Logout, Passwortänderung und Kontosperre. Prüfen Sie auch HttpOnly, Secure, SameSite und einen möglichst engen Cookie-path.

Einzelentwickler können mit der kostenlosen Claude Code Cheatsheet Prüfgewohnheiten stabilisieren. Für wiederverwendbare Prompts und Vorlagen gibt es ClaudeCodeLab products. Teams, die JWT, RBAC, Cookies, Audit Logs und CI Gates gemeinsam einführen wollen, nutzen Claude Code training and consultation.

Beim Testen des Beispiels war der wichtigste Schritt, die Claim-Tabelle vor dem Signaturcode zu schreiben. Dadurch fielen drei Probleme früh auf: fehlende aud-Prüfung, ein top-level-await-Beispiel, das im Standard-tsx-Pfad nicht lief, und das Risiko von Refresh Tokens im Klartext. Sichere JWT-Authentifizierung ist kein einzelner Signiervorgang, sondern ein prüfbarer Ablauf aus Claims, Validierung, Speicherung, Rotation, Widerruf und Schlüsselbetrieb.

#Claude Code #JWT #Authentifizierung #Sicherheit #Node.js
Kostenlos

Kostenloses PDF: Claude-Code-Cheatsheet

E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.

Wir schützen Ihre Daten und senden keinen Spam.

Masa

Über den Autor

Masa

Engineer für praktische Claude-Code-Workflows und Team-Einführung.