From 36a38ab947a42590db98e6b3dadcb96835d8cd35 Mon Sep 17 00:00:00 2001 From: Lance Pioch Date: Mon, 3 Mar 2025 15:22:12 -0500 Subject: [PATCH] Basic two factor auth implementation (#1050) * Basic two factor auth * Remove unused import * Add translation --- app/Filament/Pages/Auth/Login.php | 77 +++++++++++++++++++++++++++++++ lang/en/auth.php | 22 +++++++++ 2 files changed, 99 insertions(+) create mode 100644 lang/en/auth.php diff --git a/app/Filament/Pages/Auth/Login.php b/app/Filament/Pages/Auth/Login.php index 238f25195..173ae17db 100644 --- a/app/Filament/Pages/Auth/Login.php +++ b/app/Filament/Pages/Auth/Login.php @@ -3,17 +3,84 @@ namespace App\Filament\Pages\Auth; use App\Extensions\OAuth\Providers\OAuthProvider; +use App\Models\User; use Coderflex\FilamentTurnstile\Forms\Components\Turnstile; +use Filament\Facades\Filament; use Filament\Forms\Components\Actions; use Filament\Forms\Components\Actions\Action; use Filament\Forms\Components\Component; use Filament\Forms\Components\TextInput; +use Filament\Http\Responses\Auth\Contracts\LoginResponse; +use Filament\Notifications\Notification; use Filament\Pages\Auth\Login as BaseLogin; use Filament\Support\Colors\Color; +use Illuminate\Support\Facades\Config; +use Illuminate\Support\Sleep; use Illuminate\Validation\ValidationException; +use PragmaRX\Google2FA\Google2FA; class Login extends BaseLogin { + private Google2FA $google2FA; + + public bool $verifyTwoFactor = false; + + public function boot(Google2FA $google2FA): void + { + $this->google2FA = $google2FA; + } + + public function authenticate(): ?LoginResponse + { + $data = $this->form->getState(); + Filament::auth()->once($this->getCredentialsFromFormData($data)); + + /** @var ?User $user */ + $user = Filament::auth()->user(); + + // Make sure that rate limits apply + if (!$user) { + return parent::authenticate(); + } + + // 2FA disabled + if (!$user->use_totp) { + return parent::authenticate(); + } + + $token = $data['2fa'] ?? null; + + // 2FA not shown yet + if ($token === null) { + $this->verifyTwoFactor = true; + + return null; + } + + $isValidToken = $this->google2FA->verifyKey( + $user->totp_secret, + $token, + Config::integer('panel.auth.2fa.window'), + ); + + if (!$isValidToken) { + // Buffer to prevent bruteforce + Sleep::sleep(1); + + Notification::make() + ->title(trans('auth.failed-two-factor')) + ->body(trans('auth.failed')) + ->color('danger') + ->icon('tabler-auth-2fa') + ->danger() + ->send(); + + return null; + } + + return parent::authenticate(); + } + protected function getForms(): array { return [ @@ -24,6 +91,7 @@ class Login extends BaseLogin $this->getPasswordFormComponent(), $this->getRememberFormComponent(), $this->getOAuthFormComponent(), + $this->getTwoFactorAuthenticationComponent(), Turnstile::make('captcha') ->hidden(!config('turnstile.turnstile_enabled')) ->validationMessages([ @@ -36,6 +104,15 @@ class Login extends BaseLogin ]; } + private function getTwoFactorAuthenticationComponent(): Component + { + return TextInput::make('2fa') + ->label(trans('auth.two-factor-code')) + ->hidden(fn () => !$this->verifyTwoFactor) + ->required() + ->live(); + } + protected function throwFailureValidationException(): never { $this->dispatch('reset-captcha'); diff --git a/lang/en/auth.php b/lang/en/auth.php new file mode 100644 index 000000000..d8fb89a75 --- /dev/null +++ b/lang/en/auth.php @@ -0,0 +1,22 @@ + 'These credentials do not match our records.', + 'failed-two-factor' => 'Incorrect 2FA Code', + 'two-factor-code' => 'Two Factor Code', + 'password' => 'The provided password is incorrect.', + 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', + +];