Use Cases (Aktualisiert: 1.6.2026)

2FA mit Claude Code implementieren: TOTP, Backup-Codes, Recovery und Step-Up Auth

Praxisleitfaden für 2FA/MFA in Next.js mit Claude Code: TOTP, Backup-Codes, Recovery, Limits, Passkeys und Audit.

2FA mit Claude Code implementieren: TOTP, Backup-Codes, Recovery und Step-Up Auth

2FA ist nicht nur ein QR-Code auf einer Einstellungsseite. Eine produktionsreife 2FA/MFA-Implementierung braucht TOTP, Backup-Codes, Step-Up Auth für sensible Aktionen, Recovery, Rate Limits, gemerkte Geräte, Audit-Logs und einen Pfad zu WebAuthn/passkeys.

Dieser Leitfaden zeigt, wie ich Claude Code für eine Next.js App Router Anwendung mit TypeScript und Prisma einsetzen würde. Für die Login-Basis passt der Claude Code JWT Authentication Guide, für Passwort-Wiederherstellung der Password Reset Guide, für Berechtigungen der RBAC Guide, und für Agentenrechte die Claude Code Security Best Practices.

Als externe Grundlage dienen OWASP MFA Cheat Sheet, OWASP WSTG MFA Testing, MDN Web Authentication API und W3C WebAuthn Level 3. Diese Quellen machen klar: MFA betrifft auch Speicherung, Fehlversuche, Recovery und Faktorwechsel.

Sicherheitsgrenze zuerst

TOTP bedeutet time-based one-time password. Server und Authenticator-App teilen ein Secret und erzeugen meist alle 30 Sekunden einen kurzen Code. MFA erhöht die Vertrauensstufe einer Session, ersetzt aber keine Autorisierung. Admin-Einladungen, Billing-Änderungen, API-Key-Erstellung, Passwortänderung und Datenexport sollten eine frische MFA verlangen.

flowchart TD
  A["Password or SSO login"] --> B{"2FA enabled?"}
  B -->|No| C["Lower-assurance session"]
  B -->|Yes| D["TOTP or backup code challenge"]
  D --> E{"Valid and rate limit OK?"}
  E -->|No| F["Deny and audit"]
  E -->|Yes| G["Session with mfaAt"]
  G --> H{"Sensitive action?"}
  H -->|Yes| I{"mfaAt fresh?"}
  I -->|No| D
  I -->|Yes| J["Allow"]
  H -->|No| J

Geben Sie Claude Code diese Grenze zuerst. Bitten Sie nicht um „baue 2FA“ in einem Stück. Teilen Sie die Arbeit in Datenmodell, Crypto-Helper, Setup-API, Enable-API, Login-Challenge, Recovery, Remembered Device, Audit-Logs und Tests. Produktionsschlüssel, KMS, Support-Regeln und Admin-Rechte bleiben menschliche Entscheidungen.

Implement 2FA for a Next.js App Router app with TypeScript and Prisma.
Scope: TOTP, backup codes, remembered devices, step-up auth, recovery events, audit logs.
Rules:
- Encrypt TOTP secrets with AES-256-GCM before saving.
- Hash backup codes and remembered device tokens with HMAC-SHA256.
- Apply rate limits to setup, enable, login challenge, and recovery.
- Do not make SMS the primary factor.
- Keep a migration path to WebAuthn/passkeys.
First output the Prisma schema and review checklist only.

Prüfen Sie danach hart: TOTP Secret im Klartext, Backup-Codes im Klartext oder Log, wiederverwendbare Codes, kein Rate Limit für sechsstellige Codes, rohe Remembered-Device-Tokens in der Datenbank, Recovery durch eine einzelne Supportperson und fehlende Audit-Events für Aktivierung, Deaktivierung, Fehler, Recovery und Faktorwechsel.

Datenmodell

Das TOTP Secret wird verschlüsselt, weil der Server es später lesen muss. Backup-Codes und Remembered-Device-Tokens werden gehasht, weil nur der Vergleich nötig ist.

