From 83fe7f6f6d414baf34bc2df57a8c9175d282c9c5 Mon Sep 17 00:00:00 2001 From: James Cole Date: Mon, 19 Jan 2026 19:36:21 +0100 Subject: [PATCH] MFA disabled #11544 --- .../UserHasDisabledMFA.php} | 16 ++--- app/Handlers/Events/Security/MFAHandler.php | 29 +-------- .../Controllers/Profile/MfaController.php | 65 +++++++++---------- .../User/NotifiesUserAboutDisabledMFA.php | 57 ++++++++++++++++ app/Providers/EventServiceProvider.php | 7 +- 5 files changed, 101 insertions(+), 73 deletions(-) rename app/Events/Security/{DisabledMFA.php => User/UserHasDisabledMFA.php} (74%) create mode 100644 app/Listeners/Security/User/NotifiesUserAboutDisabledMFA.php diff --git a/app/Events/Security/DisabledMFA.php b/app/Events/Security/User/UserHasDisabledMFA.php similarity index 74% rename from app/Events/Security/DisabledMFA.php rename to app/Events/Security/User/UserHasDisabledMFA.php index ad3464e95f..c957679e03 100644 --- a/app/Events/Security/DisabledMFA.php +++ b/app/Events/Security/User/UserHasDisabledMFA.php @@ -1,8 +1,7 @@ . */ -declare(strict_types=1); - -namespace FireflyIII\Events\Security; +namespace FireflyIII\Events\Security\User; use FireflyIII\Events\Event; use FireflyIII\User; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Queue\SerializesModels; +use InvalidArgumentException; -class DisabledMFA extends Event +class UserHasDisabledMFA extends Event { use SerializesModels; @@ -39,6 +37,8 @@ class DisabledMFA extends Event { if ($user instanceof User) { $this->user = $user; + return; } + throw new InvalidArgumentException('User must be an instance of User.'); } } diff --git a/app/Handlers/Events/Security/MFAHandler.php b/app/Handlers/Events/Security/MFAHandler.php index 63f9ec31b9..25d03ef12f 100644 --- a/app/Handlers/Events/Security/MFAHandler.php +++ b/app/Handlers/Events/Security/MFAHandler.php @@ -25,22 +25,20 @@ declare(strict_types=1); namespace FireflyIII\Handlers\Events\Security; use Exception; -use FireflyIII\Events\Security\DisabledMFA; use FireflyIII\Events\Security\EnabledMFA; use FireflyIII\Events\Security\MFABackupFewLeft; use FireflyIII\Events\Security\MFABackupNoLeft; use FireflyIII\Events\Security\MFAManyFailedAttempts; use FireflyIII\Events\Security\MFANewBackupCodes; use FireflyIII\Events\Security\MFAUsedBackupCode; -use FireflyIII\Notifications\Security\DisabledMFANotification; use FireflyIII\Notifications\Security\EnabledMFANotification; use FireflyIII\Notifications\Security\MFABackupFewLeftNotification; use FireflyIII\Notifications\Security\MFABackupNoLeftNotification; use FireflyIII\Notifications\Security\MFAManyFailedAttemptsNotification; use FireflyIII\Notifications\Security\MFAUsedBackupCodeNotification; use FireflyIII\Notifications\Security\NewBackupCodesNotification; -use Illuminate\Support\Facades\Notification; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Notification; class MFAHandler { @@ -95,31 +93,6 @@ class MFAHandler } } - public function sendMFADisabledMail(DisabledMFA $event): void - { - Log::debug(sprintf('Now in %s', __METHOD__)); - - $user = $event->user; - - try { - Notification::send($user, new DisabledMFANotification($user)); - } catch (Exception $e) { - $message = $e->getMessage(); - if (str_contains($message, 'Bcc')) { - Log::warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); - - return; - } - if (str_contains($message, 'RFC 2822')) { - Log::warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); - - return; - } - Log::error($e->getMessage()); - Log::error($e->getTraceAsString()); - } - } - public function sendMFAEnabledMail(EnabledMFA $event): void { Log::debug(sprintf('Now in %s', __METHOD__)); diff --git a/app/Http/Controllers/Profile/MfaController.php b/app/Http/Controllers/Profile/MfaController.php index 23ff40d41b..7076bf6e45 100644 --- a/app/Http/Controllers/Profile/MfaController.php +++ b/app/Http/Controllers/Profile/MfaController.php @@ -24,16 +24,16 @@ declare(strict_types=1); namespace FireflyIII\Http\Controllers\Profile; -use FireflyIII\Support\Facades\Preferences; use Carbon\Carbon; -use FireflyIII\Events\Security\DisabledMFA; use FireflyIII\Events\Security\EnabledMFA; use FireflyIII\Events\Security\MFANewBackupCodes; +use FireflyIII\Events\Security\User\UserHasDisabledMFA; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Http\Middleware\IsDemoUser; use FireflyIII\Http\Requests\ExistingTokenFormRequest; use FireflyIII\Http\Requests\TokenFormRequest; use FireflyIII\Repositories\User\UserRepositoryInterface; +use FireflyIII\Support\Facades\Preferences; use FireflyIII\User; use Illuminate\Contracts\View\Factory; use Illuminate\Http\RedirectResponse; @@ -71,7 +71,7 @@ class MfaController extends Controller $this->middleware( static function ($request, $next) { - app('view')->share('title', (string) trans('firefly.profile')); + app('view')->share('title', (string)trans('firefly.profile')); app('view')->share('mainTitleIcon', 'fa-user'); return $next($request); @@ -85,7 +85,7 @@ class MfaController extends Controller } - public function backupCodes(Request $request): Factory|RedirectResponse|View + public function backupCodes(Request $request): Factory | RedirectResponse | View { if (!$this->internalAuth) { $request->session()->flash('error', trans('firefly.external_user_mgt_disabled')); @@ -102,14 +102,14 @@ class MfaController extends Controller return view('profile.mfa.backup-codes-intro'); } - public function backupCodesPost(ExistingTokenFormRequest $request): Redirector|RedirectResponse|View + public function backupCodesPost(ExistingTokenFormRequest $request): Redirector | RedirectResponse | View { if (!$this->internalAuth) { $request->session()->flash('error', trans('firefly.external_user_mgt_disabled')); return redirect(route('profile.index')); } - $enabledMFA = null !== auth()->user()->mfa_secret; + $enabledMFA = null !== auth()->user()->mfa_secret; if (false === $enabledMFA) { request()->session()->flash('info', trans('firefly.mfa_not_enabled')); @@ -118,18 +118,17 @@ class MfaController extends Controller // generate recovery codes: $recovery = app(Recovery::class); $recoveryCodes = $recovery->lowercase() - ->setCount(8) // Generate 8 codes - ->setBlocks(2) // Every code must have 2 blocks - ->setChars(6) // Each block must have 6 chars - ->toArray() - ; + ->setCount(8) // Generate 8 codes + ->setBlocks(2) // Every code must have 2 blocks + ->setChars(6) // Each block must have 6 chars + ->toArray(); $codes = implode("\r\n", $recoveryCodes); Preferences::set('mfa_recovery', $recoveryCodes); Preferences::mark(); // send user notification. - $user = auth()->user(); + $user = auth()->user(); Log::channel('audit')->info(sprintf('User "%s" has generated new backup codes.', $user->email)); event(new MFANewBackupCodes($user)); @@ -137,20 +136,20 @@ class MfaController extends Controller } - public function disableMFA(Request $request): Factory|RedirectResponse|View + public function disableMFA(Request $request): Factory | RedirectResponse | View { if (!$this->internalAuth) { request()->session()->flash('error', trans('firefly.external_user_mgt_disabled')); return redirect(route('profile.index')); } - $enabledMFA = null !== auth()->user()->mfa_secret; + $enabledMFA = null !== auth()->user()->mfa_secret; if (false === $enabledMFA) { request()->session()->flash('info', trans('firefly.mfa_already_disabled')); return redirect(route('profile.index')); } - $subTitle = (string) trans('firefly.mfa_index_title'); + $subTitle = (string)trans('firefly.mfa_index_title'); $subTitleIcon = 'fa-calculator'; return view('profile.mfa.disable-mfa')->with(['subTitle' => $subTitle, 'subTitleIcon' => $subTitleIcon, 'enabledMFA' => $enabledMFA]); @@ -159,7 +158,7 @@ class MfaController extends Controller /** * Delete 2FA routine. */ - public function disableMFAPost(ExistingTokenFormRequest $request): Redirector|RedirectResponse + public function disableMFAPost(ExistingTokenFormRequest $request): Redirector | RedirectResponse { if (!$this->internalAuth) { $request->session()->flash('error', trans('firefly.external_user_mgt_disabled')); @@ -171,15 +170,15 @@ class MfaController extends Controller $repository = app(UserRepositoryInterface::class); /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); Preferences::delete('temp-mfa-secret'); Preferences::delete('temp-mfa-codes'); $repository->setMFACode($user, null); Preferences::mark(); - session()->flash('success', (string) trans('firefly.pref_two_factor_auth_disabled')); - session()->flash('info', (string) trans('firefly.pref_two_factor_auth_remove_it')); + session()->flash('success', (string)trans('firefly.pref_two_factor_auth_disabled')); + session()->flash('info', (string)trans('firefly.pref_two_factor_auth_remove_it')); // also logout current 2FA tokens. $cookieName = config('google2fa.cookie_name', 'google2fa_token'); @@ -187,7 +186,7 @@ class MfaController extends Controller // send user notification. Log::channel('audit')->info(sprintf('User "%s" has disabled MFA', $user->email)); - event(new DisabledMFA($user)); + event(new UserHasDisabledMFA($user)); return redirect(route('profile.index')); } @@ -195,7 +194,7 @@ class MfaController extends Controller /** * Enable 2FA screen. */ - public function enableMFA(Request $request): Redirector|RedirectResponse|View + public function enableMFA(Request $request): Redirector | RedirectResponse | View { if (!$this->internalAuth) { $request->session()->flash('error', trans('firefly.external_user_mgt_disabled')); @@ -210,14 +209,14 @@ class MfaController extends Controller // If FF3 already has a secret, just set the two-factor auth enabled to 1, // and let the user continue with the existing secret. if ($enabledMFA) { - session()->flash('info', (string) trans('firefly.2fa_already_enabled')); + session()->flash('info', (string)trans('firefly.2fa_already_enabled')); return redirect(route('profile.index')); } - $domain = $this->getDomain(); - $secret = Google2FA::generateSecretKey(); - $image = Google2FA::getQRCodeInline($domain, auth()->user()->email, $secret); + $domain = $this->getDomain(); + $secret = Google2FA::generateSecretKey(); + $image = Google2FA::getQRCodeInline($domain, auth()->user()->email, $secret); Preferences::set('temp-mfa-secret', $secret); @@ -232,7 +231,7 @@ class MfaController extends Controller * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface */ - public function enableMFAPost(TokenFormRequest $request): Redirector|RedirectResponse + public function enableMFAPost(TokenFormRequest $request): Redirector | RedirectResponse { if (!$this->internalAuth) { $request->session()->flash('error', trans('firefly.external_user_mgt_disabled')); @@ -241,10 +240,10 @@ class MfaController extends Controller } /** @var User $user */ - $user = auth()->user(); + $user = auth()->user(); // verify password. - $password = $request->get('password'); + $password = $request->get('password'); if (!auth()->validate(['email' => $user->email, 'password' => $password])) { session()->flash('error', 'Bad user pw, no MFA for you!'); @@ -257,17 +256,17 @@ class MfaController extends Controller if (is_array($secret)) { $secret = null; } - $secret = (string) $secret; + $secret = (string)$secret; $repository->setMFACode($user, $secret); Preferences::delete('temp-mfa-secret'); - session()->flash('success', (string) trans('firefly.saved_preferences')); + session()->flash('success', (string)trans('firefly.saved_preferences')); Preferences::mark(); // also save the code so replay attack is prevented. - $mfaCode = $request->get('code'); + $mfaCode = $request->get('code'); $this->addToMFAHistory($mfaCode); // make sure MFA is logged out. @@ -327,7 +326,7 @@ class MfaController extends Controller Preferences::set('mfa_history', $newHistory); } - public function index(): Factory|RedirectResponse|View + public function index(): Factory | RedirectResponse | View { if (!$this->internalAuth) { request()->session()->flash('error', trans('firefly.external_user_mgt_disabled')); @@ -335,7 +334,7 @@ class MfaController extends Controller return redirect(route('profile.index')); } - $subTitle = (string) trans('firefly.mfa_index_title'); + $subTitle = (string)trans('firefly.mfa_index_title'); $subTitleIcon = 'fa-calculator'; $enabledMFA = null !== auth()->user()->mfa_secret; diff --git a/app/Listeners/Security/User/NotifiesUserAboutDisabledMFA.php b/app/Listeners/Security/User/NotifiesUserAboutDisabledMFA.php new file mode 100644 index 0000000000..2d0d101e69 --- /dev/null +++ b/app/Listeners/Security/User/NotifiesUserAboutDisabledMFA.php @@ -0,0 +1,57 @@ +. + */ + +namespace FireflyIII\Listeners\Security\User; + +use Exception; +use FireflyIII\Events\Security\User\UserHasDisabledMFA; +use FireflyIII\Notifications\Security\DisabledMFANotification; +use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Notification; + +class NotifiesUserAboutDisabledMFA implements ShouldQueue +{ + public function handle(UserHasDisabledMFA $event): void { + Log::debug(sprintf('Now in %s', __METHOD__)); + + $user = $event->user; + + try { + Notification::send($user, new DisabledMFANotification($user)); + } catch (Exception $e) { + $message = $e->getMessage(); + if (str_contains($message, 'Bcc')) { + Log::warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); + + return; + } + if (str_contains($message, 'RFC 2822')) { + Log::warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); + + return; + } + Log::error($e->getMessage()); + Log::error($e->getTraceAsString()); + } + } + +} diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 43f52047f4..2a9ca79366 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -35,7 +35,6 @@ use FireflyIII\Events\RequestedNewPassword; use FireflyIII\Events\RequestedReportOnJournals; use FireflyIII\Events\RequestedSendWebhookMessages; use FireflyIII\Events\RequestedVersionCheckStatus; -use FireflyIII\Events\Security\DisabledMFA; use FireflyIII\Events\Security\EnabledMFA; use FireflyIII\Events\Security\MFABackupFewLeft; use FireflyIII\Events\Security\MFABackupNoLeft; @@ -186,9 +185,9 @@ class EventServiceProvider extends ServiceProvider EnabledMFA::class => [ 'FireflyIII\Handlers\Events\Security\MFAHandler@sendMFAEnabledMail', ], - DisabledMFA::class => [ - 'FireflyIII\Handlers\Events\Security\MFAHandler@sendMFADisabledMail', - ], + // DisabledMFA::class => [ + // 'FireflyIII\Handlers\Events\Security\MFAHandler@sendMFADisabledMail', + // ], MFANewBackupCodes::class => [ 'FireflyIII\Handlers\Events\Security\MFAHandler@sendNewMFABackupCodesMail', ],