Security: JSON-Login durchläuft die is_active-/Verifizierungschecks

RoleAwareLoginResponse gab bei wantsJson() sofort 204 zurück – VOR den
Sicherheitschecks. Ein XHR/JSON-Login eines verifiziert-inaktiven Accounts
erhielt damit eine Session ohne Logout. Checks laufen jetzt zuerst:
verifiziert-inaktiv → Logout + Session-Invalidate + 403 (JSON) bzw. Login mit
Fehler (HTML); unverifiziert → 204 (JSON) bzw. Notice (HTML); danach der
Erfolgsfall.

Tests: JSON-Login eines inaktiven Accounts (403, guest), JSON-Login eines
aktiven Users (204, authentifiziert).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-06-16 10:19:32 +00:00
parent f4ca452c6b
commit ae79d5bee4
3 changed files with 52 additions and 16 deletions

View file

@ -11,34 +11,46 @@ use Laravel\Fortify\Contracts\LoginResponse as LoginResponseContract;
use Laravel\Fortify\Contracts\TwoFactorLoginResponse as TwoFactorLoginResponseContract;
/**
* Einheitliche Antwort für den Fortify-POST-Login UND den Abschluss der
* 2FA-Challenge. Spiegelt dieselbe Policy wie der Volt-Login:
* - unverifiziert Verifizierungs-Notice
* - verifiziert, aber inaktiv Login blockiert (Logout + Fehler)
* Einheitliche Antwort für den Fortify-POST-Login (HTML und JSON/XHR) UND den
* Abschluss der 2FA-Challenge. Spiegelt dieselbe Policy wie der Volt-Login:
* - verifiziert, aber inaktiv Session beenden, KEIN Login (auch für JSON)
* - unverifiziert Verifizierungs-Notice (JSON: 204, verified-Middleware schützt)
* - sonst rollengerechter, 403-sicherer Redirect (intended nur wenn erreichbar)
*
* Wichtig: Die Sicherheitsprüfungen laufen VOR dem JSON-Kurzschluss, damit ein
* XHR-Login keine Session für einen deaktivierten Account erhält.
*/
class RoleAwareLoginResponse implements LoginResponseContract, TwoFactorLoginResponseContract
{
public function toResponse($request): RedirectResponse|JsonResponse
{
if ($request instanceof Request && $request->wantsJson()) {
return new JsonResponse('', 204);
}
$user = $request->user();
$wantsJson = $request instanceof Request && $request->wantsJson();
if ($user && ! $user->hasVerifiedEmail()) {
return redirect()->route('verification.notice');
}
if ($user && ! $user->is_active) {
// Sicherheits-Boundary zuerst: ein verifizierter, aber deaktivierter
// Account darf keine Session behalten egal ob HTML oder JSON.
if ($user && $user->hasVerifiedEmail() && ! $user->is_active) {
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('login')->withErrors([
'email' => __('Ihr Konto ist nicht aktiv. Bitte wenden Sie sich an den Support.'),
]);
$message = __('Ihr Konto ist nicht aktiv. Bitte wenden Sie sich an den Support.');
return $wantsJson
? new JsonResponse(['message' => $message], 403)
: redirect()->route('login')->withErrors(['email' => $message]);
}
// Unverifizierte Selbst-Registrierer sind authentifiziert, aber die
// verified-Middleware sperrt geschützte Routen. HTML → Notice, JSON → 204.
if ($user && ! $user->hasVerifiedEmail()) {
return $wantsJson
? new JsonResponse('', 204)
: redirect()->route('verification.notice');
}
if ($wantsJson) {
return new JsonResponse('', 204);
}
$default = LoginRedirect::homeFor($user);