From 1b741070b3148fd43d8982c4d0c80517bad972ad Mon Sep 17 00:00:00 2001 From: Saeed Vaziry <61919774+saeedvaziry@users.noreply.github.com> Date: Mon, 2 Dec 2024 20:10:36 +0100 Subject: [PATCH] Two factor (#383) --- app/Providers/WebServiceProvider.php | 3 +- app/Web/Pages/Login.php | 143 +++++++++++++++++++++++++++ 2 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 app/Web/Pages/Login.php diff --git a/app/Providers/WebServiceProvider.php b/app/Providers/WebServiceProvider.php index e417aaa..4892063 100644 --- a/app/Providers/WebServiceProvider.php +++ b/app/Providers/WebServiceProvider.php @@ -3,6 +3,7 @@ namespace App\Providers; use App\Http\Middleware\HasProjectMiddleware; +use App\Web\Pages\Login; use App\Web\Pages\Settings\Profile; use App\Web\Pages\Settings\Projects\Widgets\SelectProject; use Exception; @@ -111,7 +112,7 @@ public function panel(Panel $panel): Panel 'profile' => MenuItem::make() ->url(fn (): string => Profile\Index::getUrl()), ]) - ->login() + ->login(Login::class) ->spa() ->globalSearchKeyBindings(['command+k', 'ctrl+k']) ->sidebarCollapsibleOnDesktop() diff --git a/app/Web/Pages/Login.php b/app/Web/Pages/Login.php new file mode 100644 index 0000000..9eec130 --- /dev/null +++ b/app/Web/Pages/Login.php @@ -0,0 +1,143 @@ +check()) { + redirect()->intended(Filament::getUrl()); + } + + $this->initTwoFactor(); + + $this->form->fill(); + } + + public function logoutAction(): Action + { + return Action::make('logout') + ->label('Logout') + ->color('danger') + ->link() + ->action(function () { + Filament::auth()->logout(); + + session()->forget('login.id'); + + $this->redirect(Filament::getLoginUrl()); + }); + } + + protected function getForms(): array + { + if (request()->session()->has('login.id')) { + return [ + 'form' => $this->form( + $this->makeForm() + ->schema([ + TextInput::make('code') + ->label('2FA Code') + ->autofocus(), + TextInput::make('recovery_code') + ->label('Recovery Code') + ->autofocus(), + ]) + ->statePath('data'), + ), + ]; + } + + return parent::getForms(); + } + + public function authenticate(): ?LoginResponse + { + if (request()->session()->has('login.id')) { + return $this->confirmTwoFactor(); + } + + $loginResponse = parent::authenticate(); + + /** @var ?User $user */ + $user = Filament::auth()->getUser(); + if ($user && $user->two_factor_secret) { + Filament::auth()->logout(); + + request()->session()->put([ + 'login.id' => $user->getKey(), + 'login.remember' => $this->data['remember'] ?? false, + ]); + + $this->redirect(Filament::getLoginUrl()); + + return null; + } + + return $loginResponse; + } + + private function confirmTwoFactor(): ?LoginResponse + { + $request = TwoFactorLoginRequest::createFrom(request())->merge([ + 'code' => $this->data['code'], + 'recovery_code' => $this->data['recovery_code'], + ]); + + /** @var ?User $user */ + $user = $request->challengedUser(); + + if ($code = $request->validRecoveryCode()) { + $user->replaceRecoveryCode($code); + } elseif (! $request->hasValidCode()) { + $field = $request->input('recovery_code') ? 'recovery_code' : 'code'; + + $this->initTwoFactor(); + + throw ValidationException::withMessages([ + 'data.'.$field => 'Invalid code!', + ]); + } + + Filament::auth()->login($user, $request->remember()); + + return app(LoginResponse::class); + } + + protected function getAuthenticateFormAction(): Action + { + if (request()->session()->has('login.id')) { + return Action::make('verify') + ->label('Verify') + ->submit('authenticate'); + } + + return parent::getAuthenticateFormAction(); + } + + private function initTwoFactor(): void + { + if (request()->session()->has('login.id')) { + FilamentView::registerRenderHook( + PanelsRenderHook::AUTH_LOGIN_FORM_BEFORE, + fn (): string => Blade::render( + <<{$this->logoutAction()->render()} + BLADE + ), + ); + } + } +}