From a998b463e30f565dca789083c7083ce65a836480 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 2 Jul 2020 21:55:25 -0700 Subject: [PATCH] Generate recovery tokens when enabling 2FA on an account --- .../Api/Client/TwoFactorController.php | 9 +++- app/Models/RecoveryToken.php | 39 +++++++++++++++ app/Models/User.php | 9 ++++ .../Eloquent/RecoveryTokenRepository.php | 16 ++++++ app/Services/Users/ToggleTwoFactorService.php | 49 ++++++++++++++++--- ...3612_create_user_recovery_tokens_table.php | 31 ++++++++++++ 6 files changed, 145 insertions(+), 8 deletions(-) create mode 100644 app/Models/RecoveryToken.php create mode 100644 app/Repositories/Eloquent/RecoveryTokenRepository.php create mode 100644 database/migrations/2020_07_02_213612_create_user_recovery_tokens_table.php diff --git a/app/Http/Controllers/Api/Client/TwoFactorController.php b/app/Http/Controllers/Api/Client/TwoFactorController.php index 8c8acfdf4..93be78fbc 100644 --- a/app/Http/Controllers/Api/Client/TwoFactorController.php +++ b/app/Http/Controllers/Api/Client/TwoFactorController.php @@ -96,9 +96,14 @@ class TwoFactorController extends ClientApiController throw new ValidationException($validator); } - $this->toggleTwoFactorService->handle($request->user(), $request->input('code'), true); + $tokens = $this->toggleTwoFactorService->handle($request->user(), $request->input('code'), true); - return new JsonResponse([], Response::HTTP_NO_CONTENT); + return new JsonResponse([ + 'object' => 'recovery_tokens', + 'attributes' => [ + 'tokens' => $tokens, + ], + ]); } /** diff --git a/app/Models/RecoveryToken.php b/app/Models/RecoveryToken.php new file mode 100644 index 000000000..7be74f53c --- /dev/null +++ b/app/Models/RecoveryToken.php @@ -0,0 +1,39 @@ + 'required|string', + ]; + + /** + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function user() + { + return $this->belongsTo(User::class); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index c93fae6dd..47334ccee 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -39,6 +39,7 @@ use Pterodactyl\Notifications\SendPasswordReset as ResetPasswordNotification; * @property \Pterodactyl\Models\ApiKey[]|\Illuminate\Database\Eloquent\Collection $apiKeys * @property \Pterodactyl\Models\Server[]|\Illuminate\Database\Eloquent\Collection $servers * @property \Pterodactyl\Models\DaemonKey[]|\Illuminate\Database\Eloquent\Collection $keys + * @property \Pterodactyl\Models\RecoveryToken[]|\Illuminate\Database\Eloquent\Collection $recoveryCodes */ class User extends Model implements AuthenticatableContract, @@ -251,4 +252,12 @@ class User extends Model implements return $this->hasMany(ApiKey::class) ->where('key_type', ApiKey::TYPE_ACCOUNT); } + + /** + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function recoveryCodes() + { + return $this->hasMany(RecoveryToken::class); + } } diff --git a/app/Repositories/Eloquent/RecoveryTokenRepository.php b/app/Repositories/Eloquent/RecoveryTokenRepository.php new file mode 100644 index 000000000..5dfeeacfe --- /dev/null +++ b/app/Repositories/Eloquent/RecoveryTokenRepository.php @@ -0,0 +1,16 @@ +encrypter = $encrypter; $this->google2FA = $google2FA; $this->repository = $repository; + $this->recoveryTokenRepository = $recoveryTokenRepository; } /** @@ -49,7 +59,7 @@ class ToggleTwoFactorService * @param \Pterodactyl\Models\User $user * @param string $token * @param bool|null $toggleState - * @return bool + * @return string[] * * @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException * @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException @@ -58,16 +68,43 @@ class ToggleTwoFactorService * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid */ - public function handle(User $user, string $token, bool $toggleState = null): bool + public function handle(User $user, string $token, bool $toggleState = null): array { $secret = $this->encrypter->decrypt($user->totp_secret); $isValidToken = $this->google2FA->verifyKey($secret, $token, config()->get('pterodactyl.auth.2fa.window')); if (! $isValidToken) { - throw new TwoFactorAuthenticationTokenInvalid( - 'The token provided is not valid.' - ); + throw new TwoFactorAuthenticationTokenInvalid('The token provided is not valid.'); + } + + // Now that we're enabling 2FA on the account, generate 10 recovery tokens for the account + // and store them hashed in the database. We'll return them to the caller so that the user + // can see and save them. + // + // If a user is unable to login with a 2FA token they can provide one of these backup codes + // which will then be marked as deleted from the database and will also bypass 2FA protections + // on their account. + $tokens = []; + if ((! $toggleState && ! $user->use_totp) || $toggleState) { + $inserts = []; + for ($i = 0; $i < 10; $i++) { + $token = Str::random(10); + + $inserts[] = [ + 'user_id' => $user->id, + 'token' => password_hash($token, PASSWORD_DEFAULT), + ]; + + $tokens[] = $token; + } + + // Bulk insert the hashed tokens. + $this->recoveryTokenRepository->insert($inserts); + } elseif ($toggleState === false || $user->use_totp) { + // If we are disabling 2FA on this account we will delete all of the recovery codes + // that exist in the database for this account. + $this->recoveryTokenRepository->deleteWhere(['user_id' => $user->id]); } $this->repository->withoutFreshModel()->update($user->id, [ @@ -75,6 +112,6 @@ class ToggleTwoFactorService 'use_totp' => (is_null($toggleState) ? ! $user->use_totp : $toggleState), ]); - return true; + return $tokens; } } diff --git a/database/migrations/2020_07_02_213612_create_user_recovery_tokens_table.php b/database/migrations/2020_07_02_213612_create_user_recovery_tokens_table.php new file mode 100644 index 000000000..cca85e67f --- /dev/null +++ b/database/migrations/2020_07_02_213612_create_user_recovery_tokens_table.php @@ -0,0 +1,31 @@ +id(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('recovery_tokens'); + } +}