Authentifizierung ist kein einzelner Login-Controller. Sie ist ein Lebenszyklus aus Registrierung, Anmeldung, Session, E-Mail-Verifizierung, Passwort-Reset, Multi-Faktor-Authentifizierung, sensiblen Kontoänderungen und Abmeldung. Eine Laravel-Anwendung kann in jedem einzelnen Schritt die richtigen Framework-Funktionen verwenden und als Gesamtsystem trotzdem angreifbar sein.
Sicher ist ein Konto erst dann, wenn Identität, Sitzung und Berechtigung bei jedem Zustandswechsel zusammenpassen.
Dieser Leitfaden betrachtet die Stellen, an denen in produktiven Laravel-Anwendungen besonders häufig Sicherheitsannahmen auseinanderlaufen. Er ergänzt den Überblick 10 häufige Sicherheitsfehler in Laravel-Projekten um eine gezielte Prüfung des gesamten Konto-Lebenszyklus.
Das Authentifizierungsmodell zuerst dokumentieren
Vor der Implementierung sollten fünf Fragen eindeutig beantwortet sein:
- Welche Guards authentifizieren Browser, API, Administration und interne Dienste?
- Welche Credentials werden automatisch vom Browser gesendet und benötigen deshalb CSRF-Schutz?
- Welche Aktionen verlangen nur eine aktive Session und welche eine kürzlich bestätigte Identität?
- Wann werden Sessions, API-Token, Remember-Me-Token und Recovery-Codes widerrufen?
- Welche Kanäle können ein gesperrter oder noch nicht verifizierter Benutzer weiterhin erreichen?
Laravel trennt Guards und User Provider. Diese technische Trennung ist hilfreich, ersetzt aber keine fachliche Zustandsmaschine. Ein Konto kann beispielsweise pending, active, suspended oder recovery_locked sein. Dieselbe Regel muss für Web-Routen, APIs, Broadcast Channels, Queue-Aktionen und administrative Impersonation gelten.
Eine Route-Gruppe macht einen Teil der Annahmen sichtbar:
Route::middleware(['auth', 'auth.session', 'verified', EnsureUserIsActive::class])
->group(function (): void {
Route::get('/dashboard', DashboardController::class);
Route::post('/exports', CreateExportController::class)
->middleware('password.confirm');
});
Empfehlung: Erstellen Sie eine kleine Matrix aus Kanal, Guard, Kontostatus und zusätzlicher Bestätigung. Ein Middleware-Eintrag ausschließlich in routes/web.php schützt weder API-Routen noch Broadcast Channels automatisch.
Login gegen Enumeration und Brute Force absichern
Unterschiedliche Antworten wie „E-Mail unbekannt“, „Passwort falsch“ oder stark abweichende Laufzeiten helfen Angreifern beim Ermitteln gültiger Konten. Die öffentliche Antwort sollte deshalb unabhängig davon bleiben, welcher Teil der Anmeldung fehlgeschlagen ist.
Rate Limiting sollte mindestens Konto und Quelladresse berücksichtigen. Nur die IP zu begrenzen trifft gemeinsam genutzte Netze; nur die E-Mail zu begrenzen erlaubt verteilte Angriffe oder gezieltes Aussperren.
RateLimiter::for('login', function (Request $request): array {
$email = Str::lower((string) $request->input('email'));
return [
Limit::perMinute(60)->by('ip:'.$request->ip()),
Limit::perMinute(10)->by('account:'.hash('sha256', $email)),
Limit::perMinute(5)->by('pair:'.hash('sha256', $email.'|'.$request->ip())),
];
});
Nach erfolgreicher Anmeldung wird die Session-ID erneuert:
if (! Auth::attempt($credentials, $request->boolean('remember'))) {
throw ValidationException::withMessages([
'email' => __('auth.failed'),
]);
}
$request->session()->regenerate();
return redirect()->intended(route('dashboard'));
Logging sollte fehlgeschlagene Muster sichtbar machen, aber keine Passwörter, vollständigen Tokens oder unnötige personenbezogene Daten speichern. CAPTCHA kann bei konkretem Missbrauch eine zusätzliche Stufe sein, ist jedoch kein Ersatz für serverseitige Limits.
Empfehlung: Testen Sie gültige und ungültige Konten, verteilte IP-Adressen, Groß- und Kleinschreibung der E-Mail sowie parallele Anmeldeversuche. Beobachten Sie außerdem, ob SSO- oder API-Login dieselben Schutzregeln umgehen.
Session-Cookies bewusst konfigurieren
Für eine gewöhnliche, ausschließlich über HTTPS erreichbare Laravel-Anwendung sind Secure, HttpOnly und ein explizites SameSite=Lax ein belastbarer Ausgangspunkt:
SESSION_SECURE_COOKIE=true
SESSION_HTTP_ONLY=true
SESSION_SAME_SITE=lax
HttpOnly verhindert den direkten Zugriff per JavaScript, macht eine Session aber nicht immun gegen XSS. Secure beschränkt die Übertragung auf TLS. SameSite=Lax reduziert Cross-Site-Requests mit automatisch gesendeten Cookies, ersetzt jedoch nicht Laravels CSRF-Prüfung für zustandsverändernde Anfragen.
SameSite=None ist kein allgemeiner Fix für getrennte Frontends. Moderne Browser akzeptieren diese Einstellung nur zusammen mit Secure; das Cookie kann dann auch bei Cross-Site-Requests gesendet werden und erweitert damit die CSRF-Angriffsfläche. Wenn ein eingebetteter Anwendungsfall diese Einstellung tatsächlich benötigt, sollten dafür möglichst getrennte Cookies, eng begrenzte Origins und vollständiger CSRF-Schutz eingesetzt werden.
Der Unterschied zwischen Site und Origin ist wichtig. Subdomains können zur selben Site gehören, obwohl sie unterschiedliche Origins sind. Eine übernommene oder fremd kontrollierte Subdomain kann dadurch für Cookie- und CORS-Entscheidungen relevant werden.
Empfehlung: Prüfen Sie die tatsächlich ausgelieferten Set-Cookie-Header in Produktion. Tests gegen Konfigurationswerte allein entdecken keine fehlerhaften Proxy-, Domain- oder TLS-Einstellungen.
CSRF und HTTP-Methoden nicht vermischen
Browser senden passende Session-Cookies automatisch. Deshalb benötigen browserbasierte, durch Cookies authentifizierte POST-, PUT-, PATCH- und DELETE-Requests einen CSRF-Schutz. Laravels web-Middleware-Gruppe schützt diese Methoden standardmäßig. In Laravel 13 akzeptiert PreventRequestForgery einen nachweislich gleich-originigen Request über Sec-Fetch-Site: same-origin; andernfalls wird wie bisher das Session-Token geprüft. Ältere Browser ohne diesen Header funktionieren deshalb weiterhin über den Token-Fallback. Eine GET-Route sollte keine Bestellung auslösen, kein Konto löschen und keine Berechtigung ändern.
// Unsicher: Ein Link oder externes Bild kann die Aktion auslösen.
Route::get('/projects/{project}/archive', ArchiveProjectController::class);
// Besser: zustandsverändernde Methode, CSRF und Autorisierung.
Route::delete('/projects/{project}', DeleteProjectController::class)
->middleware(['auth', 'can:delete,project']);
Für tokenbasierte APIs gilt eine andere Annahme: Wird das Token ausschließlich explizit im Authorization-Header gesendet, kann eine fremde Website es nicht automatisch mitsenden. Sobald dieselbe API zusätzlich Session-Cookies akzeptiert, muss der Cookie-Pfad wie eine Browser-Anwendung gegen CSRF geschützt werden.
Empfehlung: Inventarisieren Sie alle schreibenden Routes und prüfen Sie Methode, Guard, CSRF, Autorisierung und Rate Limit gemeinsam. Verlassen Sie sich nicht auf die Dateizuordnung web.php oder api.php als Sicherheitsgrenze.
Sessions richtig erneuern und beenden
Bei erfolgreichem Login verhindert session()->regenerate() Session Fixation. Beim Logout sollten Authentifizierungsdaten, Session und CSRF-Token gemeinsam erneuert werden:
public function destroy(Request $request): RedirectResponse
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
Eine Passwortänderung wirft nicht zwangsläufig alle anderen Geräte aus dem Konto. Laravel kann mit auth.session und Auth::logoutOtherDevices() passwortbasierte Sessions auf anderen Geräten invalidieren: Die Middleware vergleicht einen in der Session abgelegten Passwort-Hash mit dem aktuellen Hash des Benutzers. Persönliche API-Token oder andere Guards sind davon nicht automatisch erfasst. Das Produkt muss entscheiden, wann welche Zugangsmittel zwingend widerrufen werden, beispielsweise nach Passwort-Reset, erkanntem Account Takeover oder Entfernung eines MFA-Faktors.
Auch langlebige Worker und getrennte Session-Stores gehören in den Test. Eine erfolgreiche Datenbankänderung garantiert nicht, dass jeder aktive Token-Pfad sofort widerrufen wurde.
Empfehlung: Definieren Sie Widerrufsregeln für Session, Remember-Me, persönliche API-Token, OAuth-Token, Magic Links und Recovery-Codes. Testen Sie anschließend jedes Credential nach dem Sicherheitsereignis erneut.
Passwort-Reset als Hochrisiko-Ablauf behandeln
Der Passwort-Reset übernimmt faktisch die Rolle eines alternativen Login-Verfahrens. Er braucht generische Antworten, Rate Limits, kurzlebige und einmal verwendbare Token sowie einen klaren Umgang mit bestehenden Sessions.
Route::post('/forgot-password', SendPasswordResetLinkController::class)
->middleware('throttle:password-reset');
RateLimiter::for('password-reset', fn (Request $request) => [
Limit::perMinute(3)->by(Str::lower((string) $request->input('email'))),
Limit::perMinute(20)->by($request->ip()),
]);
Die öffentliche Antwort sollte auch bei unbekannter E-Mail denselben Ablauf signalisieren. Laravels Password Broker liefert intern unterschiedliche Statuswerte; der Controller muss daraus bewusst dieselbe öffentliche Antwort erzeugen. Nach erfolgreichem Reset werden andere Sessions und risikorelevante Token gemäß Ihrer Widerrufsstrategie ungültig gemacht. Der Benutzer erhält eine Sicherheitsbenachrichtigung, die selbst keinen sensiblen Link oder Zugangsdaten im Klartext enthält.
MFA-Recovery darf den zweiten Faktor nicht geräuschlos wirkungslos machen. Recovery-Codes sind geheime Einmal-Credentials, müssen gehasht oder angemessen geschützt gespeichert, einzeln widerrufen und nach Verwendung ersetzt werden.
Der Datenbank-Password-Broker speichert den Token gehasht, berücksichtigt Ablauf und Ausgabe-Drosselung und löscht ihn nach erfolgreichem Reset. Prüfung und Löschung liegen jedoch nicht automatisch in einer anwendungsweiten Sperre. Wenn zwei exakt parallele Verwendungen desselben Tokens Teil des Bedrohungsmodells sind, muss der Reset-Ablauf zusätzlich atomar serialisiert werden, beispielsweise über einen einmaligen Vorgangsdatensatz mit Unique Constraint oder Lock.
Empfehlung: Testen Sie abgelaufene, manipulierte und erneut verwendete Reset-Token. Für Anwendungen mit erhöhtem Schutzbedarf gehört zusätzlich ein echter Concurrency-Test in die Suite, bei dem höchstens ein paralleler Request erfolgreich sein darf.
E-Mail-Wechsel in einen ausstehenden Zustand versetzen
Wird die primäre E-Mail-Adresse sofort überschrieben, kann ein Tippfehler die Kontowiederherstellung zerstören. Bei privilegierten Domains kann eine ungeprüfte Adresse sogar eine Rollenentscheidung beeinflussen.
Ein belastbarer Ablauf speichert die neue Adresse zunächst separat:
$request->validate([
'email' => ['required', 'email:rfc', 'unique:users,email'],
]);
$request->user()->forceFill([
'pending_email' => Str::lower($request->string('email')->toString()),
'pending_email_token' => hash('sha256', $token = Str::random(64)),
'pending_email_expires_at' => now()->addMinutes(30),
])->save();
Erst nach Bestätigung wird pending_email zur primären Adresse. Für die Änderung sind eine kürzlich bestätigte Identität und eine Benachrichtigung an die bisherige Adresse sinnvoll. Eine Domain-basierte Berechtigung darf nie allein auf einer unbestätigten Adresse beruhen.
Empfehlung: Trennen Sie aktuelle, ausstehende und verifizierte Adresse im Datenmodell. Testen Sie Kollisionen, abgelaufene Links, gleichzeitige Änderungen und den Widerruf durch den bisherigen Kontoinhaber.
Signierte URLs an die richtige Identität binden
Eine gültige Laravel-Signatur belegt, dass eine URL nicht verändert wurde. Sie beweist nicht automatisch, für welchen Benutzer oder fachlichen Zustand die URL gedacht war.
$url = URL::temporarySignedRoute(
'magic-login.consume',
now()->addMinutes(10),
['login_attempt' => $attempt->uuid],
);
Route::get('/magic-login/{login_attempt}', ConsumeMagicLoginController::class)
->middleware(['guest', 'signed'])
->name('magic-login.consume');
Der serverseitige Datensatz zu der nicht erratbaren login_attempt-UUID enthält die erwartete Identität, eine eigene Ablaufzeit und consumed_at. Beim Abruf werden URL-Signatur und Datensatz geprüft und die Verwendung atomar verbraucht. Die eigene Ablaufprüfung bleibt absichtlich bestehen, damit die fachliche Gültigkeit nicht nur vom URL-Parameter abhängt:
$userId = DB::transaction(function () use ($request): int {
$attempt = LoginAttempt::query()
->lockForUpdate()
->where('uuid', $request->route('login_attempt'))
->whereNull('consumed_at')
->where('expires_at', '>', now())
->firstOrFail();
$attempt->update(['consumed_at' => now()]);
return $attempt->user_id;
});
Auth::loginUsingId($userId);
$request->session()->regenerate();
Dass eine signierte Route kontextgebunden sein muss, gilt ebenso für Einladungen, Vorschauen und Downloads. Berechtigungen können sich nach Erzeugung des Links ändern und müssen bei sensiblen Ressourcen erneut geprüft werden.
Empfehlung: Verwenden Sie eine nicht erratbare, fachlich eindeutige Referenz und machen Sie hochprivilegierte Links einmalig nutzbar. Die URL-Signatur ist eine Integritätskontrolle, kein vollständiges Autorisierungsmodell.
MFA schützt mehr als den Login
Ein zweiter Faktor beim Login hilft wenig, wenn Passwort, E-Mail, Auszahlungsziel oder MFA-Gerät anschließend ohne erneute Bestätigung geändert werden können. Kritische Aktionen benötigen Step-up Authentication mit einer kurzen Gültigkeit.
Route::post('/account/mfa/replace', ReplaceMfaController::class)
->middleware(['auth', 'password.confirm', RequireRecentMfa::class]);
TOTP-Secrets und Recovery-Codes sind Zugangsdaten, benötigen aber unterschiedliche Speicherung. Das TOTP-Secret muss zur Codeprüfung wieder verfügbar sein und wird deshalb verschlüsselt gespeichert. Recovery-Codes können einzeln gehasht werden, weil bei ihrer Verwendung nur ein Vergleich nötig ist. Neue Faktoren werden erst aktiv, nachdem ein gültiger Code bestätigt wurde. Der alte Faktor bleibt bis zum sicheren Abschluss verfügbar oder wird über einen getrennten Recovery-Prozess ersetzt.
Empfehlung: Schreiben Sie Tests für Enrollment, Bestätigung, Replay, Zeitabweichung, Recovery-Code-Verbrauch, Faktorwechsel und vollständige Kontowiederherstellung. Support-Mitarbeiter dürfen MFA nicht durch eine informelle Einzelentscheidung umgehen.
Eine negative Testmatrix aufbauen
Eine sichere Authentifizierungs-Suite testet nicht nur den erfolgreichen Login:
| Ablauf | Negativer Sicherheitsfall | Erwartung |
|---|---|---|
| Login | gültige E-Mail, falsches Passwort, sechster Versuch | begrenzt, keine Enumeration |
| Session | vor Login gesetzte Session-ID | nach Login ersetzt |
| Logout | alter Cookie wird erneut verwendet | nicht authentifiziert |
| Passwort-Reset | Token zweimal parallel verwendet | genau eine Änderung |
| E-Mail-Wechsel | neue Adresse nicht bestätigt | alte Adresse bleibt primär |
| Magic Link | gültiger Link für anderen Kontext | kontrollierte Ablehnung |
| MFA | Recovery-Code zweimal verwendet | zweiter Versuch scheitert |
| Sperrung | gesperrtes Konto nutzt API oder Broadcast | kein Zugriff |
Browsernahe Tests sollten zusätzlich Cookie-Attribute, Redirects, CORS und CSRF-Verhalten prüfen. Unit Tests einer Controller-Methode sehen nicht, welche Header ein Reverse Proxy tatsächlich ausliefert.
Praktische Review-Checkliste
- Alle Guards, Login-Wege und Credential-Typen sind inventarisiert.
- Login und Recovery antworten ohne Konto-Enumeration und besitzen abgestufte Rate Limits.
- Session-IDs rotieren bei Login und Session-Invalidierung.
- Cookies sind in Produktion explizit mit
Secure,HttpOnlyund passendemSameSitekonfiguriert. - Schreibende Browser-Requests verwenden sichere HTTP-Methoden und CSRF-Schutz.
- Passwort-Reset, E-Mail-Wechsel und MFA besitzen Einmaligkeit, Ablauf und Benachrichtigung.
- Kritische Kontoaktionen verlangen eine aktuelle Passwort- oder MFA-Bestätigung.
- Sperrung und Widerruf gelten auch für API, Broadcasts, Jobs und bestehende Sessions.
- Signierte Links enthalten eine eindeutige Identitäts- oder Vorgangsbindung.
- Negative Tests belegen jeden relevanten Zustandswechsel.
Verwandte Laravel-Sicherheitsartikel
- 10 häufige Sicherheitsfehler in Laravel-Projekten
- Mandantenfähigkeit und Berechtigungen in Laravel richtig trennen
- Laravel Security Audit: praktische Checkliste
Quellen und Vertiefung
- Laravel 13: Authentication
- Laravel 13: Email Verification
- Laravel 13: Rate Limiting
- Laravel 13: URL Generation and Signed URLs
- OWASP: Authentication Cheat Sheet
- OWASP: Session Management Cheat Sheet
- Securing Laravel: SameSite Cookies
- Securing Laravel: The Signed URL Trap
Fazit
Laravel liefert gute Bausteine für Authentifizierung und Sessions. Die entscheidende Sicherheitsleistung besteht darin, diese Bausteine über den gesamten Konto-Lebenszyklus konsistent zu verbinden. Wer Zustände, den Widerruf von Zugangsmitteln und negative Tests explizit modelliert, verhindert jene Lücken, die zwischen zwei einzeln vernünftigen Implementierungen entstehen.