tests(unit): add RequireTwoFactorAuthenticationTest

This commit is contained in:
Matthew Penner 2021-07-18 11:28:14 -06:00
parent 790f109e66
commit 64110d84af
9 changed files with 373 additions and 7 deletions

1
.gitignore vendored
View file

@ -47,6 +47,7 @@ yarn-error.log
# PHP # PHP
/vendor /vendor
.php_cs.cache .php_cs.cache
.phpunit.result.cache
#-------------------# #-------------------#
# Operating Systems # # Operating Systems #

View file

@ -6,7 +6,7 @@ use Exception;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use LaravelWebauthn\Facades\Webauthn; use LaravelWebauthn\Facades\Webauthn;
use LaravelWebauthn\Models\WebauthnKey; use Pterodactyl\Models\WebauthnKey;
use Webauthn\PublicKeyCredentialCreationOptions; use Webauthn\PublicKeyCredentialCreationOptions;
use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Database\Eloquent\ModelNotFoundException;
use Pterodactyl\Transformers\Api\Client\WebauthnKeyTransformer; use Pterodactyl\Transformers\Api\Client\WebauthnKeyTransformer;

View file

@ -5,7 +5,6 @@ namespace Pterodactyl\Models;
use Pterodactyl\Rules\Username; use Pterodactyl\Rules\Username;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Auth\Authenticatable; use Illuminate\Auth\Authenticatable;
use LaravelWebauthn\Models\WebauthnKey;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Auth\Passwords\CanResetPassword; 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\Server[]|\Illuminate\Database\Eloquent\Collection $servers
* @property \Pterodactyl\Models\UserSSHKey|\Illuminate\Database\Eloquent\Collection $sshKeys * @property \Pterodactyl\Models\UserSSHKey|\Illuminate\Database\Eloquent\Collection $sshKeys
* @property \Pterodactyl\Models\RecoveryToken[]|\Illuminate\Database\Eloquent\Collection $recoveryTokens * @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 class User extends Model implements
AuthenticatableContract, AuthenticatableContract,

View 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);
}
}

View file

@ -2,7 +2,7 @@
namespace Pterodactyl\Transformers\Api\Client; namespace Pterodactyl\Transformers\Api\Client;
use LaravelWebauthn\Models\WebauthnKey; use Pterodactyl\Models\WebauthnKey;
class WebauthnKeyTransformer extends BaseClientTransformer class WebauthnKeyTransformer extends BaseClientTransformer
{ {

View file

@ -77,8 +77,15 @@ return [
'charset' => 'utf8mb4', 'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci', 'collation' => 'utf8mb4_unicode_ci',
'prefix' => '', 'prefix' => '',
'strict' => false, 'strict' => env('TESTING_DB_STRICT_MODE', false),
'timezone' => env('DB_TIMEZONE', Time::getMySQLTimezoneOffset(env('APP_TIMEZONE', 'UTC'))), '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),
]) : [],
], ],
], ],

View 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,
];
}
}

View file

@ -43,6 +43,7 @@ trait RequestMockHelpers
*/ */
public function generateRequestUserModel(array $args = []): User public function generateRequestUserModel(array $args = []): User
{ {
/** @var \Pterodactyl\Models\User $user */
$user = User::factory()->make($args); $user = User::factory()->make($args);
$this->setRequestUserModel($user); $this->setRequestUserModel($user);
@ -70,8 +71,9 @@ trait RequestMockHelpers
/** /**
* Set the active request object to be an instance of a mocked request. * 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); $this->request = m::mock($this->requestMockClass);
if (!$this->request instanceof Request) { if (!$this->request instanceof Request) {
throw new InvalidArgumentException('Request mock class must be an instance of ' . Request::class . ' when mocked.'); throw new InvalidArgumentException('Request mock class must be an instance of ' . Request::class . ' when mocked.');

View file

@ -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);
}
}