- 2FA: Einrichtung verlangt jetzt die Bestätigung mit einem App-Code (Fortify confirm-Semantik); vorher blieb two_factor_confirmed_at leer und der Login fragte nie nach der Challenge. - Recovery-Codes nur noch einmalig sichtbar (nach Bestätigung bzw. Neu-Erzeugung), nicht mehr bei jedem Seitenaufruf. - E-Mail-Änderung als Pending-Flow: Bestätigungslink geht an die NEUE Adresse, die alte bleibt bis zum Klick aktiv — kein Aussperren durch Tippfehler und keine Registrierungs-Notice mehr nach dem Ändern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
124 lines
4.1 KiB
PHP
124 lines
4.1 KiB
PHP
<?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('an unconfirmed two-factor setup does not trigger the challenge', function () {
|
|
/** @var TestCase $this */
|
|
$this->seed(RolesAndPermissionsSeeder::class);
|
|
$user = User::factory()->create(['is_active' => true]);
|
|
$user->assignRole('customer');
|
|
|
|
// Secret erzeugt, aber Einrichtung nie mit App-Code bestätigt
|
|
// (two_factor_confirmed_at bleibt null → Fortify-'confirm'-Semantik).
|
|
$user->forceFill([
|
|
'two_factor_secret' => encrypt(app(Google2FA::class)->generateSecretKey()),
|
|
'two_factor_confirmed_at' => null,
|
|
])->save();
|
|
|
|
Volt::test('auth.login')
|
|
->set('email', $user->email)
|
|
->set('password', 'password')
|
|
->call('login')
|
|
->assertHasNoErrors()
|
|
->assertRedirect(route('me.dashboard', absolute: false));
|
|
|
|
$this->assertAuthenticatedAs($user);
|
|
});
|
|
|
|
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('a json login does not grant a session to an inactive verified account', function () {
|
|
/** @var TestCase $this */
|
|
$user = User::factory()->create(['is_active' => false]);
|
|
|
|
$this->postJson('/login', ['email' => $user->email, 'password' => 'password'])
|
|
->assertStatus(403);
|
|
|
|
$this->assertGuest();
|
|
});
|
|
|
|
test('a json login for an active user succeeds with 204', function () {
|
|
/** @var TestCase $this */
|
|
$this->seed(RolesAndPermissionsSeeder::class);
|
|
$customer = User::factory()->create(['is_active' => true]);
|
|
$customer->assignRole('customer');
|
|
|
|
$this->postJson('/login', ['email' => $customer->email, 'password' => 'password'])
|
|
->assertNoContent();
|
|
|
|
$this->assertAuthenticatedAs($customer);
|
|
});
|
|
|
|
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);
|
|
});
|