Path: blob/1.0-develop/tests/Integration/Api/Client/TwoFactorControllerTest.php
7459 views
<?php12namespace Pterodactyl\Tests\Integration\Api\Client;34use Carbon\Carbon;5use Pterodactyl\Models\User;6use Illuminate\Http\Response;7use PragmaRX\Google2FA\Google2FA;8use Pterodactyl\Models\RecoveryToken;9use PHPUnit\Framework\ExpectationFailedException;1011class TwoFactorControllerTest extends ClientApiIntegrationTestCase12{13/**14* Test that image data for enabling 2FA is returned by the endpoint and that the user15* record in the database is updated as expected.16*/17public function testTwoFactorImageDataIsReturned()18{19/** @var User $user */20$user = User::factory()->create(['use_totp' => false]);2122$this->assertFalse($user->use_totp);23$this->assertEmpty($user->totp_secret);24$this->assertEmpty($user->totp_authenticated_at);2526$response = $this->actingAs($user)->getJson('/api/client/account/two-factor');2728$response->assertOk();29$response->assertJsonStructure(['data' => ['image_url_data']]);3031$user = $user->refresh();3233$this->assertFalse($user->use_totp);34$this->assertNotEmpty($user->totp_secret);35$this->assertEmpty($user->totp_authenticated_at);36}3738/**39* Test that an error is returned if the user's account already has 2FA enabled on it.40*/41public function testErrorIsReturnedWhenTwoFactorIsAlreadyEnabled()42{43/** @var User $user */44$user = User::factory()->create(['use_totp' => true]);4546$response = $this->actingAs($user)->getJson('/api/client/account/two-factor');4748$response->assertStatus(Response::HTTP_BAD_REQUEST);49$response->assertJsonPath('errors.0.code', 'BadRequestHttpException');50$response->assertJsonPath('errors.0.detail', 'Two-factor authentication is already enabled on this account.');51}5253/**54* Test that a validation error is thrown if invalid data is passed to the 2FA endpoint.55*/56public function testValidationErrorIsReturnedIfInvalidDataIsPassedToEnabled2FA()57{58/** @var User $user */59$user = User::factory()->create(['use_totp' => false]);6061$this->actingAs($user)62->postJson('/api/client/account/two-factor', ['code' => ''])63->assertUnprocessable()64->assertJsonPath('errors.0.meta.rule', 'required')65->assertJsonPath('errors.0.meta.source_field', 'code')66->assertJsonPath('errors.1.meta.rule', 'required')67->assertJsonPath('errors.1.meta.source_field', 'password');68}6970/**71* Tests that 2FA can be enabled on an account for the user.72*/73public function testTwoFactorCanBeEnabledOnAccount()74{75/** @var User $user */76$user = User::factory()->create(['use_totp' => false]);7778// Make the initial call to get the account setup for 2FA.79$this->actingAs($user)->getJson('/api/client/account/two-factor')->assertOk();8081$user = $user->refresh();82$this->assertNotNull($user->totp_secret);8384/** @var Google2FA $service */85$service = $this->app->make(Google2FA::class);8687$secret = decrypt($user->totp_secret);88$token = $service->getCurrentOtp($secret);8990$response = $this->actingAs($user)->postJson('/api/client/account/two-factor', [91'code' => $token,92'password' => 'password',93]);9495$response->assertOk();96$response->assertJsonPath('object', 'recovery_tokens');9798$user = $user->refresh();99$this->assertTrue($user->use_totp);100101$tokens = RecoveryToken::query()->where('user_id', $user->id)->get();102$this->assertCount(10, $tokens);103$this->assertStringStartsWith('$2y$10$', $tokens[0]->token);104// Ensure the recovery tokens that were created include a "created_at" timestamp105// value on them.106//107// @see https://github.com/pterodactyl/panel/issues/3163108$this->assertNotNull($tokens[0]->created_at);109110$tokens = $tokens->pluck('token')->toArray();111112foreach ($response->json('attributes.tokens') as $raw) {113foreach ($tokens as $hashed) {114if (password_verify($raw, $hashed)) {115continue 2;116}117}118119throw new ExpectationFailedException(sprintf('Failed asserting that token [%s] exists as a hashed value in recovery_tokens table.', $raw));120}121}122123/**124* Test that two-factor authentication can be disabled on an account as long as the password125* provided is valid for the account.126*/127public function testTwoFactorCanBeDisabledOnAccount()128{129Carbon::setTestNow(Carbon::now());130131/** @var User $user */132$user = User::factory()->create(['use_totp' => true]);133134$response = $this->actingAs($user)->postJson('/api/client/account/two-factor/disable', [135'password' => 'invalid',136]);137138$response->assertStatus(Response::HTTP_BAD_REQUEST);139$response->assertJsonPath('errors.0.code', 'BadRequestHttpException');140$response->assertJsonPath('errors.0.detail', 'The password provided was not valid.');141142$response = $this->actingAs($user)->postJson('/api/client/account/two-factor/disable', [143'password' => 'password',144]);145146$response->assertStatus(Response::HTTP_NO_CONTENT);147148$user = $user->refresh();149$this->assertFalse($user->use_totp);150$this->assertNotNull($user->totp_authenticated_at);151$this->assertSame(Carbon::now()->toAtomString(), $user->totp_authenticated_at->toAtomString());152}153154/**155* Test that no error is returned when trying to disabled two factor on an account where it156* was not enabled in the first place.157*/158public function testNoErrorIsReturnedIfTwoFactorIsNotEnabled()159{160Carbon::setTestNow(Carbon::now());161162/** @var User $user */163$user = User::factory()->create(['use_totp' => false]);164165$response = $this->actingAs($user)->postJson('/api/client/account/two-factor/disable', [166'password' => 'password',167]);168169$response->assertStatus(Response::HTTP_NO_CONTENT);170}171172/**173* Test that a valid account password is required when enabling two-factor.174*/175public function testEnablingTwoFactorRequiresValidPassword()176{177$user = User::factory()->create(['use_totp' => false]);178179$this->actingAs($user)180->postJson('/api/client/account/two-factor', [181'code' => '123456',182'password' => 'foo',183])184->assertStatus(Response::HTTP_BAD_REQUEST)185->assertJsonPath('errors.0.detail', 'The password provided was not valid.');186187$this->assertFalse($user->refresh()->use_totp);188}189190/**191* Test that a valid account password is required when disabling two-factor.192*/193public function testDisablingTwoFactorRequiresValidPassword()194{195$user = User::factory()->create(['use_totp' => true]);196197$this->actingAs($user)198->postJson('/api/client/account/two-factor/disable', [199'password' => 'foo',200])201->assertStatus(Response::HTTP_BAD_REQUEST)202->assertJsonPath('errors.0.detail', 'The password provided was not valid.');203204$this->assertTrue($user->refresh()->use_totp);205}206}207208209