Warum eigene 2FA-Integration statt Jetstream/Fortify?
Viele Teams starten nicht „greenfield“. Das Backend steht, die SPA (Vue/React) ist live, der Login läuft über JSON-Endpoints, und sensible Routen hängen an auth:sanctum. Genau in dieser Situation wirkt ein kompletter Wechsel auf Jetstream/Fortify oft wie ein Umbau am offenen Herzen.
Der praktische Weg ist deshalb: 2FA Laravel als gezielte Erweiterung in den bestehenden Auth-Flow setzen – mit klarer Trennung zwischen Passwort-Login und zweitem Faktor, mit sauberen API-Responses (keine HTML-Redirects) und mit UX-Details wie „Trusted Devices“ und Laravel Recovery Codes.
Der Artikel beschreibt eine Implementierung, die in einer realen Laravel-SPA-Architektur funktioniert:
- Laravel API mit
auth:sanctumfür geschützte Endpoints. (laravel.com) - TOTP mit
pragmarx/google2fa-laravel(als Bridge für Google2FA). (packagist.org) - QR-Code serverseitig per SVG, ohne externe Dienste, mit
bacon/bacon-qr-code. (packagist.org)
Wenn Sie Ihre Laravel-Grundlagen zu Routen/Middleware auffrischen wollen, passt als Vorwissen unser Beitrag zu Laravel Routing sehr gut.
Zielbild: 2FA ist Teil des Login- und Request-Lebenszyklus
In einer SPA darf 2FA nicht „ein Modal irgendwann nach dem Login“ sein. Sie brauchen stattdessen einen verlässlichen Zustand:
- Passwort-Login: Zugangsdaten stimmen.
- 2FA-Status: Benutzer hat 2FA aktiv oder muss es einrichten.
- Challenge-Phase: Benutzer ist bewusst noch nicht „voll eingeloggt“, bis TOTP/Recovery-Code geprüft ist.
- Trusted Device: optionaler Komfort, aber kein Abschalten des zweiten Faktors.
Das Ergebnis sind klare Antworten der API:
requires_2fa_setuprequires_2fa_verification
… und im Frontend ein globaler Fehler-Interceptor, der die SPA in Setup/Challenge routet.
Pakete und Stack: was wirklich gebraucht wird
Im Beispiel kommen zwei Composer-Pakete zum Einsatz:
pragmarx/google2fa-laravel:^2.3– Stand heute istv2.3.0auf Packagist gelistet. (packagist.org)bacon/bacon-qr-code:^3.0– auf Packagist ist die 3.x-Linie verfügbar (z. B.v3.0.3), mit PHP-Anforderung^8.1. (packagist.org)
Wichtig für die Planung: Wenn Sie ein älteres PHP im Produktivsystem haben, kann bacon/bacon-qr-code 3.x dadurch ausscheiden. Dann müssen Sie entweder PHP anheben oder eine alternative QR-Code-Bibliothek wählen.
Datenmodell: 2FA-Daten direkt am User – aber verschlüsselt
Die Implementierung speichert 2FA-Zustand und Geräte-Vertrauen in der users-Tabelle. Das reduziert Join-Komplexität und macht den Flow einfacher.
Migrationsschema:
$table->text('two_factor_secret')->nullable();
$table->text('two_factor_recovery_codes')->nullable();
$table->timestamp('two_factor_confirmed_at')->nullable();
$table->json('two_factor_device_fingerprints')->nullable();
Warum confirmed_at Pflicht ist
Ein generiertes Secret ist noch keine aktivierte 2FA. Nutzer brechen Setup-Prozesse ab, wechseln Geräte, schließen Tabs.
Darum gilt: 2FA ist nur aktiv, wenn Secret vorhanden und Setup bestätigt ist.
Im User-Modell:
public function hasTwoFactorEnabled(): bool
{
return ! is_null($this->two_factor_secret)
&& ! is_null($this->two_factor_confirmed_at);
}
Verschlüsselung: Secrets und Recovery Codes gehören nicht im Klartext in die DB
Laravel bringt dafür eine eingebaute Verschlüsselungsschicht mit (Crypt/Helper), die OpenSSL nutzt und auf dem APP_KEY basiert. (laravel.com)
In der Praxis bedeutet das:
two_factor_secretwird alsencrypt($secret)gespeichert.- Recovery-Codes werden als JSON serialisiert und ebenfalls verschlüsselt gespeichert.
Das schützt vor „DB-Leak = sofort alle Tokens weg“. Es ersetzt aber nicht grundlegende Infrastruktur-Sicherheit.
Mehr zu DB-Design und Migrationen finden Sie auch in unserem Artikel zu Laravel Datenbank.
Setup-Flow: QR-Code erzeugen, bestätigen, Recovery Codes sichern
Der Setup-Flow in einer SPA funktioniert gut als Stepper:
- QR-Code erzeugen (plus Secret als manuelle Fallback-Anzeige)
- TOTP-Code bestätigen
- Recovery Codes anzeigen und explizit bestätigen lassen
Step 1: Secret erzeugen und QR-Code als SVG liefern
Beispiel (Enable-Endpoint):
$secret = $this->google2fa->generateSecretKey();
$user->two_factor_secret = encrypt($secret);
$user->two_factor_confirmed_at = null;
$user->two_factor_recovery_codes = null;
$user->two_factor_device_fingerprints = null;
$user->save();
Danach erzeugt das Backend ein SVG (mit BaconQrCode) und gibt es an die SPA zurück. Vorteil: Kein externes QR-Service, kein Datenabfluss.
Zu bacon/bacon-qr-code: Das Paket ist ein QR-Code-Generator für PHP und wird breit genutzt. (packagist.org)
Step 2: TOTP bestätigen (TOTP Laravel)
Der Benutzer tippt den 6-stelligen Code aus Authenticator-App.
Serverseitig:
- Secret entschlüsseln
- TOTP prüfen
- bei Erfolg
two_factor_confirmed_atsetzen
pragmarx/google2fa-laravel ist genau für „Codes prüfen und QR-Codes erzeugen“ gedacht. (packagist.org)
Step 3: Laravel Recovery Codes generieren und speichern
Recovery Codes sind die Notfall-Option, wenn das Handy weg ist.
Im Projekt:
- 8 Codes
- One-Time: ein verwendeter Code wird aus der Liste entfernt
- Sicherheitsentscheidung: Nach Verwendung eines Recovery Codes wird 2FA komplett zurückgesetzt und neu eingerichtet
Das ist nicht die einzige mögliche Strategie, aber eine konsistente: Wer gerade einen Notfallcode gebraucht hat, hatte wahrscheinlich ein Gerät verloren oder gewechselt. Eine Neuinitialisierung macht den Zustand wieder sauber.
Login-Flow: Passwort-Login in „pending“ Zustand aufteilen
Klassisches Problem bei SPAs: Sie möchten nach Passwort-Login nicht sofort ein vollwertiges Token/Session-Flag setzen, wenn 2FA aktiv ist.
Die Lösung hier: Brücke über die Session.
- Nach erfolgreichem Passwort-Check wird geprüft, ob 2FA aktiv ist.
- Ist das Gerät nicht vertraut, setzt das Backend
2fa_pending_user_idin die Session und loggt wieder aus. - Die API antwortet mit
requires_2fa_verification.
Laravel unterstützt Session-Zugriff über session() Helper bzw. Request-Session-API. (laravel.com)
Beispiel-Logik:
if ($user->hasTwoFactorEnabled()) {
$fingerprint = $this->getDeviceFingerprint($request, $clientFingerprint);
$isDeviceTrusted = $user->isDeviceTrusted($fingerprint, $clientFingerprint);
if (! $isDeviceTrusted) {
session(['2fa_pending_user_id' => $user->id]);
Auth::logout();
return response()->json([
'requires_2fa_verification' => true,
'pending_user_id' => $user->id,
]);
}
}
if (! $user->hasTwoFactorEnabled()) {
return response()->json([
'requires_2fa_setup' => true,
]);
}
Hinweis aus der Praxis: Die Reihenfolge ist Absicht. Erst prüfen, ob 2FA aktiv ist. Nur wenn nicht aktiv, Setup erzwingen.
Verifikation: POST /api/2fa/verify als „Sonder-Endpunkt“
Der Verify-Endpunkt bekommt:
- TOTP-Code oder Recovery Code
- optional: „dieses Gerät 90 Tage vertrauen“
Bei Erfolg:
- User wird final eingeloggt
- Session-Flag
2fa_verifiedwird gesetzt
Warum das sauber ist:
- Während der Challenge ist der User nicht normal authentifiziert.
- Die Session (
2fa_pending_user_id) hält den Übergang stabil. - Nach Erfolg wird aus „pending“ ein regulärer Login.
Trusted Devices: Komfort ohne 2FA-Abschaltung
„Trusted Device“ heißt nicht „2FA ist aus“. Es heißt: Für dieses Gerät überspringt die App die erneute Abfrage für einen begrenzten Zeitraum.
Im Projekt:
- Speicherung pro Gerät (Fingerprint + Metadaten)
- Ablaufdatum (
expires_at) nach 90 Tagen - maximal 10 Devices pro User
Beispiel-Datenstruktur:
fingerprintuser_agentadded_atexpires_at- optional
client_fingerprint - optional
device_info
Fingerprint-Strategie: keine IP-Adresse (mobilfreundlich)
Viele „Device Fingerprints“ brechen auf Mobilgeräten, wenn IPs häufig wechseln (Mobilfunk, VPN, Hotel-WLAN). Die Implementierung nutzt deshalb serverseitig eher stabile Faktoren:
- normalisierter User-Agent
Accept-LanguageAccept-Encoding- optional einen zusätzlichen Client-Fingerprint aus dem Browser
Die SPA kann einen eigenen Fingerprint aus Browser-Eigenschaften erzeugen (Zeitzone, Sprache, Hardware Concurrency etc.) und in jedem Request als Header senden, z. B. X-Device-Fingerprint.
Wichtig: Fingerprints sind nie „magisch eindeutig“. Sie sind ein pragmatischer Kompromiss für UX. Halten Sie das Zeitfenster begrenzt und bieten Sie eine Geräteverwaltung an.
Middlewares: 2FA zentral erzwingen – ohne SPA-Redirects
Der Kernpunkt für eine bestehende API: Sie wollen 2FA nicht „an ein paar Endpoints“ kleben. Sie wollen sie zentral erzwingen, so wie Sie auch auth:sanctum zentral nutzen.
Sanctum schützt Endpoints typischerweise über auth:sanctum. (laravel.com)
Darüber legen Sie zwei eigene Middlewares:
Require2FASetup: blockt User ohne aktivierte 2FA mitrequires_2fa_setupVerify2FA: blockt User mit aktivierter 2FA, aber ohne verifizierte Session und ohne Trusted Device, mitrequires_2fa_verification
Der wichtigste Stolperstein: 2FA-Routen ausnehmen
Wenn Ihre Middleware auch /api/2fa/verify blockt, sperren Sie Nutzer zuverlässig aus. Nehmen Sie daher Ihre 2FA-Setup- und 2FA-Verify-Endpunkte explizit aus.
Warum JSON-Flags statt Redirects
Eine SPA braucht Zustandsflags. HTML-Redirects sind bei API-first Clients meist nur schwer sauber zu verarbeiten.
Frontend in der SPA: Interceptor + Router statt „2FA als Sonderfall“
In Vue (analog in React) hat sich folgende Struktur bewährt:
- Axios Request-Interceptor setzt
X-Device-Fingerprint - Axios Response-Interceptor reagiert auf
403und prüft Flags requires_2fa_setup→ RouteTwoFactorSetuprequires_2fa_verification→ RouteTwoFactorChallenge
Damit behandeln Sie 2FA wie einen Teil des Auth-Zustands, nicht wie ein Popup.
Wenn Sie viel mit Vue-Routing arbeiten, passt unser Beitrag zur Back-Funktion in Vue mit Vue Router als ergänzender Baustein, weil 2FA-Flows oft „Zurück“-Logik und Guarding brauchen.
Betrieb und Administration: Deaktivieren, Reset, Device-Management
In der Realität brauchen Sie mehr als „Setup“:
- 2FA deaktivieren (mit Passwort-Bestätigung)
- einzelne Trusted Devices entfernen
- Recovery Codes neu generieren
- Admin-Reset für Benutzer (UI + CLI)
Für Support-Fälle ist ein CLI-Command Gold wert, weil Sie nicht auf UI oder DB-Manual-Fixes angewiesen sind.
Dev-Bypass: praktisch, aber hart eingezäunt
Für lokale Entwicklung ist ein Bypass wie 000000 bequem, solange Sie ihn streng begrenzen:
- nur in Nicht-Produktionsumgebungen
- nur für
localhostoder.test - im Code klar dokumentieren
- idealerweise zusätzlich über Feature-Flag oder ENV aktivierbar
Sobald so ein Bypass in Staging/Prod landet, verlieren Sie den gesamten Sicherheitsgewinn.
Stolperfallen aus der Praxis (die erfahrungsgemäß wirklich passieren)
- 2FA zu früh „aktiv“ setzen: Secret generiert ≠ Setup bestätigt. Nutzen Sie
two_factor_confirmed_at. - Unverschlüsselte Secrets: Speichern Sie Secret und Recovery Codes ausschließlich verschlüsselt (Laravel Encrypter). (laravel.com)
- Trusted Devices ohne Ablauf: Dann wird aus „Komfort“ schnell „dauerhaft ohne 2FA“.
- 2FA-Endpunkte nicht ausgenommen: Ergebnis ist eine 403-Schleife.
- Backend denkt in Redirects: SPA braucht Flags und Frontend-Routing.
- IP als Fingerprint-Bestandteil: mobil unbrauchbar.
Wenn Sie ohnehin gerade an einer sauberen API-Struktur arbeiten, kann auch unser Beitrag zur automatischen Generierung von API-Dokumentation in Laravel helfen, weil 2FA-Flags (requires_2fa_*) dokumentiert werden sollten.
Wenn Sie zu solchen Praxis-Implementierungen mehr lesen möchten, finden Sie auf dem ADMIN CODE Blog weitere Laravel-Artikel. Und falls Sie bei der Absicherung eines produktiven Systems Unterstützung brauchen, können Sie uns über die Kontaktseite erreichen: https://www.admin-intelligence.de/kontakt/

Anwendungsentwickler
ADMIN INTELLIGENCE GmbH