Path: blob/1.0-develop/tests/Integration/Http/Controllers/Auth/LoginCheckpointControllerTest.php
10293 views
<?php12namespace Pterodactyl\Tests\Integration\Http\Controllers\Auth;34use Carbon\Carbon;5use Pterodactyl\Models\User;6use PragmaRX\Google2FA\Google2FA;7use Illuminate\Auth\Events\Failed;8use Illuminate\Support\Facades\Event;9use Illuminate\Support\Facades\Session;10use Pterodactyl\Events\Auth\DirectLogin;11use PHPUnit\Framework\Attributes\TestWith;12use Pterodactyl\Tests\Integration\Http\HttpTestCase;13use Pterodactyl\Events\Auth\ProvidedAuthenticationToken;1415class LoginCheckpointControllerTest extends HttpTestCase16{17public function setUp(): void18{19parent::setUp();2021Event::fake([Failed::class, DirectLogin::class, ProvidedAuthenticationToken::class]);22}2324/**25* Basic test that a user can be signed in using their TOTP token and that26* the `totp_authenticated_at` field in the database is updated to the login27* verification time.28*/29#[TestWith([null])]30#[TestWith([-31])]31#[TestWith([-60])]32public function testUserCanSignInUsingTotpToken(?int $ts): void33{34$user = User::factory()->create([35'use_totp' => true,36'totp_secret' => encrypt(str_repeat('a', 16)),37'totp_authenticated_at' => is_null($ts) ? null : Carbon::now()->addSeconds($ts),38]);3940Session::put('auth_confirmation_token', [41'user_id' => $user->id,42'token_value' => 'token',43'expires_at' => now()->addMinutes(5),44]);4546$totp = $this->app->make(Google2FA::class)->getCurrentOtp(str_repeat('a', 16));4748$this->withoutExceptionHandling()->postJson(route('auth.login-checkpoint', [49'confirmation_token' => 'token',50'authentication_code' => $totp,51]))52->assertOk()53->assertSessionMissing('auth_confirmation_token')54->assertJsonPath('data.complete', true)55->assertJsonPath('data.intended', '/')56->assertJsonPath('data.user.uuid', $user->uuid);5758$this->assertEquals(now(), $user->refresh()->totp_authenticated_at);5960$this->assertAuthenticatedAs($user);6162Event::assertDispatched(fn (DirectLogin $event) => $event->user->is($user) && $event->remember);63Event::assertDispatched(fn (ProvidedAuthenticationToken $event) => $event->user->is($user));64}6566/**67* Test that a TOTP token cannot be reused by verifying that the OTP verification68* logic fails if the token's timestamp is before the `totp_authenticated_at`69* column value.70*71* @see https://github.com/pterodactyl/panel/security/advisories/GHSA-rgmp-4873-r68372*/73#[TestWith([1])]74#[TestWith([30])]75#[TestWith([80])]76public function testTotpTokenCannotBeReused(int $seconds): void77{78$user = User::factory()->create([79'use_totp' => true,80'totp_secret' => encrypt(str_repeat('a', 16)),81'totp_authenticated_at' => now()->addSeconds($seconds),82]);8384Session::put('auth_confirmation_token', [85'user_id' => $user->id,86'token_value' => 'token',87'expires_at' => now()->addMinutes(5),88]);8990$totp = $this->app->make(Google2FA::class)->getCurrentOtp(str_repeat('a', 16));9192$this->postJson(route('auth.login-checkpoint', [93'confirmation_token' => 'token',94'authentication_code' => $totp,95]))96->assertBadRequest()97->assertJsonPath('errors.0.detail', 'The two-factor authentication token was invalid.');9899$this->assertGuest();100$this->assertEquals(now()->addSeconds($seconds), $user->refresh()->totp_authenticated_at);101102Event::assertDispatched(fn (Failed $event) => $event->guard === 'auth' && $event->user->is($user));103}104105public function testEndpointReturnsErrorIfSessionMissing(): void106{107$this->postJson(route('auth.login-checkpoint'))108->assertUnprocessable()109->assertJsonPath('errors.0.meta.source_field', 'confirmation_token')110->assertJsonPath('errors.1.meta.source_field', 'authentication_code')111->assertJsonPath('errors.2.meta.source_field', 'recovery_token');112113$this->postJson(route('auth.login-checkpoint', [114'confirmation_token' => 'token',115'authentication_code' => '123456',116]))117->assertBadRequest()118->assertJsonPath('errors.0.detail', 'The authentication token provided has expired, please refresh the page and try again.');119120$this->assertGuest();121122Event::assertDispatched(fn (Failed $event) => $event->guard === 'auth');123}124125public function testEndpointAppliesThrottling(): void126{127for ($i = 0; $i < 5; ++$i) {128$this->postJson(route('auth.login-checkpoint', ['confirmation_token' => 'token', 'authentication_code' => '123456']))129->assertBadRequest();130}131132$this->postJson(route('auth.login-checkpoint', ['confirmation_token' => 'token', 'authentication_code' => '123456']))133->assertTooManyRequests();134}135136public function testEndpointBlocksSessionDataMismatch(): void137{138$user = User::factory()->create([139'use_totp' => true,140'totp_secret' => str_repeat('a', 16),141]);142143Session::put('auth_confirmation_token', [144'user_id' => $user->id,145'token_value' => 'token',146'expires_at' => now()->addMinutes(5),147]);148149$this->postJson(route('auth.login-checkpoint', [150'confirmation_token' => 'wrong-token',151'authentication_code' => $this->app->make(Google2FA::class)->getCurrentOtp(str_repeat('a', 16)),152]))153->assertBadRequest();154155$this->assertGuest();156157Event::assertDispatched(Failed::class);158}159160public function testEndpointReturnsErrorIfUserDoesNotExist(): void161{162Session::put('auth_confirmation_token', [163'user_id' => 0,164'token_value' => 'token',165'expires_at' => now()->addMinutes(5),166]);167168$this->postJson(route('auth.login-checkpoint', [169'confirmation_token' => 'token',170'authentication_code' => '123456',171]))172->assertBadRequest()173->assertJsonPath('errors.0.detail', 'The authentication token provided has expired, please refresh the page and try again.');174}175176public function testEndpointAllowsRecoveryToken(): void177{178$user = User::factory()->create();179$token = $user->recoveryTokens()->forceCreate(['token' => password_hash('recovery', PASSWORD_DEFAULT)]);180181Session::put('auth_confirmation_token', [182'user_id' => $user->id,183'token_value' => 'token',184'expires_at' => now()->addMinutes(5),185]);186187$this->postJson(route('auth.login-checkpoint', [188'confirmation_token' => 'token',189'recovery_token' => 'invalid',190]))191->assertBadRequest()192->assertJsonPath('errors.0.detail', 'The recovery token provided is not valid.');193194$this->assertGuest();195196$this->postJson(route('auth.login-checkpoint', [197'confirmation_token' => 'token',198'recovery_token' => 'recovery',199]))200->assertOk()201->assertSessionMissing('auth_confirmation_token');202203Event::assertDispatched(ProvidedAuthenticationToken::class);204Event::assertDispatched(DirectLogin::class);205}206}207208209