. */ declare(strict_types=1); namespace FireflyIII\Http\Controllers\Auth; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Http\Controllers\Controller; use FireflyIII\User; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Routing\Redirector; use PragmaRX\Google2FALaravel\Support\Authenticator; use Preferences; /** * Class TwoFactorController. */ class TwoFactorController extends Controller { /** * Create a new controller instance. */ public function __construct() { parent::__construct(); $loginProvider = config('firefly.login_provider'); $authGuard = config('firefly.authentication_guard'); if ('eloquent' !== $loginProvider || 'web' !== $authGuard) { throw new FireflyException('Using external identity provider. Cannot continue.'); } } /** * What to do if 2FA lost? * * @return mixed */ public function lostTwoFactor() { /** @var User $user */ $user = auth()->user(); $siteOwner = config('firefly.site_owner'); $title = (string) trans('firefly.two_factor_forgot_title'); return view('auth.lost-two-factor', compact('user', 'siteOwner', 'title')); } /** * @param Request $request * * @return RedirectResponse|Redirector */ public function submitMFA(Request $request) { /** @var array $mfaHistory */ $mfaHistory = Preferences::get('mfa_history', [])->data; $mfaCode = $request->get('one_time_password'); // 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); if ($authenticator->isAuthenticated()) { // save MFA in preferences $this->addToMFAHistory($mfaCode); // otp auth success! return redirect(route('home')); } // could be user has a backup code. if ($this->isBackupCode($mfaCode)) { $this->removeFromBackupCodes($mfaCode); $authenticator->login(); session()->flash('info', trans('firefly.mfa_backup_code')); return redirect(route('home')); } session()->flash('error', trans('firefly.wrong_mfa_code')); return redirect(route('home')); } /** * @param string $mfaCode */ private function addToMFAHistory(string $mfaCode): void { /** @var array $mfaHistory */ $mfaHistory = Preferences::get('mfa_history', [])->data; $entry = [ 'time' => time(), 'code' => $mfaCode, ]; $mfaHistory[] = $entry; Preferences::set('mfa_history', $mfaHistory); $this->filterMFAHistory(); } /** * Remove old entries from the preferences array. */ private function filterMFAHistory(): void { /** @var array $mfaHistory */ $mfaHistory = Preferences::get('mfa_history', [])->data; $newHistory = []; $now = time(); foreach ($mfaHistory as $entry) { $time = $entry['time']; $code = $entry['code']; if ($now - $time <= 300) { $newHistory[] = [ 'time' => $time, 'code' => $code, ]; } } Preferences::set('mfa_history', $newHistory); } /** * 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. * * @param string $mfaCode * @param array $mfaHistory * * @return bool */ private function inMFAHistory(string $mfaCode, array $mfaHistory): bool { $now = time(); foreach ($mfaHistory as $entry) { $time = $entry['time']; $code = $entry['code']; if ($code === $mfaCode && $now - $time <= 300) { return true; } } return false; } /** * Checks if code is in users backup codes. * * @param string $mfaCode * * @return bool */ private function isBackupCode(string $mfaCode): bool { $list = Preferences::get('mfa_recovery', [])->data; if (in_array($mfaCode, $list, true)) { return true; } return false; } /** * Remove the used code from the list of backup codes. * * @param string $mfaCode */ private function removeFromBackupCodes(string $mfaCode): void { $list = Preferences::get('mfa_recovery', [])->data; $newList = array_values(array_diff($list, [$mfaCode])); Preferences::set('mfa_recovery', $newList); } }