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:
parent
11c4f3f6f2
commit
e3df0738da
20 changed files with 249 additions and 234 deletions
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
{
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
{
|
|
@ -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
|
||||
{
|
Loading…
Add table
Add a link
Reference in a new issue