From 1f4c4c4b6c2c39dc4917600220d78a44580d1327 Mon Sep 17 00:00:00 2001 From: Simon Van Accoleyen Date: Tue, 4 May 2021 20:30:52 +0200 Subject: [PATCH] feat: allow recovery codes when disabling 2FA (#4970) --- CONTRIBUTORS | 1 + .../Auth/RecoveryLoginController.php | 30 +--------------- .../Settings/MultiFAController.php | 35 ++++++++++++++----- app/Models/User/RecoveryCode.php | 12 +++++++ app/Models/User/User.php | 22 ++++++++++++ .../js/components/settings/MfaActivate.vue | 4 +-- resources/lang/en/auth.php | 1 + 7 files changed, 66 insertions(+), 39 deletions(-) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 2fafb439e0b..8a4f41153f4 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -49,3 +49,4 @@ Jack Kuo @JackKuo-tw Russell Ault @RussellAult Martijn van der Ven @Zegnat Matthew Fitzgerald @mfitzgerald2 +Simon Van Accoleyen @SimonVanacco diff --git a/app/Http/Controllers/Auth/RecoveryLoginController.php b/app/Http/Controllers/Auth/RecoveryLoginController.php index 33e756b166c..179dd756bb1 100644 --- a/app/Http/Controllers/Auth/RecoveryLoginController.php +++ b/app/Http/Controllers/Auth/RecoveryLoginController.php @@ -2,11 +2,9 @@ namespace App\Http\Controllers\Auth; -use App\Models\User\User; use Illuminate\Http\Request; use App\Events\RecoveryLogin; use App\Http\Controllers\Controller; -use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Validator; use Illuminate\Foundation\Auth\RedirectsUsers; @@ -51,7 +49,7 @@ public function store(Request $request) $recovery = $request->input('recovery'); if ($user instanceof \App\Models\User\User && - $this->recoveryLogin($user, $recovery)) { + $user->recoveryChallenge($recovery)) { $this->fireLoginEvent($user); } else { abort(403); @@ -60,32 +58,6 @@ public function store(Request $request) return redirect($this->redirectPath()); } - /** - * Try login with the recovery code. - * - * @param \App\Models\User\User $user - * @param string $recovery - * @return bool - */ - protected function recoveryLogin(User $user, string $recovery) - { - $recoveryCodes = $user->recoveryCodes() - ->where('used', false) - ->get(); - - foreach ($recoveryCodes as $recoveryCode) { - if ($recoveryCode->recovery == $recovery) { - $recoveryCode->forceFill([ - 'used' => true, - ])->save(); - - return true; - } - } - - return false; - } - /** * Fire the login event. * diff --git a/app/Http/Controllers/Settings/MultiFAController.php b/app/Http/Controllers/Settings/MultiFAController.php index 2c975270272..b246bed9067 100644 --- a/app/Http/Controllers/Settings/MultiFAController.php +++ b/app/Http/Controllers/Settings/MultiFAController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers\Settings; +use App\Models\User\User; use Illuminate\Http\Request; use App\Http\Controllers\Controller; use App\Traits\JsonRespondController; @@ -108,23 +109,41 @@ public function deactivateTwoFactor(Request $request) $user = $request->user(); + if ($this->validateTwoFactorLogin($request, $user, $request['one_time_password'])) { + //make secret column blank + $user->google2fa_secret = null; + $user->save(); + + return response()->json(['success' => true]); + } + + return response()->json(['success' => false]); + } + + /** + * Validate 2nd factor for user with 2FA code or recovery code. + * + * @param Request $request + * @param User $user + * @param string $oneTimePassword + * @return bool + */ + private function validateTwoFactorLogin(Request $request, User $user, string $oneTimePassword): bool + { //retrieve secret $secret = $user->google2fa_secret; $authenticator = app(Authenticator::class)->boot($request); - if ($authenticator->verifyGoogle2FA($secret, $request['one_time_password'])) { - - //make secret column blank - $user->google2fa_secret = null; - $user->save(); - + // try provided token as a 2FA code, or as a recovery code + if ($authenticator->verifyGoogle2FA($secret, $oneTimePassword) + || $user->recoveryChallenge($oneTimePassword)) { $authenticator->logout(); - return response()->json(['success' => true]); + return true; } - return response()->json(['success' => false]); + return false; } /** diff --git a/app/Models/User/RecoveryCode.php b/app/Models/User/RecoveryCode.php index 64f1f0bdfe6..7f7863a6ed6 100644 --- a/app/Models/User/RecoveryCode.php +++ b/app/Models/User/RecoveryCode.php @@ -3,6 +3,7 @@ namespace App\Models\User; use App\Models\ModelBinding as Model; +use Illuminate\Database\Eloquent\Builder; class RecoveryCode extends Model { @@ -18,4 +19,15 @@ class RecoveryCode extends Model 'user_id', 'recovery', ]; + + /** + * Scope a query to only include unused code. + * + * @param Builder $query + * @return Builder + */ + public function scopeUnused($query) + { + return $query->where('used', 0); + } } diff --git a/app/Models/User/User.php b/app/Models/User/User.php index b7341d7e896..37977e64579 100644 --- a/app/Models/User/User.php +++ b/app/Models/User/User.php @@ -257,4 +257,26 @@ public function preferredLocale() { return $this->locale; } + + /** + * Try using a recovery code. + * + * @param string $recovery + * @return bool + */ + public function recoveryChallenge(string $recovery): bool + { + $recoveryCodes = $this->recoveryCodes()->unused()->get(); + + foreach ($recoveryCodes as $recoveryCode) { + if ($recoveryCode->recovery === $recovery) { + $recoveryCode->used = true; + $recoveryCode->save(); + + return true; + } + } + + return false; + } } diff --git a/resources/js/components/settings/MfaActivate.vue b/resources/js/components/settings/MfaActivate.vue index c106932c187..80a7adda5e1 100644 --- a/resources/js/components/settings/MfaActivate.vue +++ b/resources/js/components/settings/MfaActivate.vue @@ -60,8 +60,8 @@ diff --git a/resources/lang/en/auth.php b/resources/lang/en/auth.php index 2e9e4d341d6..ba264de4641 100644 --- a/resources/lang/en/auth.php +++ b/resources/lang/en/auth.php @@ -31,6 +31,7 @@ '2fa_wrong_validation' => 'The two factor authentication has failed.', '2fa_one_time_password' => 'Two factor authentication code', '2fa_recuperation_code' => 'Enter a two factor recovery code', + '2fa_one_time_or_recuperation' => 'Enter a two factor authentication code or a recovery code', '2fa_otp_help' => 'Open up your two factor authentication mobile app and copy the code', 'login_to_account' => 'Login to your account',