From 4cb95d80639045b55fd79104f6286fbb1fb44a88 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 27 Jun 2020 11:06:35 -0700 Subject: [PATCH] Add test coverage for 2fa --- .../Api/Client/TwoFactorController.php | 8 +- .../Api/Client/TwoFactorControllerTest.php | 158 ++++++++++++++++++ 2 files changed, 162 insertions(+), 4 deletions(-) create mode 100644 tests/Integration/Api/Client/TwoFactorControllerTest.php diff --git a/app/Http/Controllers/Api/Client/TwoFactorController.php b/app/Http/Controllers/Api/Client/TwoFactorController.php index 0dae225e5..8c8acfdf4 100644 --- a/app/Http/Controllers/Api/Client/TwoFactorController.php +++ b/app/Http/Controllers/Api/Client/TwoFactorController.php @@ -61,11 +61,11 @@ class TwoFactorController extends ClientApiController */ public function index(Request $request) { - if ($request->user()->totp_enabled) { + if ($request->user()->use_totp) { throw new BadRequestHttpException('Two-factor authentication is already enabled on this account.'); } - return JsonResponse::create([ + return new JsonResponse([ 'data' => [ 'image_url_data' => $this->setupService->handle($request->user()), ], @@ -98,7 +98,7 @@ class TwoFactorController extends ClientApiController $this->toggleTwoFactorService->handle($request->user(), $request->input('code'), true); - return JsonResponse::create([], Response::HTTP_NO_CONTENT); + return new JsonResponse([], Response::HTTP_NO_CONTENT); } /** @@ -124,6 +124,6 @@ class TwoFactorController extends ClientApiController 'use_totp' => false, ]); - return JsonResponse::create([], Response::HTTP_NO_CONTENT); + return new JsonResponse([], Response::HTTP_NO_CONTENT); } } diff --git a/tests/Integration/Api/Client/TwoFactorControllerTest.php b/tests/Integration/Api/Client/TwoFactorControllerTest.php new file mode 100644 index 000000000..b49c1b57c --- /dev/null +++ b/tests/Integration/Api/Client/TwoFactorControllerTest.php @@ -0,0 +1,158 @@ +forceDelete(); + + parent::tearDown(); + } + + /** + * Test that image data for enabling 2FA is returned by the endpoint and that the user + * record in the database is updated as expected. + */ + public function testTwoFactorImageDataIsReturned() + { + /** @var \Pterodactyl\Models\User $user */ + $user = factory(User::class)->create(['use_totp' => false]); + + $this->assertFalse($user->use_totp); + $this->assertEmpty($user->totp_secret); + $this->assertEmpty($user->totp_authenticated_at); + + $response = $this->actingAs($user)->getJson('/api/client/account/two-factor'); + + $response->assertOk(); + $response->assertJsonStructure(['data' => ['image_url_data']]); + + $user = $user->refresh(); + + $this->assertFalse($user->use_totp); + $this->assertNotEmpty($user->totp_secret); + $this->assertEmpty($user->totp_authenticated_at); + } + + /** + * Test that an error is returned if the user's account already has 2FA enabled on it. + */ + public function testErrorIsReturnedWhenTwoFactorIsAlreadyEnabled() + { + /** @var \Pterodactyl\Models\User $user */ + $user = factory(User::class)->create(['use_totp' => true]); + + $response = $this->actingAs($user)->getJson('/api/client/account/two-factor'); + + $response->assertStatus(Response::HTTP_BAD_REQUEST); + $response->assertJsonPath('errors.0.code', 'BadRequestHttpException'); + $response->assertJsonPath('errors.0.detail', 'Two-factor authentication is already enabled on this account.'); + } + + /** + * Test that a validation error is thrown if invalid data is passed to the 2FA endpoint. + */ + public function testValidationErrorIsReturnedIfInvalidDataIsPassedToEnabled2FA() + { + /** @var \Pterodactyl\Models\User $user */ + $user = factory(User::class)->create(['use_totp' => false]); + + $response = $this->actingAs($user)->postJson('/api/client/account/two-factor', [ + 'code' => '', + ]); + + $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); + $response->assertJsonPath('errors.0.code', 'required'); + } + + /** + * Tests that 2FA can be enabled on an account for the user. + */ + public function testTwoFactorCanBeEnabledOnAccount() + { + /** @var \Pterodactyl\Models\User $user */ + $user = factory(User::class)->create(['use_totp' => false]); + + // Make the initial call to get the account setup for 2FA. + $this->actingAs($user)->getJson('/api/client/account/two-factor')->assertOk(); + + $user = $user->refresh(); + $this->assertNotNull($user->totp_secret); + + /** @var \PragmaRX\Google2FA\Google2FA $service */ + $service = $this->app->make(Google2FA::class); + + $secret = decrypt($user->totp_secret); + $token = $service->getCurrentOtp($secret); + + $response = $this->actingAs($user)->postJson('/api/client/account/two-factor', [ + 'code' => $token, + ]); + + $response->assertStatus(Response::HTTP_NO_CONTENT); + + $user = $user->refresh(); + + $this->assertTrue($user->use_totp); + } + + /** + * Test that two factor authentication can be disabled on an account as long as the password + * provided is valid for the account. + */ + public function testTwoFactorCanBeDisabledOnAccount() + { + Carbon::setTestNow(Carbon::now()); + + /** @var \Pterodactyl\Models\User $user */ + $user = factory(User::class)->create(['use_totp' => true]); + + $response = $this->actingAs($user)->deleteJson('/api/client/account/two-factor', [ + 'password' => 'invalid', + ]); + + $response->assertStatus(Response::HTTP_BAD_REQUEST); + $response->assertJsonPath('errors.0.code', 'BadRequestHttpException'); + $response->assertJsonPath('errors.0.detail', 'The password provided was not valid.'); + + $response = $this->actingAs($user)->deleteJson('/api/client/account/two-factor', [ + 'password' => 'password', + ]); + + $response->assertStatus(Response::HTTP_NO_CONTENT); + + $user = $user->refresh(); + $this->assertFalse($user->use_totp); + $this->assertNotNull($user->totp_authenticated_at); + $this->assertSame(Carbon::now()->toIso8601String(), $user->totp_authenticated_at->toIso8601String()); + } + + /** + * Test that no error is returned when trying to disabled two factor on an account where it + * was not enabled in the first place. + */ + public function testNoErrorIsReturnedIfTwoFactorIsNotEnabled() + { + Carbon::setTestNow(Carbon::now()); + + /** @var \Pterodactyl\Models\User $user */ + $user = factory(User::class)->create(['use_totp' => false]); + + $response = $this->actingAs($user)->deleteJson('/api/client/account/two-factor', [ + 'password' => 'password', + ]); + + $response->assertStatus(Response::HTTP_NO_CONTENT); + } +}