2020-06-27 18:06:35 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace Pterodactyl\Tests\Integration\Api\Client;
|
|
|
|
|
|
|
|
use Carbon\Carbon;
|
|
|
|
use Pterodactyl\Models\User;
|
|
|
|
use Illuminate\Http\Response;
|
|
|
|
use PragmaRX\Google2FA\Google2FA;
|
2020-07-10 04:32:31 +00:00
|
|
|
use Pterodactyl\Models\RecoveryToken;
|
|
|
|
use PHPUnit\Framework\ExpectationFailedException;
|
2020-06-27 18:06:35 +00:00
|
|
|
|
2020-06-27 19:04:41 +00:00
|
|
|
class TwoFactorControllerTest extends ClientApiIntegrationTestCase
|
2020-06-27 18:06:35 +00:00
|
|
|
{
|
|
|
|
/**
|
|
|
|
* 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 */
|
2021-01-23 20:09:16 +00:00
|
|
|
$user = User::factory()->create(['use_totp' => false]);
|
2020-06-27 18:06:35 +00:00
|
|
|
|
|
|
|
$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 */
|
2021-01-23 20:09:16 +00:00
|
|
|
$user = User::factory()->create(['use_totp' => true]);
|
2020-06-27 18:06:35 +00:00
|
|
|
|
|
|
|
$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 */
|
2021-01-23 20:09:16 +00:00
|
|
|
$user = User::factory()->create(['use_totp' => false]);
|
2020-06-27 18:06:35 +00:00
|
|
|
|
2022-07-03 18:27:37 +00:00
|
|
|
$this->actingAs($user)
|
|
|
|
->postJson('/api/client/account/two-factor', ['code' => ''])
|
|
|
|
->assertUnprocessable()
|
|
|
|
->assertJsonPath('errors.0.meta.rule', 'required')
|
|
|
|
->assertJsonPath('errors.0.meta.source_field', 'code')
|
|
|
|
->assertJsonPath('errors.1.meta.rule', 'required')
|
|
|
|
->assertJsonPath('errors.1.meta.source_field', 'password');
|
2020-06-27 18:06:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Tests that 2FA can be enabled on an account for the user.
|
|
|
|
*/
|
|
|
|
public function testTwoFactorCanBeEnabledOnAccount()
|
|
|
|
{
|
|
|
|
/** @var \Pterodactyl\Models\User $user */
|
2021-01-23 20:09:16 +00:00
|
|
|
$user = User::factory()->create(['use_totp' => false]);
|
2020-06-27 18:06:35 +00:00
|
|
|
|
|
|
|
// 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,
|
2022-07-03 18:27:37 +00:00
|
|
|
'password' => 'password',
|
2020-06-27 18:06:35 +00:00
|
|
|
]);
|
|
|
|
|
2020-07-10 04:32:31 +00:00
|
|
|
$response->assertOk();
|
|
|
|
$response->assertJsonPath('object', 'recovery_tokens');
|
2020-06-27 18:06:35 +00:00
|
|
|
|
|
|
|
$user = $user->refresh();
|
|
|
|
$this->assertTrue($user->use_totp);
|
2020-07-10 04:32:31 +00:00
|
|
|
|
|
|
|
$tokens = RecoveryToken::query()->where('user_id', $user->id)->get();
|
|
|
|
$this->assertCount(10, $tokens);
|
|
|
|
$this->assertStringStartsWith('$2y$10$', $tokens[0]->token);
|
2021-03-21 17:43:01 +00:00
|
|
|
// Ensure the recovery tokens that were created include a "created_at" timestamp
|
|
|
|
// value on them.
|
|
|
|
//
|
|
|
|
// @see https://github.com/pterodactyl/panel/issues/3163
|
|
|
|
$this->assertNotNull($tokens[0]->created_at);
|
2020-07-10 04:32:31 +00:00
|
|
|
|
|
|
|
$tokens = $tokens->pluck('token')->toArray();
|
|
|
|
|
|
|
|
foreach ($response->json('attributes.tokens') as $raw) {
|
|
|
|
foreach ($tokens as $hashed) {
|
|
|
|
if (password_verify($raw, $hashed)) {
|
|
|
|
continue 2;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-23 20:33:34 +00:00
|
|
|
throw new ExpectationFailedException(sprintf('Failed asserting that token [%s] exists as a hashed value in recovery_tokens table.', $raw));
|
2020-07-10 04:32:31 +00:00
|
|
|
}
|
2020-06-27 18:06:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-10-14 16:59:20 +00:00
|
|
|
* Test that two-factor authentication can be disabled on an account as long as the password
|
2020-06-27 18:06:35 +00:00
|
|
|
* provided is valid for the account.
|
|
|
|
*/
|
|
|
|
public function testTwoFactorCanBeDisabledOnAccount()
|
|
|
|
{
|
|
|
|
Carbon::setTestNow(Carbon::now());
|
|
|
|
|
|
|
|
/** @var \Pterodactyl\Models\User $user */
|
2021-01-23 20:09:16 +00:00
|
|
|
$user = User::factory()->create(['use_totp' => true]);
|
2020-06-27 18:06:35 +00:00
|
|
|
|
|
|
|
$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);
|
2022-10-14 16:59:20 +00:00
|
|
|
$this->assertSame(Carbon::now()->toAtomString(), $user->totp_authenticated_at->toAtomString());
|
2020-06-27 18:06:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 */
|
2021-01-23 20:09:16 +00:00
|
|
|
$user = User::factory()->create(['use_totp' => false]);
|
2020-06-27 18:06:35 +00:00
|
|
|
|
|
|
|
$response = $this->actingAs($user)->deleteJson('/api/client/account/two-factor', [
|
|
|
|
'password' => 'password',
|
|
|
|
]);
|
|
|
|
|
|
|
|
$response->assertStatus(Response::HTTP_NO_CONTENT);
|
|
|
|
}
|
2022-07-03 18:27:37 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Test that a valid account password is required when enabling two-factor.
|
|
|
|
*/
|
|
|
|
public function testEnablingTwoFactorRequiresValidPassword()
|
|
|
|
{
|
|
|
|
$user = User::factory()->create(['use_totp' => false]);
|
|
|
|
|
|
|
|
$this->actingAs($user)
|
|
|
|
->postJson('/api/client/account/two-factor', [
|
|
|
|
'code' => '123456',
|
|
|
|
'password' => 'foo',
|
|
|
|
])
|
|
|
|
->assertStatus(Response::HTTP_BAD_REQUEST)
|
|
|
|
->assertJsonPath('errors.0.detail', 'The password provided was not valid.');
|
|
|
|
|
|
|
|
$this->assertFalse($user->refresh()->use_totp);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Test that a valid account password is required when disabling two-factor.
|
|
|
|
*/
|
|
|
|
public function testDisablingTwoFactorRequiresValidPassword()
|
|
|
|
{
|
|
|
|
$user = User::factory()->create(['use_totp' => true]);
|
|
|
|
|
|
|
|
$this->actingAs($user)
|
|
|
|
->deleteJson('/api/client/account/two-factor', [
|
|
|
|
'password' => 'foo',
|
|
|
|
])
|
|
|
|
->assertStatus(Response::HTTP_BAD_REQUEST)
|
|
|
|
->assertJsonPath('errors.0.detail', 'The password provided was not valid.');
|
|
|
|
|
|
|
|
$this->assertTrue($user->refresh()->use_totp);
|
|
|
|
}
|
2020-06-27 18:06:35 +00:00
|
|
|
}
|