// prisma/schema.prisma
model User {
  id                         String              @id @default(cuid())
  email                      String              @unique
  passwordHash               String
  twoFactorEnabled           Boolean             @default(false)
  twoFactorEnabledAt         DateTime?
  twoFactorSecretCiphertext  String?
  twoFactorSecretIv          String?
  twoFactorSecretTag         String?
  backupCodes                BackupCode[]
  rememberedDevices          RememberedDevice[]
  auditLogs                  AuditLog[]
  createdAt                  DateTime            @default(now())
  updatedAt                  DateTime            @updatedAt
}

model BackupCode {
  id        String    @id @default(cuid())
  userId    String
  codeHash  String
  usedAt    DateTime?
  createdAt DateTime  @default(now())
  user      User      @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@index([userId, usedAt])
  @@unique([userId, codeHash])
}

model RememberedDevice {
  id          String   @id @default(cuid())
  userId      String
  deviceHash  String
  userAgent   String?
  expiresAt   DateTime
  lastUsedAt  DateTime?
  createdAt   DateTime @default(now())
  user        User     @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@index([userId, expiresAt])
  @@unique([userId, deviceHash])
}

model AuditLog {
  id        String   @id @default(cuid())
  userId    String?
  action    String
  metadata  Json?
  ipAddress String?
  userAgent String?
  createdAt DateTime @default(now())
  user      User?    @relation(fields: [userId], references: [id], onDelete: SetNull)

  @@index([userId, createdAt])
  @@index([action, createdAt])
}
# .env.example
DATABASE_URL="postgresql://user:password@localhost:5432/app"
TWO_FACTOR_ENCRYPTION_KEY="base64-encoded-32-byte-key"
TWO_FACTOR_HASH_SECRET="long-random-hmac-secret"

Crypto, Hashing und Rate Limit

