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
40
app/Http/Middleware/Api/Admin/AuthenticateIPAccess.php
Normal file
40
app/Http/Middleware/Api/Admin/AuthenticateIPAccess.php
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Middleware\Api\Admin;
|
||||
|
||||
use Closure;
|
||||
use IPTools\IP;
|
||||
use IPTools\Range;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
|
||||
class AuthenticateIPAccess
|
||||
{
|
||||
/**
|
||||
* Determine if a request IP has permission to access the API.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*
|
||||
* @throws \Exception
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
$model = $request->attributes->get('api_key');
|
||||
|
||||
if (is_null($model->allowed_ips) || empty($model->allowed_ips)) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
$find = new IP($request->ip());
|
||||
foreach ($model->allowed_ips as $ip) {
|
||||
if (Range::parse($ip)->contains($find)) {
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
|
||||
throw new AccessDeniedHttpException('This IP address does not have permission to access the API using these credentials.');
|
||||
}
|
||||
}
|
82
app/Http/Middleware/Api/Admin/AuthenticateKey.php
Normal file
82
app/Http/Middleware/Api/Admin/AuthenticateKey.php
Normal file
|
@ -0,0 +1,82 @@
|
|||
<?php
|
||||
|
||||
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;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
|
||||
class AuthenticateKey
|
||||
{
|
||||
/**
|
||||
* @var \Illuminate\Auth\AuthManager
|
||||
*/
|
||||
private $auth;
|
||||
|
||||
/**
|
||||
* @var \Illuminate\Contracts\Encryption\Encrypter
|
||||
*/
|
||||
private $encrypter;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* AuthenticateKey constructor.
|
||||
*
|
||||
* @param \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface $repository
|
||||
* @param \Illuminate\Auth\AuthManager $auth
|
||||
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
|
||||
*/
|
||||
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 exists in the database.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
if (is_null($request->bearerToken())) {
|
||||
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([['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);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
52
app/Http/Middleware/Api/Admin/SetSessionDriver.php
Normal file
52
app/Http/Middleware/Api/Admin/SetSessionDriver.php
Normal file
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Middleware\Api\Admin;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Barryvdh\Debugbar\LaravelDebugbar;
|
||||
use Illuminate\Contracts\Foundation\Application;
|
||||
use Illuminate\Contracts\Config\Repository as ConfigRepository;
|
||||
|
||||
class SetSessionDriver
|
||||
{
|
||||
/**
|
||||
* @var \Illuminate\Contracts\Foundation\Application
|
||||
*/
|
||||
private $app;
|
||||
|
||||
/**
|
||||
* @var \Illuminate\Contracts\Config\Repository
|
||||
*/
|
||||
private $config;
|
||||
|
||||
/**
|
||||
* SetSessionDriver constructor.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Foundation\Application $app
|
||||
* @param \Illuminate\Contracts\Config\Repository $config
|
||||
*/
|
||||
public function __construct(Application $app, ConfigRepository $config)
|
||||
{
|
||||
$this->app = $app;
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the session for API calls to only last for the one request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
if ($this->app->environment() !== 'production') {
|
||||
$this->app->make(LaravelDebugbar::class)->disable();
|
||||
}
|
||||
|
||||
$this->config->set('session.driver', 'array');
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
69
app/Http/Middleware/Api/Daemon/DaemonAuthenticate.php
Normal file
69
app/Http/Middleware/Api/Daemon/DaemonAuthenticate.php
Normal file
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Middleware\Api\Daemon;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
|
||||
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
|
||||
class DaemonAuthenticate
|
||||
{
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* Daemon routes that this middleware should be skipped on.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $except = [
|
||||
'daemon.configuration',
|
||||
];
|
||||
|
||||
/**
|
||||
* DaemonAuthenticate constructor.
|
||||
*
|
||||
* @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository
|
||||
*/
|
||||
public function __construct(NodeRepositoryInterface $repository)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a request from the daemon can be properly attributed back to a single node instance.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
if (in_array($request->route()->getName(), $this->except)) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
$token = $request->bearerToken();
|
||||
|
||||
if (is_null($token)) {
|
||||
throw new HttpException(401, null, null, ['WWW-Authenticate' => 'Bearer']);
|
||||
}
|
||||
|
||||
try {
|
||||
$node = $this->repository->findFirstWhere([['daemonSecret', '=', $token]]);
|
||||
} catch (RecordNotFoundException $exception) {
|
||||
throw new AccessDeniedHttpException;
|
||||
}
|
||||
|
||||
$request->attributes->set('node', $node);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue