tests(unit): add RequireTwoFactorAuthenticationTest
This commit is contained in:
parent
790f109e66
commit
64110d84af
9 changed files with 373 additions and 7 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -47,6 +47,7 @@ yarn-error.log
|
|||
# PHP
|
||||
/vendor
|
||||
.php_cs.cache
|
||||
.phpunit.result.cache
|
||||
|
||||
#-------------------#
|
||||
# Operating Systems #
|
||||
|
|
|
@ -6,7 +6,7 @@ use Exception;
|
|||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use LaravelWebauthn\Facades\Webauthn;
|
||||
use LaravelWebauthn\Models\WebauthnKey;
|
||||
use Pterodactyl\Models\WebauthnKey;
|
||||
use Webauthn\PublicKeyCredentialCreationOptions;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Pterodactyl\Transformers\Api\Client\WebauthnKeyTransformer;
|
||||
|
|
|
@ -5,7 +5,6 @@ namespace Pterodactyl\Models;
|
|||
use Pterodactyl\Rules\Username;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Auth\Authenticatable;
|
||||
use LaravelWebauthn\Models\WebauthnKey;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Auth\Passwords\CanResetPassword;
|
||||
|
@ -42,7 +41,7 @@ use Pterodactyl\Notifications\SendPasswordReset as ResetPasswordNotification;
|
|||
* @property \Pterodactyl\Models\Server[]|\Illuminate\Database\Eloquent\Collection $servers
|
||||
* @property \Pterodactyl\Models\UserSSHKey|\Illuminate\Database\Eloquent\Collection $sshKeys
|
||||
* @property \Pterodactyl\Models\RecoveryToken[]|\Illuminate\Database\Eloquent\Collection $recoveryTokens
|
||||
* @property \LaravelWebauthn\Models\WebauthnKey[]|\Illuminate\Database\Eloquent\Collection $webauthnKeys
|
||||
* @property \Pterodactyl\Models\WebauthnKey[]|\Illuminate\Database\Eloquent\Collection $webauthnKeys
|
||||
*/
|
||||
class User extends Model implements
|
||||
AuthenticatableContract,
|
||||
|
|
16
app/Models/WebauthnKey.php
Normal file
16
app/Models/WebauthnKey.php
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
||||
class WebauthnKey extends \LaravelWebauthn\Models\WebauthnKey
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace Pterodactyl\Transformers\Api\Client;
|
||||
|
||||
use LaravelWebauthn\Models\WebauthnKey;
|
||||
use Pterodactyl\Models\WebauthnKey;
|
||||
|
||||
class WebauthnKeyTransformer extends BaseClientTransformer
|
||||
{
|
||||
|
|
|
@ -77,8 +77,15 @@ return [
|
|||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'prefix' => '',
|
||||
'strict' => false,
|
||||
'timezone' => env('DB_TIMEZONE', Time::getMySQLTimezoneOffset(env('APP_TIMEZONE', 'UTC'))),
|
||||
'strict' => env('TESTING_DB_STRICT_MODE', false),
|
||||
'timezone' => env('TESTING_DB_TIMEZONE', Time::getMySQLTimezoneOffset(env('APP_TIMEZONE', 'UTC'))),
|
||||
'sslmode' => env('TESTING_DB_SSLMODE', 'prefer'),
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
PDO::MYSQL_ATTR_SSL_CERT => env('MYSQL_ATTR_SSL_CERT'),
|
||||
PDO::MYSQL_ATTR_SSL_KEY => env('MYSQL_ATTR_SSL_KEY'),
|
||||
PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT => env('MYSQL_ATTR_SSL_VERIFY_SERVER_CERT', true),
|
||||
]) : [],
|
||||
],
|
||||
],
|
||||
|
||||
|
|
24
database/Factories/WebauthnKeyFactory.php
Normal file
24
database/Factories/WebauthnKeyFactory.php
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Pterodactyl\Models\WebauthnKey;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class WebauthnKeyFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The name of the factory's corresponding model.
|
||||
*/
|
||||
protected $model = WebauthnKey::class;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->faker->name,
|
||||
];
|
||||
}
|
||||
}
|
|
@ -43,6 +43,7 @@ trait RequestMockHelpers
|
|||
*/
|
||||
public function generateRequestUserModel(array $args = []): User
|
||||
{
|
||||
/** @var \Pterodactyl\Models\User $user */
|
||||
$user = User::factory()->make($args);
|
||||
$this->setRequestUserModel($user);
|
||||
|
||||
|
@ -70,8 +71,9 @@ trait RequestMockHelpers
|
|||
/**
|
||||
* Set the active request object to be an instance of a mocked request.
|
||||
*/
|
||||
protected function buildRequestMock()
|
||||
protected function buildRequestMock($uri = '/')
|
||||
{
|
||||
// $this->request = Request::create($uri);
|
||||
$this->request = m::mock($this->requestMockClass);
|
||||
if (!$this->request instanceof Request) {
|
||||
throw new InvalidArgumentException('Request mock class must be an instance of ' . Request::class . ' when mocked.');
|
||||
|
|
|
@ -0,0 +1,317 @@
|
|||
<?php
|
||||
|
||||
namespace Http\Middleware;
|
||||
|
||||
use Mockery as m;
|
||||
use Pterodactyl\Models\User;
|
||||
use Pterodactyl\Models\WebauthnKey;
|
||||
use Prologue\Alerts\AlertsMessageBag;
|
||||
use Pterodactyl\Tests\Unit\Http\Middleware\MiddlewareTestCase;
|
||||
use Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication;
|
||||
use Pterodactyl\Exceptions\Http\TwoFactorAuthRequiredException;
|
||||
|
||||
class RequireTwoFactorAuthenticationTest extends MiddlewareTestCase
|
||||
{
|
||||
private $alerts;
|
||||
|
||||
/**
|
||||
* Setup tests.
|
||||
*/
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->alerts = m::mock(AlertsMessageBag::class);
|
||||
}
|
||||
|
||||
public function testNoRequirement__userWithout_2fa()
|
||||
{
|
||||
// Disable the 2FA requirement
|
||||
config()->set('pterodactyl.auth.2fa_required', RequireTwoFactorAuthentication::LEVEL_NONE);
|
||||
|
||||
$user = $this->generateRequestUserModel(['use_totp' => false]);
|
||||
|
||||
$this->assertFalse($user->use_totp);
|
||||
$this->assertEmpty($user->totp_secret);
|
||||
$this->assertEmpty($user->totp_authenticated_at);
|
||||
|
||||
$this->request->shouldReceive('getRequestUri')->withNoArgs()->andReturn('/');
|
||||
$this->request->shouldReceive('route->getName')->withNoArgs()->andReturn(null);
|
||||
$this->request->shouldReceive('isJson')->withNoArgs()->andReturn(true);
|
||||
|
||||
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
|
||||
}
|
||||
|
||||
public function testNoRequirement__userWithTotp_2fa()
|
||||
{
|
||||
// Disable the 2FA requirement
|
||||
config()->set('pterodactyl.auth.2fa_required', RequireTwoFactorAuthentication::LEVEL_NONE);
|
||||
|
||||
$user = $this->generateRequestUserModel(['use_totp' => true]);
|
||||
|
||||
$this->assertTrue($user->use_totp);
|
||||
$this->assertEmpty($user->totp_secret);
|
||||
$this->assertEmpty($user->totp_authenticated_at);
|
||||
|
||||
$this->request->shouldReceive('getRequestUri')->withNoArgs()->andReturn('/');
|
||||
$this->request->shouldReceive('route->getName')->withNoArgs()->andReturn(null);
|
||||
$this->request->shouldReceive('isJson')->withNoArgs()->andReturn(true);
|
||||
|
||||
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
|
||||
}
|
||||
|
||||
public function testNoRequirement__userWithWebauthn_2fa()
|
||||
{
|
||||
// Disable the 2FA requirement
|
||||
config()->set('pterodactyl.auth.2fa_required', RequireTwoFactorAuthentication::LEVEL_NONE);
|
||||
|
||||
/** @var \Pterodactyl\Models\User $user */
|
||||
$user = User::factory()
|
||||
->has(WebauthnKey::factory()->count(1))
|
||||
->create(['use_totp' => false]);
|
||||
$this->setRequestUserModel($user);
|
||||
|
||||
$this->assertFalse($user->use_totp);
|
||||
$this->assertEmpty($user->totp_secret);
|
||||
$this->assertEmpty($user->totp_authenticated_at);
|
||||
$this->assertNotEmpty($user->webauthnKeys);
|
||||
|
||||
$this->request->shouldReceive('getRequestUri')->withNoArgs()->andReturn('/');
|
||||
$this->request->shouldReceive('route->getName')->withNoArgs()->andReturn(null);
|
||||
$this->request->shouldReceive('isJson')->withNoArgs()->andReturn(true);
|
||||
|
||||
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
|
||||
}
|
||||
|
||||
public function testNoRequirement__guestUser()
|
||||
{
|
||||
// Disable the 2FA requirement
|
||||
config()->set('pterodactyl.auth.2fa_required', RequireTwoFactorAuthentication::LEVEL_NONE);
|
||||
|
||||
$this->setRequestUserModel();
|
||||
|
||||
$this->request->shouldReceive('getRequestUri')->withNoArgs()->andReturn('/auth/login');
|
||||
$this->request->shouldReceive('route->getName')->withNoArgs()->andReturn('auth.login');
|
||||
$this->request->shouldReceive('isJson')->withNoArgs()->andReturn(true);
|
||||
|
||||
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
|
||||
}
|
||||
|
||||
public function testAllRequirement__userWithout_2fa()
|
||||
{
|
||||
$this->expectException(TwoFactorAuthRequiredException::class);
|
||||
|
||||
// Disable the 2FA requirement
|
||||
config()->set('pterodactyl.auth.2fa_required', RequireTwoFactorAuthentication::LEVEL_ALL);
|
||||
|
||||
$user = $this->generateRequestUserModel(['use_totp' => false]);
|
||||
|
||||
$this->assertFalse($user->use_totp);
|
||||
$this->assertEmpty($user->totp_secret);
|
||||
$this->assertEmpty($user->totp_authenticated_at);
|
||||
|
||||
$this->request->shouldReceive('getRequestUri')->withNoArgs()->andReturn('/');
|
||||
$this->request->shouldReceive('route->getName')->withNoArgs()->andReturn(null);
|
||||
$this->request->shouldReceive('isJson')->withNoArgs()->andReturn(true);
|
||||
|
||||
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
|
||||
}
|
||||
|
||||
public function testAllRequirement__userWithTotp_2fa()
|
||||
{
|
||||
// Disable the 2FA requirement
|
||||
config()->set('pterodactyl.auth.2fa_required', RequireTwoFactorAuthentication::LEVEL_ALL);
|
||||
|
||||
$user = $this->generateRequestUserModel(['use_totp' => true]);
|
||||
|
||||
$this->assertTrue($user->use_totp);
|
||||
$this->assertEmpty($user->totp_secret);
|
||||
$this->assertEmpty($user->totp_authenticated_at);
|
||||
|
||||
$this->request->shouldReceive('getRequestUri')->withNoArgs()->andReturn('/');
|
||||
$this->request->shouldReceive('route->getName')->withNoArgs()->andReturn(null);
|
||||
$this->request->shouldReceive('isJson')->withNoArgs()->andReturn(true);
|
||||
|
||||
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
|
||||
}
|
||||
|
||||
public function testAllRequirement__ruserWithWebauthn_2fa()
|
||||
{
|
||||
// Disable the 2FA requirement
|
||||
config()->set('pterodactyl.auth.2fa_required', RequireTwoFactorAuthentication::LEVEL_ALL);
|
||||
|
||||
/** @var \Pterodactyl\Models\User $user */
|
||||
$user = User::factory()
|
||||
->has(WebauthnKey::factory()->count(1))
|
||||
->create(['use_totp' => false]);
|
||||
$this->setRequestUserModel($user);
|
||||
|
||||
$this->assertFalse($user->use_totp);
|
||||
$this->assertEmpty($user->totp_secret);
|
||||
$this->assertEmpty($user->totp_authenticated_at);
|
||||
$this->assertNotEmpty($user->webauthnKeys);
|
||||
|
||||
$this->request->shouldReceive('getRequestUri')->withNoArgs()->andReturn('/');
|
||||
$this->request->shouldReceive('route->getName')->withNoArgs()->andReturn(null);
|
||||
$this->request->shouldReceive('isJson')->withNoArgs()->andReturn(true);
|
||||
|
||||
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
|
||||
}
|
||||
|
||||
public function testAllRequirement__guestUser()
|
||||
{
|
||||
// Disable the 2FA requirement
|
||||
config()->set('pterodactyl.auth.2fa_required', RequireTwoFactorAuthentication::LEVEL_ALL);
|
||||
|
||||
$this->setRequestUserModel();
|
||||
|
||||
$this->request->shouldReceive('getRequestUri')->withNoArgs()->andReturn('/auth/login');
|
||||
$this->request->shouldReceive('route->getName')->withNoArgs()->andReturn('auth.login');
|
||||
$this->request->shouldReceive('isJson')->withNoArgs()->andReturn(true);
|
||||
|
||||
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
|
||||
}
|
||||
|
||||
public function testAdminRequirement__userWithout_2fa()
|
||||
{
|
||||
// Disable the 2FA requirement
|
||||
config()->set('pterodactyl.auth.2fa_required', RequireTwoFactorAuthentication::LEVEL_ADMIN);
|
||||
|
||||
$user = $this->generateRequestUserModel(['use_totp' => false]);
|
||||
|
||||
$this->assertFalse($user->use_totp);
|
||||
$this->assertEmpty($user->totp_secret);
|
||||
$this->assertEmpty($user->totp_authenticated_at);
|
||||
$this->assertFalse($user->root_admin);
|
||||
|
||||
$this->request->shouldReceive('getRequestUri')->withNoArgs()->andReturn('/');
|
||||
$this->request->shouldReceive('route->getName')->withNoArgs()->andReturn(null);
|
||||
$this->request->shouldReceive('isJson')->withNoArgs()->andReturn(true);
|
||||
|
||||
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
|
||||
}
|
||||
|
||||
public function testAdminRequirement__adminUserWithout_2fa()
|
||||
{
|
||||
$this->expectException(TwoFactorAuthRequiredException::class);
|
||||
|
||||
// Disable the 2FA requirement
|
||||
config()->set('pterodactyl.auth.2fa_required', RequireTwoFactorAuthentication::LEVEL_ADMIN);
|
||||
|
||||
$user = $this->generateRequestUserModel(['use_totp' => false, 'root_admin' => true]);
|
||||
|
||||
$this->assertFalse($user->use_totp);
|
||||
$this->assertEmpty($user->totp_secret);
|
||||
$this->assertEmpty($user->totp_authenticated_at);
|
||||
$this->assertTrue($user->root_admin);
|
||||
|
||||
$this->request->shouldReceive('getRequestUri')->withNoArgs()->andReturn('/');
|
||||
$this->request->shouldReceive('route->getName')->withNoArgs()->andReturn(null);
|
||||
$this->request->shouldReceive('isJson')->withNoArgs()->andReturn(true);
|
||||
|
||||
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
|
||||
}
|
||||
|
||||
public function testAdminRequirement__userWithTotp_2fa()
|
||||
{
|
||||
// Disable the 2FA requirement
|
||||
config()->set('pterodactyl.auth.2fa_required', RequireTwoFactorAuthentication::LEVEL_ADMIN);
|
||||
|
||||
$user = $this->generateRequestUserModel(['use_totp' => true]);
|
||||
|
||||
$this->assertTrue($user->use_totp);
|
||||
$this->assertEmpty($user->totp_secret);
|
||||
$this->assertEmpty($user->totp_authenticated_at);
|
||||
$this->assertFalse($user->root_admin);
|
||||
|
||||
$this->request->shouldReceive('getRequestUri')->withNoArgs()->andReturn('/');
|
||||
$this->request->shouldReceive('route->getName')->withNoArgs()->andReturn(null);
|
||||
$this->request->shouldReceive('isJson')->withNoArgs()->andReturn(true);
|
||||
|
||||
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
|
||||
}
|
||||
|
||||
public function testAdminRequirement__adminUserWithTotp_2fa()
|
||||
{
|
||||
// Disable the 2FA requirement
|
||||
config()->set('pterodactyl.auth.2fa_required', RequireTwoFactorAuthentication::LEVEL_ADMIN);
|
||||
|
||||
$user = $this->generateRequestUserModel(['use_totp' => true, 'root_admin' => true]);
|
||||
|
||||
$this->assertTrue($user->use_totp);
|
||||
$this->assertEmpty($user->totp_secret);
|
||||
$this->assertEmpty($user->totp_authenticated_at);
|
||||
$this->assertTrue($user->root_admin);
|
||||
|
||||
$this->request->shouldReceive('getRequestUri')->withNoArgs()->andReturn('/');
|
||||
$this->request->shouldReceive('route->getName')->withNoArgs()->andReturn(null);
|
||||
$this->request->shouldReceive('isJson')->withNoArgs()->andReturn(true);
|
||||
|
||||
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
|
||||
}
|
||||
|
||||
public function testAdminRequirement__userWithWebauthn_2fa()
|
||||
{
|
||||
// Disable the 2FA requirement
|
||||
config()->set('pterodactyl.auth.2fa_required', RequireTwoFactorAuthentication::LEVEL_ADMIN);
|
||||
|
||||
/** @var \Pterodactyl\Models\User $user */
|
||||
$user = User::factory()->has(WebauthnKey::factory()->count(1))->create(['use_totp' => false]);
|
||||
$this->setRequestUserModel($user);
|
||||
|
||||
$this->assertFalse($user->use_totp);
|
||||
$this->assertEmpty($user->totp_secret);
|
||||
$this->assertEmpty($user->totp_authenticated_at);
|
||||
$this->assertFalse($user->root_admin);
|
||||
$this->assertNotEmpty($user->webauthnKeys);
|
||||
|
||||
$this->request->shouldReceive('getRequestUri')->withNoArgs()->andReturn('/');
|
||||
$this->request->shouldReceive('route->getName')->withNoArgs()->andReturn(null);
|
||||
$this->request->shouldReceive('isJson')->withNoArgs()->andReturn(true);
|
||||
|
||||
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
|
||||
}
|
||||
|
||||
public function testAdminRequirement__adminUserWithWebauthn_2fa()
|
||||
{
|
||||
// Disable the 2FA requirement
|
||||
config()->set('pterodactyl.auth.2fa_required', RequireTwoFactorAuthentication::LEVEL_ADMIN);
|
||||
|
||||
/** @var \Pterodactyl\Models\User $user */
|
||||
$user = User::factory()
|
||||
->has(WebauthnKey::factory()->count(1))
|
||||
->create(['use_totp' => false, 'root_admin' => true]);
|
||||
$this->setRequestUserModel($user);
|
||||
|
||||
$this->assertFalse($user->use_totp);
|
||||
$this->assertEmpty($user->totp_secret);
|
||||
$this->assertEmpty($user->totp_authenticated_at);
|
||||
$this->assertTrue($user->root_admin);
|
||||
$this->assertNotEmpty($user->webauthnKeys);
|
||||
|
||||
$this->request->shouldReceive('getRequestUri')->withNoArgs()->andReturn('/');
|
||||
$this->request->shouldReceive('route->getName')->withNoArgs()->andReturn(null);
|
||||
$this->request->shouldReceive('isJson')->withNoArgs()->andReturn(true);
|
||||
|
||||
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
|
||||
}
|
||||
|
||||
public function testAdminRequirement__guestUser()
|
||||
{
|
||||
// Disable the 2FA requirement
|
||||
config()->set('pterodactyl.auth.2fa_required', RequireTwoFactorAuthentication::LEVEL_ADMIN);
|
||||
|
||||
$this->setRequestUserModel();
|
||||
|
||||
$this->request->shouldReceive('getRequestUri')->withNoArgs()->andReturn('/auth/login');
|
||||
$this->request->shouldReceive('route->getName')->withNoArgs()->andReturn('auth.login');
|
||||
$this->request->shouldReceive('isJson')->withNoArgs()->andReturn(true);
|
||||
|
||||
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
|
||||
}
|
||||
|
||||
private function getMiddleware(): RequireTwoFactorAuthentication
|
||||
{
|
||||
return new RequireTwoFactorAuthentication($this->alerts);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue