QA-Befunde A4: 2FA-Bestätigungsschritt + sicherer E-Mail-Wechsel

- 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>
This commit is contained in:
Kevin Adametz 2026-07-07 10:53:24 +00:00
parent b7bbf6df4e
commit 15387d0a3f
8 changed files with 505 additions and 45 deletions

View file

@ -34,6 +34,29 @@ test('a user with two-factor enabled is handed to the challenge, not logged in',
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'));