// src/lib/two-factor.ts
import { createCipheriv, createDecipheriv, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
import { authenticator } from "otplib";

authenticator.options = { step: 30, window: 1 };

const buckets = new Map<string, { count: number; resetAt: number }>();

function encryptionKey() {
  const key = Buffer.from(process.env.TWO_FACTOR_ENCRYPTION_KEY ?? "", "base64");
  if (key.length !== 32) throw new Error("TWO_FACTOR_ENCRYPTION_KEY must be 32 bytes in base64.");
  return key;
}

function hmacSecret() {
  const secret = process.env.TWO_FACTOR_HASH_SECRET;
  if (!secret || secret.length < 32) throw new Error("TWO_FACTOR_HASH_SECRET is too short.");
  return secret;
}

export function encryptSecret(secret: string) {
  const iv = randomBytes(12);
  const cipher = createCipheriv("aes-256-gcm", encryptionKey(), iv);
  const ciphertext = Buffer.concat([cipher.update(secret, "utf8"), cipher.final()]);
  return { ciphertext: ciphertext.toString("base64"), iv: iv.toString("base64"), tag: cipher.getAuthTag().toString("base64") };
}

export function decryptSecret(input: { ciphertext: string; iv: string; tag: string }) {
  const decipher = createDecipheriv("aes-256-gcm", encryptionKey(), Buffer.from(input.iv, "base64"));
  decipher.setAuthTag(Buffer.from(input.tag, "base64"));
  return Buffer.concat([decipher.update(Buffer.from(input.ciphertext, "base64")), decipher.final()]).toString("utf8");
}

export function verifyTotp(token: string, secret: string) {
  return /^\d{6}$/.test(token) && authenticator.verify({ token, secret });
}

export function generateBackupCodes(count = 10) {
  return Array.from({ length: count }, () => {
    const raw = randomBytes(5).toString("hex").toUpperCase();
    return `${raw.slice(0, 5)}-${raw.slice(5)}`;
  });
}

export function hashBackupCode(userId: string, code: string) {
  const normalized = code.replace(/[^a-zA-Z0-9]/g, "").toUpperCase();
  return createHmac("sha256", hmacSecret()).update(`backup:${userId}:${normalized}`).digest("hex");
}

export function hashRememberedDevice(userId: string, token: string) {
  return createHmac("sha256", hmacSecret()).update(`remember:${userId}:${token}`).digest("hex");
}

export function safeEqualHex(a: string, b: string) {
  const left = Buffer.from(a, "hex");
  const right = Buffer.from(b, "hex");
  return left.length === right.length && timingSafeEqual(left, right);
}

export function consumeRateLimit(key: string, limit: number, windowMs: number) {
  const now = Date.now();
  const bucket = buckets.get(key);
  if (!bucket || bucket.resetAt <= now) {
    buckets.set(key, { count: 1, resetAt: now + windowMs });
    return true;
  }
  bucket.count += 1;
  return bucket.count <= limit;
}

Das Map-Limit ist nur für lokale Entwicklung geeignet. In Produktion sollte Claude Code es mit Redis oder Upstash ersetzen, aber die gleiche Signatur behalten.

Enable-API und Backup-Codes

Setup erzeugt Secret und QR-Code, aktiviert aber nicht sofort. Erst ein gültiger TOTP schaltet 2FA ein und erzeugt Backup-Codes, die nur einmal angezeigt werden.

// src/app/api/account/2fa/enable/route.ts
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { prisma } from "@/lib/prisma";
import { requireUser } from "@/lib/require-user";
import { decryptSecret, generateBackupCodes, hashBackupCode, consumeRateLimit, verifyTotp } from "@/lib/two-factor";

const schema = z.object({ token: z.string().regex(/^\d{6}$/) });

export async function POST(request: NextRequest) {
  const user = await requireUser();
  const { token } = schema.parse(await request.json());

  if (!consumeRateLimit(`2fa-enable:${user.id}`, 5, 10 * 60 * 1000)) {
    return NextResponse.json({ error: "Too many attempts." }, { status: 429 });
  }

  const record = await prisma.user.findUniqueOrThrow({ where: { id: user.id } });
  if (!record.twoFactorSecretCiphertext || !record.twoFactorSecretIv || !record.twoFactorSecretTag) {
    return NextResponse.json({ error: "2FA setup has not started." }, { status: 400 });
  }

  const secret = decryptSecret({
    ciphertext: record.twoFactorSecretCiphertext,
    iv: record.twoFactorSecretIv,
    tag: record.twoFactorSecretTag,
  });

  if (!verifyTotp(token, secret)) {
    await prisma.auditLog.create({ data: { userId: user.id, action: "2fa.enable.failed" } });
    return NextResponse.json({ error: "Invalid code." }, { status: 400 });
  }

  const backupCodes = generateBackupCodes();
  await prisma.$transaction([
    prisma.backupCode.deleteMany({ where: { userId: user.id } }),
    prisma.user.update({ where: { id: user.id }, data: { twoFactorEnabled: true, twoFactorEnabledAt: new Date() } }),
    prisma.backupCode.createMany({ data: backupCodes.map((code) => ({ userId: user.id, codeHash: hashBackupCode(user.id, code) })) }),
    prisma.auditLog.create({ data: { userId: user.id, action: "2fa.enabled", metadata: { backupCodeCount: backupCodes.length } } }),
  ]);

  return NextResponse.json({ backupCodes });
}

Beim Login akzeptieren Sie TOTP oder ungenutzten Backup-Code. Ein erfolgreicher Backup-Code bekommt sofort usedAt. Für Remembered Device erzeugen Sie ein zufälliges Token, speichern nur den HMAC und senden das echte Token als httpOnly, secure, sameSite Cookie mit Ablauf und Widerrufsmöglichkeit.

UI-Komponente

// src/components/TwoFactorSetupPanel.tsx
"use client";

import { useState } from "react";

export function TwoFactorSetupPanel() {
  const [qrCodeDataUrl, setQrCodeDataUrl] = useState("");
  const [manualKey, setManualKey] = useState("");
  const [token, setToken] = useState("");
  const [backupCodes, setBackupCodes] = useState<string[]>([]);
  const [error, setError] = useState("");

  async function startSetup() {
    const response = await fetch("/api/account/2fa/setup", { method: "POST" });
    const data = await response.json();
    if (!response.ok) return setError(data.error ?? "Setup failed.");
    setQrCodeDataUrl(data.qrCodeDataUrl);
    setManualKey(data.manualKey);
  }

  async function enable() {
    const response = await fetch("/api/account/2fa/enable", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ token }),
    });
    const data = await response.json();
    if (!response.ok) return setError(data.error ?? "Invalid code.");
    setBackupCodes(data.backupCodes);
  }

  return (
    <section aria-label="Two-factor authentication">
      {!qrCodeDataUrl && <button onClick={startSetup}>Set up 2FA</button>}
      {qrCodeDataUrl && backupCodes.length === 0 && (
        <>
          <img src={qrCodeDataUrl} alt="Authenticator app QR code" width={220} height={220} />
          <p>Manual key: <code>{manualKey}</code></p>
          <input inputMode="numeric" autoComplete="one-time-code" value={token} onChange={(event) => setToken(event.target.value)} maxLength={6} />
          <button onClick={enable}>Verify and enable</button>
        </>
      )}
      {backupCodes.length > 0 && (
        <ul>{backupCodes.map((code) => <li key={code}><code>{code}</code></li>)}</ul>
      )}
      {error && <p role="alert">{error}</p>}
    </section>
  );
}

