diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index c018427ea..f26d82090 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -6,6 +6,7 @@ use App\Jobs\FollowPipeline\FollowAcceptPipeline; use App\Jobs\FollowPipeline\FollowPipeline; use App\Jobs\FollowPipeline\FollowRejectPipeline; use App\Mail\ConfirmEmail; +use App\Models\AccountLog; use App\Models\EmailVerification; use App\Models\Follower; use App\Models\FollowRequest; @@ -521,6 +522,19 @@ class AccountController extends Controller return redirect('/'); } + // Audit failed 2FA verification so brute-force attempts at the MFA + // layer are visible (the route throttle bounds the rate per user). + $log = new AccountLog; + $log->user_id = $user->id; + $log->item_id = $user->id; + $log->item_type = User::class; + $log->action = 'auth.2fa.failed'; + $log->message = '2FA verification failed'; + $log->link = null; + $log->ip_address = $request->ip(); + $log->user_agent = $request->userAgent(); + $log->save(); + if ($request->session()->has('2fa.attempts')) { $count = (int) $request->session()->get('2fa.attempts'); if ($count == 3) { diff --git a/routes/web.php b/routes/web.php index 3d7131ee0..fbf2738aa 100644 --- a/routes/web.php +++ b/routes/web.php @@ -196,7 +196,8 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact Route::get('auth/sudo', [AccountController::class, 'confirmPassword'])->name('password.confirm'); Route::post('auth/sudo', [AccountController::class, 'confirmPasswordStore']); Route::get('auth/checkpoint', [AccountController::class, 'twoFactorCheckpoint']); - Route::post('auth/checkpoint', [AccountController::class, 'twoFactorVerify']); + Route::post('auth/checkpoint', [AccountController::class, 'twoFactorVerify']) + ->middleware('throttle:5,15'); Route::get('results', [SearchController::class, 'results']); Route::post('visibility', [StatusController::class, 'toggleVisibility']); diff --git a/tests/Feature/Auth/TwoFactorCheckpointTest.php b/tests/Feature/Auth/TwoFactorCheckpointTest.php new file mode 100644 index 000000000..b2df42a99 --- /dev/null +++ b/tests/Feature/Auth/TwoFactorCheckpointTest.php @@ -0,0 +1,49 @@ +first(function ($r) { + return $r->uri() === 'i/auth/checkpoint' && in_array('POST', $r->methods()); + }); + + expect($route)->not->toBeNull(); + + $hasThrottle = collect($route->gatherMiddleware()) + ->contains(fn ($m) => is_string($m) && str_starts_with($m, 'throttle')); + + expect($hasThrottle)->toBeTrue(); +}); + +it('audit-logs a failed 2FA verification', function () { + $google2fa = new Google2FA; + $secret = $google2fa->generateSecretKey(); + + $user = User::factory()->create(['2fa_secret' => $secret, '2fa_enabled' => true]); + $user->refresh(); + + $this->actingAs($user) + ->post('/i/auth/checkpoint', ['code' => '000000']); + + expect( + AccountLog::where('user_id', $user->id) + ->where('action', 'auth.2fa.failed') + ->exists() + )->toBeTrue(); +});