Security: 2FA-Bypass beheben & Login-Pfade konsolidieren
Befund (Review 16.06.): Der Volt-Login machte direkt Auth::attempt() und umging Fortifys 2FA-Pipeline (2FA-Bypass); zusätzlich existierte der Fortify-POST /login parallel mit schwächeren Post-Login-Regeln. Fix (Volt-nativ): - Volt-Login prüft Credentials ohne sofortiges Login; bei aktivem 2FA wird der Session-Vertrag login.id/login.remember gesetzt und auf eine neue Volt- 2FA-Challenge-Seite (/two-factor-challenge) geleitet, die an Fortifys bestehenden Controller postet (TOTP + Recovery-Code). - Gemeinsame Post-Login-Logik in App\Support\LoginRedirect (rollengerechtes Home + 403-sicherer intended-Redirect), genutzt von Volt-Login UND Response. - RoleAwareLoginResponse implementiert jetzt LoginResponse UND TwoFactorLoginResponse und erzwingt einheitlich: unverifiziert → Notice, verifiziert-inaktiv → Logout+Fehler, sonst 403-sicherer Redirect. Damit ist auch der direkte Fortify-POST-Pfad gehärtet. Tests: 2FA-Übergabe, Challenge-Guard, voller TOTP-Flow, Fortify-POST blockt inaktive User und hält Customer aus dem Admin-Bereich. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
d98d297524
commit
f4ca452c6b
8 changed files with 295 additions and 81 deletions
79
tests/Feature/Auth/TwoFactorLoginTest.php
Normal file
79
tests/Feature/Auth/TwoFactorLoginTest.php
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolesAndPermissionsSeeder;
|
||||
use Livewire\Volt\Volt;
|
||||
use PragmaRX\Google2FA\Google2FA;
|
||||
use Tests\TestCase;
|
||||
|
||||
function enableTwoFactor(User $user): string
|
||||
{
|
||||
$secret = app(Google2FA::class)->generateSecretKey();
|
||||
|
||||
$user->forceFill([
|
||||
'two_factor_secret' => encrypt($secret),
|
||||
'two_factor_confirmed_at' => now(),
|
||||
])->save();
|
||||
|
||||
return $secret;
|
||||
}
|
||||
|
||||
test('a user with two-factor enabled is handed to the challenge, not logged in', function () {
|
||||
/** @var TestCase $this */
|
||||
$user = User::factory()->create(['is_active' => true]);
|
||||
enableTwoFactor($user);
|
||||
|
||||
Volt::test('auth.login')
|
||||
->set('email', $user->email)
|
||||
->set('password', 'password')
|
||||
->call('login')
|
||||
->assertHasNoErrors()
|
||||
->assertRedirect(route('two-factor.challenge'));
|
||||
|
||||
$this->assertGuest();
|
||||
expect(session('login.id'))->toBe($user->id);
|
||||
});
|
||||
|
||||
test('the challenge page redirects to login without a pending challenge', function () {
|
||||
/** @var TestCase $this */
|
||||
$this->get(route('two-factor.challenge'))->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('a valid two-factor code completes login with a role-aware redirect', function () {
|
||||
/** @var TestCase $this */
|
||||
$this->seed(RolesAndPermissionsSeeder::class);
|
||||
$customer = User::factory()->create(['is_active' => true]);
|
||||
$customer->assignRole('customer');
|
||||
$secret = enableTwoFactor($customer);
|
||||
|
||||
$otp = app(Google2FA::class)->getCurrentOtp($secret);
|
||||
|
||||
$this->withSession(['login.id' => $customer->id, 'login.remember' => false])
|
||||
->post('/two-factor-challenge', ['code' => $otp])
|
||||
->assertRedirect(route('me.dashboard', absolute: false));
|
||||
|
||||
$this->assertAuthenticatedAs($customer);
|
||||
});
|
||||
|
||||
test('the fortify login post blocks inactive but verified users', function () {
|
||||
/** @var TestCase $this */
|
||||
$user = User::factory()->create(['is_active' => false]);
|
||||
|
||||
$this->post('/login', ['email' => $user->email, 'password' => 'password'])
|
||||
->assertRedirect(route('login'));
|
||||
|
||||
$this->assertGuest();
|
||||
});
|
||||
|
||||
test('the fortify login post keeps a customer out of the admin area on stale intended', function () {
|
||||
/** @var TestCase $this */
|
||||
$this->seed(RolesAndPermissionsSeeder::class);
|
||||
$customer = User::factory()->create(['is_active' => true]);
|
||||
$customer->assignRole('customer');
|
||||
|
||||
$this->withSession(['url.intended' => url('/admin/users')])
|
||||
->post('/login', ['email' => $customer->email, 'password' => 'password'])
|
||||
->assertRedirect(route('me.dashboard', absolute: false));
|
||||
|
||||
$this->assertAuthenticatedAs($customer);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue