| 
									
										
										
										
											2016-03-19 16:22:57 +01:00
										 |  |  | <?php | 
					
						
							| 
									
										
										
										
											2022-12-29 19:41:57 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-03-19 16:22:57 +01:00
										 |  |  | /** | 
					
						
							|  |  |  |  * TwoFactorController.php | 
					
						
							| 
									
										
										
										
											2020-01-31 07:32:04 +01:00
										 |  |  |  * Copyright (c) 2019 james@firefly-iii.org | 
					
						
							| 
									
										
										
										
											2016-03-19 16:22:57 +01:00
										 |  |  |  * | 
					
						
							| 
									
										
										
										
											2019-10-02 06:37:26 +02:00
										 |  |  |  * This file is part of Firefly III (https://github.com/firefly-iii). | 
					
						
							| 
									
										
										
										
											2016-10-05 06:52:15 +02:00
										 |  |  |  * | 
					
						
							| 
									
										
										
										
											2019-10-02 06:37:26 +02:00
										 |  |  |  * This program is free software: you can redistribute it and/or modify | 
					
						
							|  |  |  |  * it under the terms of the GNU Affero General Public License as | 
					
						
							|  |  |  |  * published by the Free Software Foundation, either version 3 of the | 
					
						
							|  |  |  |  * License, or (at your option) any later version. | 
					
						
							| 
									
										
										
										
											2017-10-21 08:40:00 +02:00
										 |  |  |  * | 
					
						
							| 
									
										
										
										
											2019-10-02 06:37:26 +02:00
										 |  |  |  * This program is distributed in the hope that it will be useful, | 
					
						
							| 
									
										
										
										
											2017-10-21 08:40:00 +02:00
										 |  |  |  * but WITHOUT ANY WARRANTY; without even the implied warranty of | 
					
						
							|  |  |  |  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | 
					
						
							| 
									
										
										
										
											2019-10-02 06:37:26 +02:00
										 |  |  |  * GNU Affero General Public License for more details. | 
					
						
							| 
									
										
										
										
											2017-10-21 08:40:00 +02:00
										 |  |  |  * | 
					
						
							| 
									
										
										
										
											2019-10-02 06:37:26 +02:00
										 |  |  |  * You should have received a copy of the GNU Affero General Public License | 
					
						
							|  |  |  |  * along with this program.  If not, see <https://www.gnu.org/licenses/>. | 
					
						
							| 
									
										
										
										
											2016-03-19 16:22:57 +01:00
										 |  |  |  */ | 
					
						
							| 
									
										
										
										
											2017-04-09 07:44:22 +02:00
										 |  |  | declare(strict_types=1); | 
					
						
							| 
									
										
										
										
											2016-05-20 12:27:31 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-03-19 16:22:57 +01:00
										 |  |  | namespace FireflyIII\Http\Controllers\Auth; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-05-24 05:52:31 +02:00
										 |  |  | use Carbon\Carbon; | 
					
						
							| 
									
										
										
										
											2024-10-08 07:21:23 +02:00
										 |  |  | use FireflyIII\Events\Security\MFABackupFewLeft; | 
					
						
							|  |  |  | use FireflyIII\Events\Security\MFABackupNoLeft; | 
					
						
							| 
									
										
										
										
											2024-10-08 14:45:37 +02:00
										 |  |  | use FireflyIII\Events\Security\MFAManyFailedAttempts; | 
					
						
							| 
									
										
										
										
											2024-10-08 07:21:23 +02:00
										 |  |  | use FireflyIII\Events\Security\MFAUsedBackupCode; | 
					
						
							| 
									
										
										
										
											2016-03-19 16:22:57 +01:00
										 |  |  | use FireflyIII\Http\Controllers\Controller; | 
					
						
							| 
									
										
										
										
											2018-07-09 19:24:08 +02:00
										 |  |  | use FireflyIII\User; | 
					
						
							| 
									
										
										
										
											2021-09-18 10:20:19 +02:00
										 |  |  | use Illuminate\Contracts\View\Factory; | 
					
						
							|  |  |  | use Illuminate\Contracts\View\View; | 
					
						
							| 
									
										
										
										
											2020-03-17 15:01:00 +01:00
										 |  |  | use Illuminate\Http\RedirectResponse; | 
					
						
							| 
									
										
										
										
											2017-02-16 21:01:22 +01:00
										 |  |  | use Illuminate\Http\Request; | 
					
						
							| 
									
										
										
										
											2020-03-17 15:01:00 +01:00
										 |  |  | use Illuminate\Routing\Redirector; | 
					
						
							| 
									
										
										
										
											2024-10-08 07:21:23 +02:00
										 |  |  | use Illuminate\Support\Facades\Log; | 
					
						
							| 
									
										
										
										
											2019-08-04 07:00:47 +02:00
										 |  |  | use PragmaRX\Google2FALaravel\Support\Authenticator; | 
					
						
							| 
									
										
										
										
											2016-03-19 16:22:57 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							| 
									
										
										
										
											2017-11-15 12:25:49 +01:00
										 |  |  |  * Class TwoFactorController. | 
					
						
							| 
									
										
										
										
											2016-03-19 16:22:57 +01:00
										 |  |  |  */ | 
					
						
							|  |  |  | class TwoFactorController extends Controller | 
					
						
							|  |  |  | { | 
					
						
							| 
									
										
										
										
											2020-03-10 18:29:27 +01:00
										 |  |  |     /** | 
					
						
							|  |  |  |      * What to do if 2FA lost? | 
					
						
							|  |  |  |      * | 
					
						
							| 
									
										
										
										
											2021-09-18 10:20:19 +02:00
										 |  |  |      * @return Factory|View | 
					
						
							| 
									
										
										
										
											2020-03-10 18:29:27 +01:00
										 |  |  |      */ | 
					
						
							|  |  |  |     public function lostTwoFactor() | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         /** @var User $user */ | 
					
						
							|  |  |  |         $user      = auth()->user(); | 
					
						
							|  |  |  |         $siteOwner = config('firefly.site_owner'); | 
					
						
							| 
									
										
										
										
											2024-10-08 07:21:23 +02:00
										 |  |  |         $title     = (string) trans('firefly.two_factor_forgot_title'); | 
					
						
							| 
									
										
										
										
											2020-03-17 15:01:00 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-01-29 14:11:12 +01:00
										 |  |  |         return view('auth.lost-two-factor', compact('user', 'siteOwner', 'title')); | 
					
						
							| 
									
										
										
										
											2020-03-10 18:29:27 +01:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-08-04 07:00:47 +02:00
										 |  |  |     /** | 
					
						
							| 
									
										
										
										
											2023-12-20 19:35:52 +01:00
										 |  |  |      * @return Redirector|RedirectResponse | 
					
						
							| 
									
										
										
										
											2019-08-04 07:00:47 +02:00
										 |  |  |      */ | 
					
						
							|  |  |  |     public function submitMFA(Request $request) | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         /** @var array $mfaHistory */ | 
					
						
							| 
									
										
										
										
											2024-10-14 05:14:52 +02:00
										 |  |  |         $mfaHistory    = app('preferences')->get('mfa_history', [])->data; | 
					
						
							|  |  |  |         $mfaCode       = (string) $request->get('one_time_password'); | 
					
						
							| 
									
										
										
										
											2019-08-04 07:00:47 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |         // is in history? then refuse to use it.
 | 
					
						
							|  |  |  |         if ($this->inMFAHistory($mfaCode, $mfaHistory)) { | 
					
						
							|  |  |  |             $this->filterMFAHistory(); | 
					
						
							|  |  |  |             session()->flash('error', trans('firefly.wrong_mfa_code')); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             return redirect(route('home')); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         /** @var Authenticator $authenticator */ | 
					
						
							|  |  |  |         $authenticator = app(Authenticator::class)->boot($request); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-08 14:45:37 +02:00
										 |  |  |         // if not OK, save error.
 | 
					
						
							|  |  |  |         if (!$authenticator->isAuthenticated()) { | 
					
						
							| 
									
										
										
										
											2024-10-14 05:14:52 +02:00
										 |  |  |             $user    = auth()->user(); | 
					
						
							| 
									
										
										
										
											2024-10-08 14:45:37 +02:00
										 |  |  |             $this->addToMFAFailureCounter(); | 
					
						
							|  |  |  |             $counter = $this->getMFAFailureCounter(); | 
					
						
							|  |  |  |             if (3 === $counter || 10 === $counter) { | 
					
						
							|  |  |  |                 // do not reset MFA failure counter, but DO send a warning to the user.
 | 
					
						
							|  |  |  |                 Log::channel('audit')->info(sprintf('User "%s" has had %d failed MFA attempts.', $user->email, $counter)); | 
					
						
							|  |  |  |                 event(new MFAManyFailedAttempts($user, $counter)); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             unset($user); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-08-04 07:00:47 +02:00
										 |  |  |         if ($authenticator->isAuthenticated()) { | 
					
						
							|  |  |  |             // save MFA in preferences
 | 
					
						
							|  |  |  |             $this->addToMFAHistory($mfaCode); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-08 14:45:37 +02:00
										 |  |  |             // reset failure count
 | 
					
						
							|  |  |  |             $this->resetMFAFailureCounter(); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-08-04 07:00:47 +02:00
										 |  |  |             // otp auth success!
 | 
					
						
							|  |  |  |             return redirect(route('home')); | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2019-08-04 07:10:18 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |         // could be user has a backup code.
 | 
					
						
							|  |  |  |         if ($this->isBackupCode($mfaCode)) { | 
					
						
							|  |  |  |             $this->removeFromBackupCodes($mfaCode); | 
					
						
							|  |  |  |             $authenticator->login(); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-08 14:45:37 +02:00
										 |  |  |             // reset failure count
 | 
					
						
							|  |  |  |             $this->resetMFAFailureCounter(); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-08-04 07:10:18 +02:00
										 |  |  |             session()->flash('info', trans('firefly.mfa_backup_code')); | 
					
						
							| 
									
										
										
										
											2024-10-08 07:21:23 +02:00
										 |  |  |             // send user notification.
 | 
					
						
							|  |  |  |             $user = auth()->user(); | 
					
						
							|  |  |  |             Log::channel('audit')->info(sprintf('User "%s" has used a backup code.', $user->email)); | 
					
						
							|  |  |  |             event(new MFAUsedBackupCode($user)); | 
					
						
							| 
									
										
										
										
											2019-08-04 07:10:18 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |             return redirect(route('home')); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-08-04 07:00:47 +02:00
										 |  |  |         session()->flash('error', trans('firefly.wrong_mfa_code')); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return redirect(route('home')); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							| 
									
										
										
										
											2023-06-21 12:34:58 +02:00
										 |  |  |      * Each MFA history has a timestamp and a code, saving the MFA entries for 5 minutes. So if the | 
					
						
							|  |  |  |      * submitted MFA code has been submitted in the last 5 minutes, it won't work despite being valid. | 
					
						
							| 
									
										
										
										
											2019-08-04 07:00:47 +02:00
										 |  |  |      */ | 
					
						
							| 
									
										
										
										
											2023-06-21 12:34:58 +02:00
										 |  |  |     private function inMFAHistory(string $mfaCode, array $mfaHistory): bool | 
					
						
							| 
									
										
										
										
											2019-08-04 07:00:47 +02:00
										 |  |  |     { | 
					
						
							| 
									
										
										
										
											2025-05-24 05:52:31 +02:00
										 |  |  |         $now = Carbon::now()->getTimestamp(); | 
					
						
							| 
									
										
										
										
											2023-06-21 12:34:58 +02:00
										 |  |  |         foreach ($mfaHistory as $entry) { | 
					
						
							|  |  |  |             $time = $entry['time']; | 
					
						
							|  |  |  |             $code = $entry['code']; | 
					
						
							|  |  |  |             if ($code === $mfaCode && $now - $time <= 300) { | 
					
						
							|  |  |  |                 return true; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2019-08-04 07:00:47 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-21 12:34:58 +02:00
										 |  |  |         return false; | 
					
						
							| 
									
										
										
										
											2019-08-04 07:00:47 +02:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Remove old entries from the preferences array. | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     private function filterMFAHistory(): void | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         /** @var array $mfaHistory */ | 
					
						
							| 
									
										
										
										
											2023-10-29 12:10:03 +01:00
										 |  |  |         $mfaHistory = app('preferences')->get('mfa_history', [])->data; | 
					
						
							| 
									
										
										
										
											2019-08-04 07:00:47 +02:00
										 |  |  |         $newHistory = []; | 
					
						
							| 
									
										
										
										
											2025-05-24 05:52:31 +02:00
										 |  |  |         $now        = Carbon::now()->getTimestamp(); | 
					
						
							| 
									
										
										
										
											2019-08-04 07:00:47 +02:00
										 |  |  |         foreach ($mfaHistory as $entry) { | 
					
						
							|  |  |  |             $time = $entry['time']; | 
					
						
							|  |  |  |             $code = $entry['code']; | 
					
						
							|  |  |  |             if ($now - $time <= 300) { | 
					
						
							|  |  |  |                 $newHistory[] = [ | 
					
						
							|  |  |  |                     'time' => $time, | 
					
						
							|  |  |  |                     'code' => $code, | 
					
						
							|  |  |  |                 ]; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2023-10-29 12:10:03 +01:00
										 |  |  |         app('preferences')->set('mfa_history', $newHistory); | 
					
						
							| 
									
										
										
										
											2019-08-04 07:00:47 +02:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-22 08:43:12 +01:00
										 |  |  |     private function addToMFAFailureCounter(): void | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $preference = (int) app('preferences')->get('mfa_failure_count', 0)->data; | 
					
						
							|  |  |  |         ++$preference; | 
					
						
							|  |  |  |         Log::channel('audit')->info(sprintf('MFA failure count is set to %d.', $preference)); | 
					
						
							|  |  |  |         app('preferences')->set('mfa_failure_count', $preference); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     private function getMFAFailureCounter(): int | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $value = (int) app('preferences')->get('mfa_failure_count', 0)->data; | 
					
						
							|  |  |  |         Log::channel('audit')->info(sprintf('MFA failure count is %d.', $value)); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return $value; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-21 12:34:58 +02:00
										 |  |  |     private function addToMFAHistory(string $mfaCode): void | 
					
						
							| 
									
										
										
										
											2019-08-04 07:00:47 +02:00
										 |  |  |     { | 
					
						
							| 
									
										
										
										
											2023-06-21 12:34:58 +02:00
										 |  |  |         /** @var array $mfaHistory */ | 
					
						
							| 
									
										
										
										
											2023-10-29 12:10:03 +01:00
										 |  |  |         $mfaHistory   = app('preferences')->get('mfa_history', [])->data; | 
					
						
							| 
									
										
										
										
											2023-06-21 12:34:58 +02:00
										 |  |  |         $entry        = [ | 
					
						
							| 
									
										
										
										
											2025-05-24 05:52:31 +02:00
										 |  |  |             'time' => Carbon::now()->getTimestamp(), | 
					
						
							| 
									
										
										
										
											2023-06-21 12:34:58 +02:00
										 |  |  |             'code' => $mfaCode, | 
					
						
							|  |  |  |         ]; | 
					
						
							|  |  |  |         $mfaHistory[] = $entry; | 
					
						
							| 
									
										
										
										
											2019-08-04 07:00:47 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-10-29 12:10:03 +01:00
										 |  |  |         app('preferences')->set('mfa_history', $mfaHistory); | 
					
						
							| 
									
										
										
										
											2023-06-21 12:34:58 +02:00
										 |  |  |         $this->filterMFAHistory(); | 
					
						
							| 
									
										
										
										
											2019-08-04 07:00:47 +02:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2019-08-04 07:10:18 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-22 08:43:12 +01:00
										 |  |  |     private function resetMFAFailureCounter(): void | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         app('preferences')->set('mfa_failure_count', 0); | 
					
						
							|  |  |  |         Log::channel('audit')->info('MFA failure count is set to zero.'); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-08-04 07:10:18 +02:00
										 |  |  |     /** | 
					
						
							|  |  |  |      * Checks if code is in users backup codes. | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     private function isBackupCode(string $mfaCode): bool | 
					
						
							|  |  |  |     { | 
					
						
							| 
									
										
										
										
											2023-10-29 12:10:03 +01:00
										 |  |  |         $list = app('preferences')->get('mfa_recovery', [])->data; | 
					
						
							| 
									
										
										
										
											2023-12-10 06:51:59 +01:00
										 |  |  |         if (!is_array($list)) { | 
					
						
							| 
									
										
										
										
											2023-11-26 12:10:42 +01:00
										 |  |  |             $list = []; | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2019-08-04 07:10:18 +02:00
										 |  |  |         if (in_array($mfaCode, $list, true)) { | 
					
						
							|  |  |  |             return true; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return false; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Remove the used code from the list of backup codes. | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     private function removeFromBackupCodes(string $mfaCode): void | 
					
						
							|  |  |  |     { | 
					
						
							| 
									
										
										
										
											2024-10-14 05:14:52 +02:00
										 |  |  |         $list    = app('preferences')->get('mfa_recovery', [])->data; | 
					
						
							| 
									
										
										
										
											2023-12-10 06:51:59 +01:00
										 |  |  |         if (!is_array($list)) { | 
					
						
							| 
									
										
										
										
											2023-11-26 12:10:42 +01:00
										 |  |  |             $list = []; | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2019-08-04 07:10:18 +02:00
										 |  |  |         $newList = array_values(array_diff($list, [$mfaCode])); | 
					
						
							| 
									
										
										
										
											2024-10-08 07:21:23 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |         // if the list is 3 or less, send a notification.
 | 
					
						
							| 
									
										
										
										
											2024-10-08 14:45:37 +02:00
										 |  |  |         if (count($newList) <= 3 && count($newList) > 0) { | 
					
						
							| 
									
										
										
										
											2024-10-08 07:21:23 +02:00
										 |  |  |             $user = auth()->user(); | 
					
						
							|  |  |  |             Log::channel('audit')->info(sprintf('User "%s" has used a backup code. They have %d backup codes left.', $user->email, count($newList))); | 
					
						
							|  |  |  |             event(new MFABackupFewLeft($user, count($newList))); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         // if the list is empty, send notification
 | 
					
						
							| 
									
										
										
										
											2024-10-08 14:45:37 +02:00
										 |  |  |         if (0 === count($newList)) { | 
					
						
							| 
									
										
										
										
											2024-10-08 07:21:23 +02:00
										 |  |  |             $user = auth()->user(); | 
					
						
							|  |  |  |             Log::channel('audit')->info(sprintf('User "%s" has used their last backup code.', $user->email)); | 
					
						
							|  |  |  |             event(new MFABackupNoLeft($user)); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-10-29 12:10:03 +01:00
										 |  |  |         app('preferences')->set('mfa_recovery', $newList); | 
					
						
							| 
									
										
										
										
											2019-08-04 07:10:18 +02:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2016-03-29 16:17:06 +02:00
										 |  |  | } |