Skip to content

Commit e3df073

Browse files
committed
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.
1 parent 11c4f3f commit e3df073

File tree

20 files changed

+249
-234
lines changed

20 files changed

+249
-234
lines changed

app/Http/Controllers/Base/APIController.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,6 @@ public function __construct(
4949
*
5050
* @param \Illuminate\Http\Request $request
5151
* @return \Illuminate\View\View
52-
*
53-
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
5452
*/
5553
public function index(Request $request)
5654
{

app/Http/Kernel.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,19 @@
1414
use Illuminate\Routing\Middleware\ThrottleRequests;
1515
use Pterodactyl\Http\Middleware\LanguageMiddleware;
1616
use Illuminate\Foundation\Http\Kernel as HttpKernel;
17-
use Pterodactyl\Http\Middleware\API\AuthenticateKey;
1817
use Illuminate\Routing\Middleware\SubstituteBindings;
1918
use Pterodactyl\Http\Middleware\AccessingValidServer;
20-
use Pterodactyl\Http\Middleware\API\SetSessionDriver;
2119
use Illuminate\View\Middleware\ShareErrorsFromSession;
2220
use Pterodactyl\Http\Middleware\RedirectIfAuthenticated;
2321
use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth;
24-
use Pterodactyl\Http\Middleware\API\AuthenticateIPAccess;
25-
use Pterodactyl\Http\Middleware\Daemon\DaemonAuthenticate;
22+
use Pterodactyl\Http\Middleware\Api\Admin\AuthenticateKey;
2623
use Illuminate\Foundation\Http\Middleware\ValidatePostSize;
24+
use Pterodactyl\Http\Middleware\Api\Admin\SetSessionDriver;
2725
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
2826
use Pterodactyl\Http\Middleware\Server\AuthenticateAsSubuser;
27+
use Pterodactyl\Http\Middleware\Api\Daemon\DaemonAuthenticate;
2928
use Pterodactyl\Http\Middleware\Server\SubuserBelongsToServer;
29+
use Pterodactyl\Http\Middleware\Api\Admin\AuthenticateIPAccess;
3030
use Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication;
3131
use Pterodactyl\Http\Middleware\Server\DatabaseBelongsToServer;
3232
use Pterodactyl\Http\Middleware\Server\ScheduleBelongsToServer;

app/Http/Middleware/API/AuthenticateIPAccess.php renamed to app/Http/Middleware/Api/Admin/AuthenticateIPAccess.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22

3-
namespace Pterodactyl\Http\Middleware\API;
3+
namespace Pterodactyl\Http\Middleware\Api\Admin;
44

55
use Closure;
66
use IPTools\IP;

app/Http/Middleware/API/AuthenticateKey.php renamed to app/Http/Middleware/Api/Admin/AuthenticateKey.php

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
<?php
22

3-
namespace Pterodactyl\Http\Middleware\API;
3+
namespace Pterodactyl\Http\Middleware\Api\Admin;
44

55
use Closure;
66
use Illuminate\Http\Request;
7+
use Pterodactyl\Models\APIKey;
78
use Illuminate\Auth\AuthManager;
9+
use Illuminate\Contracts\Encryption\Encrypter;
810
use Symfony\Component\HttpKernel\Exception\HttpException;
911
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
1012
use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface;
@@ -17,6 +19,11 @@ class AuthenticateKey
1719
*/
1820
private $auth;
1921

22+
/**
23+
* @var \Illuminate\Contracts\Encryption\Encrypter
24+
*/
25+
private $encrypter;
26+
2027
/**
2128
* @var \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface
2229
*/
@@ -27,19 +34,18 @@ class AuthenticateKey
2734
*
2835
* @param \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface $repository
2936
* @param \Illuminate\Auth\AuthManager $auth
37+
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
3038
*/
31-
public function __construct(
32-
ApiKeyRepositoryInterface $repository,
33-
AuthManager $auth
34-
) {
39+
public function __construct(ApiKeyRepositoryInterface $repository, AuthManager $auth, Encrypter $encrypter)
40+
{
3541
$this->auth = $auth;
42+
$this->encrypter = $encrypter;
3643
$this->repository = $repository;
3744
}
3845

3946
/**
4047
* Handle an API request by verifying that the provided API key
41-
* is in a valid format, and the route being accessed is allowed
42-
* for the given key.
48+
* is in a valid format and exists in the database.
4349
*
4450
* @param \Illuminate\Http\Request $request
4551
* @param \Closure $next
@@ -54,12 +60,20 @@ public function handle(Request $request, Closure $next)
5460
throw new HttpException(401, null, null, ['WWW-Authenticate' => 'Bearer']);
5561
}
5662

63+
$raw = $request->bearerToken();
64+
$identifier = substr($raw, 0, APIKey::IDENTIFIER_LENGTH);
65+
$token = substr($raw, APIKey::IDENTIFIER_LENGTH);
66+
5767
try {
58-
$model = $this->repository->findFirstWhere([['token', '=', $request->bearerToken()]]);
68+
$model = $this->repository->findFirstWhere([['identifier', '=', $identifier]]);
5969
} catch (RecordNotFoundException $exception) {
6070
throw new AccessDeniedHttpException;
6171
}
6272

73+
if (! hash_equals($this->encrypter->decrypt($model->token), $token)) {
74+
throw new AccessDeniedHttpException;
75+
}
76+
6377
$this->auth->guard()->loginUsingId($model->user_id);
6478
$request->attributes->set('api_key', $model);
6579

app/Http/Middleware/API/SetSessionDriver.php renamed to app/Http/Middleware/Api/Admin/SetSessionDriver.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
22

3-
namespace Pterodactyl\Http\Middleware\API;
3+
namespace Pterodactyl\Http\Middleware\Api\Admin;
44

55
use Closure;
66
use Illuminate\Http\Request;

app/Http/Middleware/Daemon/DaemonAuthenticate.php renamed to app/Http/Middleware/Api/Daemon/DaemonAuthenticate.php

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,6 @@
11
<?php
2-
/*
3-
* Pterodactyl - Panel
4-
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
5-
*
6-
* Permission is hereby granted, free of charge, to any person obtaining a copy
7-
* of this software and associated documentation files (the "Software"), to deal
8-
* in the Software without restriction, including without limitation the rights
9-
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10-
* copies of the Software, and to permit persons to whom the Software is
11-
* furnished to do so, subject to the following conditions:
12-
*
13-
* The above copyright notice and this permission notice shall be included in all
14-
* copies or substantial portions of the Software.
15-
*
16-
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17-
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18-
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19-
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20-
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21-
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22-
* SOFTWARE.
23-
*/
242

25-
namespace Pterodactyl\Http\Middleware\Daemon;
3+
namespace Pterodactyl\Http\Middleware\Api\Daemon;
264

275
use Closure;
286
use Illuminate\Http\Request;

app/Models/APIKey.php

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,23 @@
66
use Sofa\Eloquence\Validable;
77
use Illuminate\Database\Eloquent\Model;
88
use Pterodactyl\Services\Acl\Api\AdminAcl;
9+
use Illuminate\Contracts\Encryption\Encrypter;
910
use Sofa\Eloquence\Contracts\CleansAttributes;
1011
use Sofa\Eloquence\Contracts\Validable as ValidableContract;
1112

1213
class APIKey extends Model implements CleansAttributes, ValidableContract
1314
{
1415
use Eloquence, Validable;
1516

17+
/**
18+
* The length of API key identifiers.
19+
*/
20+
const IDENTIFIER_LENGTH = 16;
21+
22+
/**
23+
* The length of the actual API key that is encrypted and stored
24+
* in the database.
25+
*/
1626
const KEY_LENGTH = 32;
1727

1828
/**
@@ -47,18 +57,27 @@ class APIKey extends Model implements CleansAttributes, ValidableContract
4757
* @var array
4858
*/
4959
protected $fillable = [
60+
'identifier',
5061
'token',
5162
'allowed_ips',
5263
'memo',
53-
'expires_at',
5464
];
5565

66+
/**
67+
* Fields that should not be included when calling toArray() or toJson()
68+
* on this model.
69+
*
70+
* @var array
71+
*/
72+
protected $hidden = ['token'];
73+
5674
/**
5775
* Rules defining what fields must be passed when making a model.
5876
*
5977
* @var array
6078
*/
6179
protected static $applicationRules = [
80+
'identifier' => 'required',
6281
'memo' => 'required',
6382
'user_id' => 'required',
6483
'token' => 'required',
@@ -71,10 +90,11 @@ class APIKey extends Model implements CleansAttributes, ValidableContract
7190
*/
7291
protected static $dataIntegrityRules = [
7392
'user_id' => 'exists:users,id',
74-
'token' => 'string|size:32',
93+
'identifier' => 'string|size:16|unique:api_keys,identifier',
94+
'token' => 'string',
7595
'memo' => 'nullable|string|max:500',
7696
'allowed_ips' => 'nullable|json',
77-
'expires_at' => 'nullable|datetime',
97+
'last_used_at' => 'nullable|date',
7898
'r_' . AdminAcl::RESOURCE_USERS => 'integer|min:0|max:3',
7999
'r_' . AdminAcl::RESOURCE_ALLOCATIONS => 'integer|min:0|max:3',
80100
'r_' . AdminAcl::RESOURCE_DATABASES => 'integer|min:0|max:3',
@@ -92,9 +112,19 @@ class APIKey extends Model implements CleansAttributes, ValidableContract
92112
protected $dates = [
93113
self::CREATED_AT,
94114
self::UPDATED_AT,
95-
'expires_at',
115+
'last_used_at',
96116
];
97117

118+
/**
119+
* Return a decrypted version of the token.
120+
*
121+
* @return string
122+
*/
123+
public function getDecryptedTokenAttribute()
124+
{
125+
return app()->make(Encrypter::class)->decrypt($this->token);
126+
}
127+
98128
/**
99129
* Gets the permissions associated with a key.
100130
*
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace Pterodactyl\Providers;
4+
5+
use Illuminate\Support\ServiceProvider;
6+
7+
class BladeServiceProvider extends ServiceProvider
8+
{
9+
/**
10+
* Perform post-registration booting of services.
11+
*/
12+
public function boot()
13+
{
14+
$this->app->make('blade.compiler')
15+
->directive('datetimeHuman', function ($expression) {
16+
return "<?php echo \Cake\Chronos\Chronos::createFromFormat(\Cake\Chronos\Chronos::DEFAULT_TO_STRING_FORMAT, $expression)->setTimezone(config('app.timezone'))->toDateTimeString(); ?>";
17+
});
18+
}
19+
}

app/Services/Api/KeyCreationService.php

Lines changed: 14 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Pterodactyl\Models\APIKey;
66
use Illuminate\Database\ConnectionInterface;
7+
use Illuminate\Contracts\Encryption\Encrypter;
78
use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface;
89

910
class KeyCreationService
@@ -14,9 +15,9 @@ class KeyCreationService
1415
private $connection;
1516

1617
/**
17-
* @var \Pterodactyl\Services\Api\PermissionService
18+
* @var \Illuminate\Contracts\Encryption\Encrypter
1819
*/
19-
private $permissionService;
20+
private $encrypter;
2021

2122
/**
2223
* @var \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface
@@ -28,67 +29,36 @@ class KeyCreationService
2829
*
2930
* @param \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface $repository
3031
* @param \Illuminate\Database\ConnectionInterface $connection
31-
* @param \Pterodactyl\Services\Api\PermissionService $permissionService
32+
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
3233
*/
3334
public function __construct(
3435
ApiKeyRepositoryInterface $repository,
3536
ConnectionInterface $connection,
36-
PermissionService $permissionService
37+
Encrypter $encrypter
3738
) {
3839
$this->repository = $repository;
3940
$this->connection = $connection;
40-
$this->permissionService = $permissionService;
41+
$this->encrypter = $encrypter;
4142
}
4243

4344
/**
44-
* Create a new API Key on the system with the given permissions.
45+
* Create a new API key for the Panel using the permissions passed in the data request.
46+
* This will automatically generate an identifer and an encrypted token that are
47+
* stored in the database.
4548
*
4649
* @param array $data
47-
* @param array $permissions
48-
* @param array $administrative
4950
* @return \Pterodactyl\Models\APIKey
5051
*
51-
* @throws \Exception
5252
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
5353
*/
54-
public function handle(array $data, array $permissions, array $administrative = []): APIKey
54+
public function handle(array $data): APIKey
5555
{
56-
$token = str_random(APIKey::KEY_LENGTH);
57-
$data = array_merge($data, ['token' => $token]);
56+
$data = array_merge($data, [
57+
'identifer' => str_random(APIKey::IDENTIFIER_LENGTH),
58+
'token' => $this->encrypter->encrypt(str_random(APIKey::KEY_LENGTH)),
59+
]);
5860

59-
$this->connection->beginTransaction();
6061
$instance = $this->repository->create($data, true, true);
61-
$nodes = $this->permissionService->getPermissions();
62-
63-
foreach ($permissions as $permission) {
64-
@list($block, $search) = explode('-', $permission, 2);
65-
66-
if (
67-
(empty($block) || empty($search)) ||
68-
! array_key_exists($block, $nodes['_user']) ||
69-
! in_array($search, $nodes['_user'][$block])
70-
) {
71-
continue;
72-
}
73-
74-
$this->permissionService->create($instance->id, sprintf('user.%s', $permission));
75-
}
76-
77-
foreach ($administrative as $permission) {
78-
@list($block, $search) = explode('-', $permission, 2);
79-
80-
if (
81-
(empty($block) || empty($search)) ||
82-
! array_key_exists($block, $nodes) ||
83-
! in_array($search, $nodes[$block])
84-
) {
85-
continue;
86-
}
87-
88-
$this->permissionService->create($instance->id, $permission);
89-
}
90-
91-
$this->connection->commit();
9262

9363
return $instance;
9464
}

config/app.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@
175175
*/
176176
Pterodactyl\Providers\AppServiceProvider::class,
177177
Pterodactyl\Providers\AuthServiceProvider::class,
178+
Pterodactyl\Providers\BladeServiceProvider::class,
178179
Pterodactyl\Providers\EventServiceProvider::class,
179180
Pterodactyl\Providers\HashidsServiceProvider::class,
180181
Pterodactyl\Providers\RouteServiceProvider::class,

0 commit comments

Comments
 (0)