Recovery, SMS und Passkeys

Recovery ist oft der schwächste Teil. Nutzen Sie zuerst Backup-Codes. Wenn alle Faktoren verloren sind, kombinieren Sie E-Mail-Bestätigung, bestehende vertrauenswürdige Session, Owner-Freigabe im B2B-Fall und Wartezeit vor kritischen Aktionen. Danach alte Sessions widerrufen und Nutzer informieren.

SMS ist manchmal besser als gar kein Faktor, bleibt aber anfällig für SIM-Swap, Nummern-Recycling und Social Engineering beim Provider. Für Admins, Zahlungen und API-Keys sollte SMS nicht der primäre Faktor sein.

WebAuthn/passkeys sind langfristig stärker, weil sie Public-Key-Credentials an den Origin binden. Starten Sie mit sauberem TOTP und ergänzen Sie später eine WebAuthn-Credential-Tabelle mit denselben Audit-, Widerrufs- und Step-Up-Regeln.

// src/lib/step-up.ts
export function assertFreshMfa(session: { mfaAt?: Date | null }, maxAgeMinutes = 15) {
  if (!session.mfaAt) throw new Error("MFA is required.");
  if (Date.now() - session.mfaAt.getTime() > maxAgeMinutes * 60 * 1000) {
    throw new Error("MFA challenge is too old.");
  }
}

Praxisfälle und Stolperfallen

Fall 1: Admin-Einladung. Vor Owner- oder Admin-Einladungen muss mfaAt frisch sein. Eine gestohlene Session darf nicht reichen.

Fall 2: Billing. Kartenänderung, Rechnungs-E-Mail, Rückerstattung und Steuerdaten brauchen Step-Up Auth. Auditieren Sie das Ereignis, nicht Kartendaten oder Backup-Codes.

Fall 3: API-Key-Erstellung. Neue Keys oder größere Scopes brauchen MFA und RBAC, zum Beispiel api_key:create.

Fall 4: Support-Recovery. Support erstellt eine Anfrage, deaktiviert MFA aber nicht direkt. Zwei-Personen-Freigabe, Benachrichtigung, Wartezeit und Audit sind Pflicht.

Typische Fehler sind Klartext-Secrets, wiederverwendbare Backup-Codes, kein Rate Limit, rohe Remembered-Device-Tokens, Deaktivierung ohne Step-Up, zu lange Recovery-Links und Logs mit Secrets.

Abschluss

Gute 2FA ist Session Assurance, Recovery, Audit und Betrieb in einem. Claude Code hilft, wenn die Aufgaben klein und prüfbar bleiben. ClaudeCodeLab kann bestehende MFA-Flows prüfen, Teams in sicheren Claude-Code-Workflows schulen oder eine Next.js-Roadmap von TOTP zu passkeys umsetzen.

Beim Testen prüfen Sie getrennte Schlüssel pro Umgebung, einmalige Anzeige der Backup-Codes, Rate Limit bei TOTP-Fehlern, widerrufbare httpOnly Cookies für Remembered Device, Step-Up beim Deaktivieren von 2FA und Audit-Logs ohne geheime Werte.

#Claude Code #2FA #MFA #TOTP #Sicherheit
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.