1
1
mirror of https://github.com/pterodactyl/panel.git synced 2024-11-22 09:02:28 +01:00

Change the way API keys are stored and validated; clarify API namespacing

Previously, a single key was used to access the API, this has not changed in terms of what the user sees. However, API keys now use an identifier and token internally. The identifier is the first 16 characters of the key, and the token is the remaining 32. The token is stored encrypted at rest in the database and the identifier is used by the API middleware to grab that record and make a timing attack safe comparison.
This commit is contained in:
Dane Everitt 2018-01-13 16:06:19 -06:00
parent 11c4f3f6f2
commit e3df0738da
No known key found for this signature in database
GPG Key ID: EEA66103B3D71F53
20 changed files with 249 additions and 234 deletions

View File

@ -49,8 +49,6 @@ class APIController extends Controller
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\View\View
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function index(Request $request)
{

View File

@ -14,19 +14,19 @@ use Pterodactyl\Http\Middleware\AdminAuthenticate;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Pterodactyl\Http\Middleware\LanguageMiddleware;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
use Pterodactyl\Http\Middleware\API\AuthenticateKey;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Pterodactyl\Http\Middleware\AccessingValidServer;
use Pterodactyl\Http\Middleware\API\SetSessionDriver;
use Illuminate\View\Middleware\ShareErrorsFromSession;
use Pterodactyl\Http\Middleware\RedirectIfAuthenticated;
use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth;
use Pterodactyl\Http\Middleware\API\AuthenticateIPAccess;
use Pterodactyl\Http\Middleware\Daemon\DaemonAuthenticate;
use Pterodactyl\Http\Middleware\Api\Admin\AuthenticateKey;
use Illuminate\Foundation\Http\Middleware\ValidatePostSize;
use Pterodactyl\Http\Middleware\Api\Admin\SetSessionDriver;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Pterodactyl\Http\Middleware\Server\AuthenticateAsSubuser;
use Pterodactyl\Http\Middleware\Api\Daemon\DaemonAuthenticate;
use Pterodactyl\Http\Middleware\Server\SubuserBelongsToServer;
use Pterodactyl\Http\Middleware\Api\Admin\AuthenticateIPAccess;
use Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication;
use Pterodactyl\Http\Middleware\Server\DatabaseBelongsToServer;
use Pterodactyl\Http\Middleware\Server\ScheduleBelongsToServer;

View File

@ -1,6 +1,6 @@
<?php
namespace Pterodactyl\Http\Middleware\API;
namespace Pterodactyl\Http\Middleware\Api\Admin;
use Closure;
use IPTools\IP;

View File

@ -1,10 +1,12 @@
<?php
namespace Pterodactyl\Http\Middleware\API;
namespace Pterodactyl\Http\Middleware\Api\Admin;
use Closure;
use Illuminate\Http\Request;
use Pterodactyl\Models\APIKey;
use Illuminate\Auth\AuthManager;
use Illuminate\Contracts\Encryption\Encrypter;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface;
@ -17,6 +19,11 @@ class AuthenticateKey
*/
private $auth;
/**
* @var \Illuminate\Contracts\Encryption\Encrypter
*/
private $encrypter;
/**
* @var \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface
*/
@ -27,19 +34,18 @@ class AuthenticateKey
*
* @param \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface $repository
* @param \Illuminate\Auth\AuthManager $auth
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
*/
public function __construct(
ApiKeyRepositoryInterface $repository,
AuthManager $auth
) {
public function __construct(ApiKeyRepositoryInterface $repository, AuthManager $auth, Encrypter $encrypter)
{
$this->auth = $auth;
$this->encrypter = $encrypter;
$this->repository = $repository;
}
/**
* Handle an API request by verifying that the provided API key
* is in a valid format, and the route being accessed is allowed
* for the given key.
* is in a valid format and exists in the database.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
@ -54,12 +60,20 @@ class AuthenticateKey
throw new HttpException(401, null, null, ['WWW-Authenticate' => 'Bearer']);
}
$raw = $request->bearerToken();
$identifier = substr($raw, 0, APIKey::IDENTIFIER_LENGTH);
$token = substr($raw, APIKey::IDENTIFIER_LENGTH);
try {
$model = $this->repository->findFirstWhere([['token', '=', $request->bearerToken()]]);
$model = $this->repository->findFirstWhere([['identifier', '=', $identifier]]);
} catch (RecordNotFoundException $exception) {
throw new AccessDeniedHttpException;
}
if (! hash_equals($this->encrypter->decrypt($model->token), $token)) {
throw new AccessDeniedHttpException;
}
$this->auth->guard()->loginUsingId($model->user_id);
$request->attributes->set('api_key', $model);

View File

@ -1,6 +1,6 @@
<?php
namespace Pterodactyl\Http\Middleware\API;
namespace Pterodactyl\Http\Middleware\Api\Admin;
use Closure;
use Illuminate\Http\Request;

View File

@ -1,28 +1,6 @@
<?php
/*
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
namespace Pterodactyl\Http\Middleware\Daemon;
namespace Pterodactyl\Http\Middleware\Api\Daemon;
use Closure;
use Illuminate\Http\Request;

View File

@ -6,6 +6,7 @@ use Sofa\Eloquence\Eloquence;
use Sofa\Eloquence\Validable;
use Illuminate\Database\Eloquent\Model;
use Pterodactyl\Services\Acl\Api\AdminAcl;
use Illuminate\Contracts\Encryption\Encrypter;
use Sofa\Eloquence\Contracts\CleansAttributes;
use Sofa\Eloquence\Contracts\Validable as ValidableContract;
@ -13,6 +14,15 @@ class APIKey extends Model implements CleansAttributes, ValidableContract
{
use Eloquence, Validable;
/**
* The length of API key identifiers.
*/
const IDENTIFIER_LENGTH = 16;
/**
* The length of the actual API key that is encrypted and stored
* in the database.
*/
const KEY_LENGTH = 32;
/**
@ -47,18 +57,27 @@ class APIKey extends Model implements CleansAttributes, ValidableContract
* @var array
*/
protected $fillable = [
'identifier',
'token',
'allowed_ips',
'memo',
'expires_at',
];
/**
* Fields that should not be included when calling toArray() or toJson()
* on this model.
*
* @var array
*/
protected $hidden = ['token'];
/**
* Rules defining what fields must be passed when making a model.
*
* @var array
*/
protected static $applicationRules = [
'identifier' => 'required',
'memo' => 'required',
'user_id' => 'required',
'token' => 'required',
@ -71,10 +90,11 @@ class APIKey extends Model implements CleansAttributes, ValidableContract
*/
protected static $dataIntegrityRules = [
'user_id' => 'exists:users,id',
'token' => 'string|size:32',
'identifier' => 'string|size:16|unique:api_keys,identifier',
'token' => 'string',
'memo' => 'nullable|string|max:500',
'allowed_ips' => 'nullable|json',
'expires_at' => 'nullable|datetime',
'last_used_at' => 'nullable|date',
'r_' . AdminAcl::RESOURCE_USERS => 'integer|min:0|max:3',
'r_' . AdminAcl::RESOURCE_ALLOCATIONS => 'integer|min:0|max:3',
'r_' . AdminAcl::RESOURCE_DATABASES => 'integer|min:0|max:3',
@ -92,9 +112,19 @@ class APIKey extends Model implements CleansAttributes, ValidableContract
protected $dates = [
self::CREATED_AT,
self::UPDATED_AT,
'expires_at',
'last_used_at',
];
/**
* Return a decrypted version of the token.
*
* @return string
*/
public function getDecryptedTokenAttribute()
{
return app()->make(Encrypter::class)->decrypt($this->token);
}
/**
* Gets the permissions associated with a key.
*

View File

@ -0,0 +1,19 @@
<?php
namespace Pterodactyl\Providers;
use Illuminate\Support\ServiceProvider;
class BladeServiceProvider extends ServiceProvider
{
/**
* Perform post-registration booting of services.
*/
public function boot()
{
$this->app->make('blade.compiler')
->directive('datetimeHuman', function ($expression) {
return "<?php echo \Cake\Chronos\Chronos::createFromFormat(\Cake\Chronos\Chronos::DEFAULT_TO_STRING_FORMAT, $expression)->setTimezone(config('app.timezone'))->toDateTimeString(); ?>";
});
}
}

View File

@ -4,6 +4,7 @@ namespace Pterodactyl\Services\Api;
use Pterodactyl\Models\APIKey;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface;
class KeyCreationService
@ -14,9 +15,9 @@ class KeyCreationService
private $connection;
/**
* @var \Pterodactyl\Services\Api\PermissionService
* @var \Illuminate\Contracts\Encryption\Encrypter
*/
private $permissionService;
private $encrypter;
/**
* @var \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface
@ -28,67 +29,36 @@ class KeyCreationService
*
* @param \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface $repository
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Pterodactyl\Services\Api\PermissionService $permissionService
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
*/
public function __construct(
ApiKeyRepositoryInterface $repository,
ConnectionInterface $connection,
PermissionService $permissionService
Encrypter $encrypter
) {
$this->repository = $repository;
$this->connection = $connection;
$this->permissionService = $permissionService;
$this->encrypter = $encrypter;
}
/**
* Create a new API Key on the system with the given permissions.
* Create a new API key for the Panel using the permissions passed in the data request.
* This will automatically generate an identifer and an encrypted token that are
* stored in the database.
*
* @param array $data
* @param array $permissions
* @param array $administrative
* @return \Pterodactyl\Models\APIKey
*
* @throws \Exception
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
*/
public function handle(array $data, array $permissions, array $administrative = []): APIKey
public function handle(array $data): APIKey
{
$token = str_random(APIKey::KEY_LENGTH);
$data = array_merge($data, ['token' => $token]);
$data = array_merge($data, [
'identifer' => str_random(APIKey::IDENTIFIER_LENGTH),
'token' => $this->encrypter->encrypt(str_random(APIKey::KEY_LENGTH)),
]);
$this->connection->beginTransaction();
$instance = $this->repository->create($data, true, true);
$nodes = $this->permissionService->getPermissions();
foreach ($permissions as $permission) {
@list($block, $search) = explode('-', $permission, 2);
if (
(empty($block) || empty($search)) ||
! array_key_exists($block, $nodes['_user']) ||
! in_array($search, $nodes['_user'][$block])
) {
continue;
}
$this->permissionService->create($instance->id, sprintf('user.%s', $permission));
}
foreach ($administrative as $permission) {
@list($block, $search) = explode('-', $permission, 2);
if (
(empty($block) || empty($search)) ||
! array_key_exists($block, $nodes) ||
! in_array($search, $nodes[$block])
) {
continue;
}
$this->permissionService->create($instance->id, $permission);
}
$this->connection->commit();
return $instance;
}

View File

@ -175,6 +175,7 @@ return [
*/
Pterodactyl\Providers\AppServiceProvider::class,
Pterodactyl\Providers\AuthServiceProvider::class,
Pterodactyl\Providers\BladeServiceProvider::class,
Pterodactyl\Providers\EventServiceProvider::class,
Pterodactyl\Providers\HashidsServiceProvider::class,
Pterodactyl\Providers\RouteServiceProvider::class,

View File

@ -227,7 +227,8 @@ $factory->define(Pterodactyl\Models\APIKey::class, function (Faker $faker) {
return [
'id' => $faker->unique()->randomNumber(),
'user_id' => $faker->randomNumber(),
'token' => str_random(Pterodactyl\Models\APIKey::KEY_LENGTH),
'identifier' => str_random(Pterodactyl\Models\APIKey::IDENTIFIER_LENGTH),
'token' => 'encrypted_string',
'memo' => 'Test Function Key',
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
'updated_at' => \Carbon\Carbon::now()->toDateTimeString(),

View File

@ -0,0 +1,62 @@
<?php
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class SetupTableForKeyEncryption extends Migration
{
/**
* Run the migrations.
*
* @return void
* @throws \Exception
* @throws \Throwable
*/
public function up()
{
Schema::table('api_keys', function (Blueprint $table) {
$table->char('identifier', 16)->unique()->after('user_id');
$table->dropUnique(['token']);
});
Schema::table('api_keys', function (Blueprint $table) {
$table->text('token')->change();
});
DB::transaction(function () {
foreach (DB::table('api_keys')->cursor() as $key) {
DB::table('api_keys')->where('id', $key->id)->update([
'identifier' => str_random(16),
'token' => Crypt::encrypt($key->token),
]);
}
});
}
/**
* Reverse the migrations.
*
* @return void
* @throws \Exception
* @throws \Throwable
*/
public function down()
{
/** @var \Pterodactyl\Models\APIKey $key */
DB::transaction(function () {
foreach (DB::table('api_keys')->cursor() as $key) {
DB::table('api_keys')->where('id', $key->id)->update([
'token' => Crypt::decrypt($key->token),
]);
}
});
Schema::table('api_keys', function (Blueprint $table) {
$table->dropColumn('identifier');
$table->string('token', 32)->unique()->change();
});
}
}

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddLastUsedAtColumn extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('api_keys', function (Blueprint $table) {
$table->timestamp('last_used_at')->after('memo')->nullable();
$table->dropColumn('expires_at');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('api_keys', function (Blueprint $table) {
$table->timestamp('expires_at')->after('memo')->nullable();
$table->dropColumn('last_used_at');
});
}
}

View File

@ -83,4 +83,5 @@ return [
'fri' => 'Friday',
'sat' => 'Saturday',
],
'last_used' => 'Last Used',
];

View File

@ -31,26 +31,26 @@
<table class="table table-hover">
<tbody>
<tr>
<th>@lang('strings.public_key')</th>
<th>@lang('strings.memo')</th>
<th class="text-center hidden-sm hidden-xs">@lang('strings.created')</th>
<th class="text-center hidden-sm hidden-xs">@lang('strings.expires')</th>
<th>@lang('strings.public_key')</th>
<th class="text-right hidden-sm hidden-xs">@lang('strings.last_used')</th>
<th class="text-right hidden-sm hidden-xs">@lang('strings.created')</th>
<th></th>
</tr>
@foreach ($keys as $key)
<tr>
<td><code>{{ $key->token }}</code></td>
<td>{{ $key->memo }}</td>
<td class="text-center hidden-sm hidden-xs">
{{ (new Carbon($key->created_at))->toDayDateTimeString() }}
</td>
<td class="text-center hidden-sm hidden-xs">
@if(is_null($key->expires_at))
<span class="label label-default">@lang('strings.never')</span>
<td><code>{{ $key->identifier . decrypt($key->token) }}</code></td>
<td class="text-right hidden-sm hidden-xs">
@if(!is_null($key->last_used_at))
@datetimeHuman($key->last_used_at)
@else
{{ (new Carbon($key->expires_at))->toDayDateTimeString() }}
&mdash;
@endif
</td>
<td class="text-right hidden-sm hidden-xs">
@datetimeHuman($key->created_at)
</td>
<td class="text-center">
<a href="#delete" class="text-danger" data-action="delete" data-attr="{{ $key->token }}"><i class="fa fa-trash"></i></a>
</td>

View File

@ -1,109 +0,0 @@
<?php
namespace Tests\Unit\Http\Middleware\API;
use Mockery as m;
use Pterodactyl\Models\User;
use Pterodactyl\Models\APIKey;
use Pterodactyl\Models\APIPermission;
use Tests\Unit\Http\Middleware\MiddlewareTestCase;
use Pterodactyl\Http\Middleware\API\HasPermissionToResource;
use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface;
class HasPermissionToResourceTest extends MiddlewareTestCase
{
/**
* @var \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface|\Mockery\Mock
*/
private $repository;
/**
* Setup tests.
*/
public function setUp()
{
parent::setUp();
$this->repository = m::mock(ApiKeyRepositoryInterface::class);
}
/**
* Test that a non-admin user cannot access admin level routes.
*
* @expectedException \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
public function testNonAdminAccessingAdminLevel()
{
$model = factory(APIKey::class)->make();
$this->setRequestAttribute('api_key', $model);
$this->setRequestUser(factory(User::class)->make(['root_admin' => false]));
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
}
/**
* Test non-admin accessing non-admin route.
*/
public function testAccessingAllowedRoute()
{
$model = factory(APIKey::class)->make();
$model->setRelation('permissions', collect([
factory(APIPermission::class)->make(['permission' => 'user.test-route']),
]));
$this->setRequestAttribute('api_key', $model);
$this->setRequestUser(factory(User::class)->make(['root_admin' => false]));
$this->request->shouldReceive('route->getName')->withNoArgs()->once()->andReturn('api.user.test.route');
$this->repository->shouldReceive('loadPermissions')->with($model)->once()->andReturn($model);
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions(), 'user');
}
/**
* Test admin accessing administrative route.
*/
public function testAccessingAllowedAdminRoute()
{
$model = factory(APIKey::class)->make();
$model->setRelation('permissions', collect([
factory(APIPermission::class)->make(['permission' => 'test-route']),
]));
$this->setRequestAttribute('api_key', $model);
$this->setRequestUser(factory(User::class)->make(['root_admin' => true]));
$this->request->shouldReceive('route->getName')->withNoArgs()->once()->andReturn('api.admin.test.route');
$this->repository->shouldReceive('loadPermissions')->with($model)->once()->andReturn($model);
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
}
/**
* Test a user accessing a disallowed route.
*
* @expectedException \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
*/
public function testAccessingDisallowedRoute()
{
$model = factory(APIKey::class)->make();
$model->setRelation('permissions', collect([
factory(APIPermission::class)->make(['permission' => 'user.other-route']),
]));
$this->setRequestAttribute('api_key', $model);
$this->setRequestUser(factory(User::class)->make(['root_admin' => false]));
$this->request->shouldReceive('route->getName')->withNoArgs()->once()->andReturn('api.user.test.route');
$this->repository->shouldReceive('loadPermissions')->with($model)->once()->andReturn($model);
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions(), 'user');
}
/**
* Return an instance of the middleware with mocked dependencies for testing.
*
* @return \Pterodactyl\Http\Middleware\API\HasPermissionToResource
*/
private function getMiddleware(): HasPermissionToResource
{
return new HasPermissionToResource($this->repository);
}
}

View File

@ -1,21 +1,13 @@
<?php
namespace Tests\Unit\Http\Middleware\API;
namespace Tests\Unit\Http\Middleware\Api\Admin;
use Pterodactyl\Models\APIKey;
use Tests\Unit\Http\Middleware\MiddlewareTestCase;
use Pterodactyl\Http\Middleware\API\AuthenticateIPAccess;
use Pterodactyl\Http\Middleware\Api\Admin\AuthenticateIPAccess;
class AuthenticateIPAccessTest extends MiddlewareTestCase
{
/**
* Setup tests.
*/
public function setUp()
{
parent::setUp();
}
/**
* Test middleware when there are no IP restrictions.
*/
@ -73,7 +65,7 @@ class AuthenticateIPAccessTest extends MiddlewareTestCase
/**
* Return an instance of the middleware to be used when testing.
*
* @return \Pterodactyl\Http\Middleware\API\AuthenticateIPAccess
* @return \Pterodactyl\Http\Middleware\Api\Admin\AuthenticateIPAccess
*/
private function getMiddleware(): AuthenticateIPAccess
{

View File

@ -1,13 +1,14 @@
<?php
namespace Tests\Unit\Http\Middleware\API;
namespace Tests\Unit\Http\Middleware\Api;
use Mockery as m;
use Pterodactyl\Models\APIKey;
use Illuminate\Auth\AuthManager;
use Illuminate\Contracts\Encryption\Encrypter;
use Tests\Unit\Http\Middleware\MiddlewareTestCase;
use Pterodactyl\Http\Middleware\API\AuthenticateKey;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Pterodactyl\Http\Middleware\Api\Admin\AuthenticateKey;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface;
@ -18,6 +19,11 @@ class AuthenticateKeyTest extends MiddlewareTestCase
*/
private $auth;
/**
* @var \Illuminate\Contracts\Encryption\Encrypter|\Mockery\Mock
*/
private $encrypter;
/**
* @var \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface|\Mockery\Mock
*/
@ -31,6 +37,7 @@ class AuthenticateKeyTest extends MiddlewareTestCase
parent::setUp();
$this->auth = m::mock(AuthManager::class);
$this->encrypter = m::mock(Encrypter::class);
$this->repository = m::mock(ApiKeyRepositoryInterface::class);
}
@ -50,11 +57,11 @@ class AuthenticateKeyTest extends MiddlewareTestCase
}
/**
* Test that an invalid API token throws an exception.
* Test that an invalid API identifer throws an exception.
*
* @expectedException \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
*/
public function testInvalidTokenThrowsException()
public function testInvalidIdentifier()
{
$this->request->shouldReceive('bearerToken')->withNoArgs()->twice()->andReturn('abcd1234');
$this->repository->shouldReceive('findFirstWhere')->andThrow(new RecordNotFoundException);
@ -69,22 +76,39 @@ class AuthenticateKeyTest extends MiddlewareTestCase
{
$model = factory(APIKey::class)->make();
$this->request->shouldReceive('bearerToken')->withNoArgs()->twice()->andReturn($model->token);
$this->repository->shouldReceive('findFirstWhere')->with([['token', '=', $model->token]])->once()->andReturn($model);
$this->request->shouldReceive('bearerToken')->withNoArgs()->twice()->andReturn($model->identifier . 'decrypted');
$this->repository->shouldReceive('findFirstWhere')->with([['identifier', '=', $model->identifier]])->once()->andReturn($model);
$this->encrypter->shouldReceive('decrypt')->with($model->token)->once()->andReturn('decrypted');
$this->auth->shouldReceive('guard->loginUsingId')->with($model->user_id)->once()->andReturnNull();
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
$this->assertEquals($model, $this->request->attributes->get('api_key'));
}
/**
* Test that a valid token identifier with an invalid token attached to it
* triggers an exception.
*
* @expectedException \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
*/
public function testInvalidTokenForIdentifier()
{
$model = factory(APIKey::class)->make();
$this->request->shouldReceive('bearerToken')->withNoArgs()->twice()->andReturn($model->identifier . 'asdf');
$this->repository->shouldReceive('findFirstWhere')->with([['identifier', '=', $model->identifier]])->once()->andReturn($model);
$this->encrypter->shouldReceive('decrypt')->with($model->token)->once()->andReturn('decrypted');
$this->getMiddleware()->handle($this->request, $this->getClosureAssertions());
}
/**
* Return an instance of the middleware with mocked dependencies for testing.
*
* @return \Pterodactyl\Http\Middleware\API\AuthenticateKey
* @return \Pterodactyl\Http\Middleware\Api\Admin\AuthenticateKey
*/
private function getMiddleware(): AuthenticateKey
{
return new AuthenticateKey($this->repository, $this->auth);
return new AuthenticateKey($this->repository, $this->auth, $this->encrypter);
}
}

View File

@ -1,13 +1,13 @@
<?php
namespace Tests\Unit\Http\Middleware\API;
namespace Tests\Unit\Http\Middleware\Api\Admin;
use Mockery as m;
use Barryvdh\Debugbar\LaravelDebugbar;
use Illuminate\Contracts\Config\Repository;
use Illuminate\Contracts\Foundation\Application;
use Tests\Unit\Http\Middleware\MiddlewareTestCase;
use Pterodactyl\Http\Middleware\API\SetSessionDriver;
use Pterodactyl\Http\Middleware\Api\Admin\SetSessionDriver;
class SetSessionDriverTest extends MiddlewareTestCase
{
@ -60,7 +60,7 @@ class SetSessionDriverTest extends MiddlewareTestCase
/**
* Return an instance of the middleware with mocked dependencies for testing.
*
* @return \Pterodactyl\Http\Middleware\API\SetSessionDriver
* @return \Pterodactyl\Http\Middleware\Api\Admin\SetSessionDriver
*/
private function getMiddleware(): SetSessionDriver
{

View File

@ -1,14 +1,14 @@
<?php
namespace Tests\Unit\Http\Middleware\Daemon;
namespace Tests\Unit\Http\Middleware\Api\Daemon;
use Mockery as m;
use Pterodactyl\Models\Node;
use Tests\Unit\Http\Middleware\MiddlewareTestCase;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Pterodactyl\Http\Middleware\Daemon\DaemonAuthenticate;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
use Pterodactyl\Http\Middleware\Api\Daemon\DaemonAuthenticate;
class DaemonAuthenticateTest extends MiddlewareTestCase
{
@ -93,7 +93,7 @@ class DaemonAuthenticateTest extends MiddlewareTestCase
/**
* Return an instance of the middleware using mocked dependencies.
*
* @return \Pterodactyl\Http\Middleware\Daemon\DaemonAuthenticate
* @return \Pterodactyl\Http\Middleware\Api\Daemon\DaemonAuthenticate
*/
private function getMiddleware(): DaemonAuthenticate
{