presseportale/app/Providers/FortifyServiceProvider.php
Kevin Adametz 5dfd622141 security: Andere Geräte abmelden, IP-weites Login-Limit, dauerhafte Sperrmarke, Passwortregeln, CSP im Melde-Modus
Restliche Phase 4 aus dem Sicherheitsaudit 07.10.2026:

- Sicherheitsseite: „Andere Geräte abmelden“ rotiert remember_token und
  beendet alle anderen Sessions, die aktuelle bleibt (nicht während einer
  Impersonation).
- LoginIpThrottle: 20 Fehlversuche je IP in 15 Minuten über alle Konten.
  Gezählt über das Failed-Event, damit Livewire-Login und Fortifys
  POST /login denselben Zähler teilen; Erfolg setzt ihn nicht zurück.
- Sperrmarke für Sessions in users.sessions_revoked_at_ms statt nur im
  Cache — übersteht cache:clear. Die alte Cache-Marke zählt übergangsweise
  mit und kann im nächsten Release entfallen.
- Neue und geänderte Passwörter: mindestens 12 Zeichen, in Produktion
  zusätzlich uncompromised() (HIBP, k-Anonymität). Bestehende bleiben gültig.
- Content-Security-Policy-Report-Only in Produktion; Meldungen über
  /csp-report nach storage/logs/csp-*.log, je Meldung höchstens einmal pro
  Stunde, ohne Query und tokenartige Pfadsegmente.

Migration: users.sessions_revoked_at_ms (users hat FULLTEXT, 25 MB, Sekunden).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 13:02:49 +00:00

90 lines
3.9 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Providers;
use App\Actions\Fortify\CreateNewUser;
use App\Actions\Fortify\ResetUserPassword;
use App\Actions\Fortify\UpdateUserPassword;
use App\Http\Responses\FailedTwoFactorChallengeResponse;
use App\Http\Responses\PasskeyLoginResponse;
use App\Http\Responses\RoleAwareLoginResponse;
use App\Models\User;
use App\Services\Auth\LoginIpThrottle;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;
use Laravel\Fortify\Actions\RedirectIfTwoFactorAuthenticatable;
use Laravel\Fortify\Contracts\FailedTwoFactorLoginResponse;
use Laravel\Fortify\Contracts\LoginResponse;
use Laravel\Fortify\Contracts\TwoFactorLoginResponse;
use Laravel\Fortify\Fortify;
use Laravel\Passkeys\Contracts\PasskeyLoginResponse as PasskeyLoginResponseContract;
use Laravel\Passkeys\Contracts\PasskeyUser;
use Laravel\Passkeys\Passkey;
use Laravel\Passkeys\Passkeys;
class FortifyServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
$this->app->singleton(LoginResponse::class, RoleAwareLoginResponse::class);
$this->app->singleton(TwoFactorLoginResponse::class, RoleAwareLoginResponse::class);
// Fortifys Default leitet auf die nicht registrierte Route
// `two-factor.login` (views => false) – zurück zur Livewire-Challenge.
$this->app->singleton(FailedTwoFactorLoginResponse::class, FailedTwoFactorChallengeResponse::class);
$this->app->singleton(PasskeyLoginResponseContract::class, PasskeyLoginResponse::class);
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
// Nur die notwendigen Fortify-Aktionen behalten
Fortify::createUsersUsing(CreateNewUser::class);
Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
Fortify::redirectUserForTwoFactorAuthenticationUsing(RedirectIfTwoFactorAuthenticatable::class);
// Keine View-Definitionen, die Auth-Seiten sind Livewire-Komponenten
RateLimiter::for('login', function (Request $request) {
// IP-weite Obergrenze über alle Konten (LoginIpThrottle) — der
// Zähler ist derselbe wie in der Livewire-Anmeldung.
$ipThrottle = app(LoginIpThrottle::class);
if ($ipThrottle->isBlocked($request->ip())) {
$seconds = $ipThrottle->availableIn($request->ip());
return $request->expectsJson()
? response()->json(['message' => __('auth.throttle', ['seconds' => $seconds, 'minutes' => ceil($seconds / 60)])], 429, ['Retry-After' => $seconds])
: back()->withInput($request->only(Fortify::username()))->withErrors([
Fortify::username() => __('auth.throttle', ['seconds' => $seconds, 'minutes' => ceil($seconds / 60)]),
]);
}
$throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())).'|'.$request->ip());
return Limit::perMinute(5)->by($throttleKey);
});
RateLimiter::for('two-factor', function (Request $request) {
return Limit::perMinute(5)->by($request->session()->get('login.id'));
});
RateLimiter::for('passkeys', function (Request $request) {
return Limit::perMinute(10)->by($request->user()?->getAuthIdentifier() ?? $request->ip());
});
// Passkey-Login (Entscheidung 5, 07.10.2026): nur aktive, verifizierte
// Konten — wie die übrigen Anmeldewege.
Passkeys::authorizeLoginUsing(function (Request $request, PasskeyUser $user, Passkey $passkey): bool {
return $user instanceof User && $user->is_active && $user->hasVerifiedEmail();
});
}
}