diff --git a/app/Http/Responses/FailedTwoFactorChallengeResponse.php b/app/Http/Responses/FailedTwoFactorChallengeResponse.php new file mode 100644 index 00000000..3af5612d --- /dev/null +++ b/app/Http/Responses/FailedTwoFactorChallengeResponse.php @@ -0,0 +1,31 @@ + false` nicht + * registriert ist (RouteNotFoundException). Wir leiten stattdessen zurück auf + * unsere Volt-Challenge-Seite. + */ +class FailedTwoFactorChallengeResponse implements FailedTwoFactorLoginResponse +{ + public function toResponse($request): Response + { + [$key, $message] = $request->filled('recovery_code') + ? ['recovery_code', __('Der eingegebene Wiederherstellungs-Code ist ungültig.')] + : ['code', __('Der eingegebene Code ist ungültig. Bitte versuchen Sie es mit einem aktuellen Code erneut.')]; + + if ($request->wantsJson()) { + throw ValidationException::withMessages([ + $key => [$message], + ]); + } + + return redirect()->route('two-factor.challenge')->withErrors([$key => $message]); + } +} diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php index aee537d1..d9bbeb4e 100644 --- a/app/Providers/FortifyServiceProvider.php +++ b/app/Providers/FortifyServiceProvider.php @@ -6,6 +6,7 @@ use App\Actions\Fortify\CreateNewUser; use App\Actions\Fortify\ResetUserPassword; use App\Actions\Fortify\UpdateUserPassword; use App\Actions\Fortify\UpdateUserProfileInformation; +use App\Http\Responses\FailedTwoFactorChallengeResponse; use App\Http\Responses\RoleAwareLoginResponse; use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Http\Request; @@ -13,6 +14,7 @@ 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; @@ -27,6 +29,9 @@ class FortifyServiceProvider extends ServiceProvider { $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 Volt-Challenge. + $this->app->singleton(FailedTwoFactorLoginResponse::class, FailedTwoFactorChallengeResponse::class); } /** diff --git a/tests/Feature/Auth/TwoFactorLoginTest.php b/tests/Feature/Auth/TwoFactorLoginTest.php index 9beb3036..f98e71ca 100644 --- a/tests/Feature/Auth/TwoFactorLoginTest.php +++ b/tests/Feature/Auth/TwoFactorLoginTest.php @@ -78,6 +78,35 @@ test('a valid two-factor code completes login with a role-aware redirect', funct $this->assertAuthenticatedAs($customer); }); +test('an invalid two-factor code returns to the challenge page with an error', function () { + /** @var TestCase $this */ + $user = User::factory()->create(['is_active' => true]); + enableTwoFactor($user); + + $this->withSession(['login.id' => $user->id, 'login.remember' => false]) + ->post('/two-factor-challenge', ['code' => '000000']) + ->assertRedirect(route('two-factor.challenge')) + ->assertSessionHasErrors('code'); + + $this->assertGuest(); +}); + +test('an invalid recovery code returns to the challenge page with an error', function () { + /** @var TestCase $this */ + $user = User::factory()->create(['is_active' => true]); + enableTwoFactor($user); + $user->forceFill([ + 'two_factor_recovery_codes' => encrypt(json_encode(['gueltiger-code'])), + ])->save(); + + $this->withSession(['login.id' => $user->id, 'login.remember' => false]) + ->post('/two-factor-challenge', ['recovery_code' => 'falscher-code']) + ->assertRedirect(route('two-factor.challenge')) + ->assertSessionHasErrors('recovery_code'); + + $this->assertGuest(); +}); + test('the fortify login post blocks inactive but verified users', function () { /** @var TestCase $this */ $user = User::factory()->create(['is_active' => false]);