Skip to content

Commit b051718

Browse files
committed
Fix up API handling logic for keys and set a prefix on all keys
1 parent 8605d17 commit b051718

File tree

11 files changed

+88
-31
lines changed

11 files changed

+88
-31
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ This project follows [Semantic Versioning](http://semver.org) guidelines.
88
should be completely seamless for most installations as the Panel is able to convert between the two. Custom solutions
99
using these eggs should be updated to account for the new format.
1010

11+
This release also changes API key behavior — "client" keys belonging to admin users can now be used to access
12+
the `/api/application` endpoints in their entirety. Existing "application" keys generated in the admin area should
13+
be considered deprecated, but will continue to work. Application keys _will not_ work with the client API.
14+
1115
### Fixed
1216
* Schedules are no longer run when a server is suspended or marked as installing.
1317
* The remote field when creating a database is no longer limited to an IP address and `%` wildcard — all expected MySQL remote host values are allowed.
@@ -22,6 +26,8 @@ using these eggs should be updated to account for the new format.
2226
* Additional permissions (`CREATE TEMPORARY TABLES`, `CREATE VIEW`, `SHOW VIEW`, `EVENT`, and `TRIGGER`) are granted to users when creating new databases for servers.
2327
* development: removed Laravel Debugbar in favor of Clockwork for debugging.
2428
* The 2FA input field when logging in is now correctly identified as `one-time-password` to help browser autofill capabilities.
29+
* Changed API authentication mechanisms to make use of Laravel Sanctum to significantly clean up our internal handling of sessions.
30+
* API keys generated by the system now set a prefix to identify them as Pterodactyl API keys, and if they are client or application keys. This prefix looks like `ptlc_` for client keys, and `ptla_` for application keys. Existing API keys are unaffected by this change.
2531

2632
### Added
2733
* Added support for PHP 8.1 in addition to PHP 8.0 and 7.4.
@@ -33,9 +39,11 @@ using these eggs should be updated to account for the new format.
3339
* Adds command to return the configuration for a specific node in both YAML and JSON format (`php artisan p:node:configuration`).
3440
* Adds command to return a list of all nodes available on the Panel in both table and JSON format (`php artisan p:node:list`).
3541
* Adds server network (inbound/outbound) usage graphs to the console screen.
42+
* Adds support for configuring CORS on the API by setting the `APP_CORS_ALLOWED_ORIGINS=example.com,dashboard.example.com` environment variable. By default all instances are configured with this set to `*` which allows any origin.
3643

3744
### Removed
3845
* Removes Google Analytics from the front end code.
46+
* Removes multiple middleware that were previously used for configuring API access and controlling model fetching. This has all been replaced with Laravel Sanctum and standard Laravel API tooling. This should make codebase discovery significantly more simple.
3947

4048
## v1.7.0
4149
### Fixed

app/Http/Controllers/Api/Client/ApiKeyController.php

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,25 +63,21 @@ public function index(ClientApiRequest $request)
6363
* @return array
6464
*
6565
* @throws \Pterodactyl\Exceptions\DisplayException
66-
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
6766
*/
6867
public function store(StoreApiKeyRequest $request)
6968
{
7069
if ($request->user()->apiKeys->count() >= 5) {
7170
throw new DisplayException('You have reached the account limit for number of API keys.');
7271
}
7372

74-
$key = $this->keyCreationService->setKeyType(ApiKey::TYPE_ACCOUNT)->handle([
75-
'user_id' => $request->user()->id,
76-
'memo' => $request->input('description'),
77-
'allowed_ips' => $request->input('allowed_ips') ?? [],
78-
]);
73+
$token = $request->user()->createToken(
74+
$request->input('description'),
75+
$request->input('allowed_ips')
76+
);
7977

80-
return $this->fractal->item($key)
78+
return $this->fractal->item($token->accessToken)
8179
->transformWith($this->getTransformer(ApiKeyTransformer::class))
82-
->addMeta([
83-
'secret_token' => $this->encrypter->decrypt($key->token),
84-
])
80+
->addMeta(['secret_token' => $token->plainTextToken])
8581
->toArray();
8682
}
8783

app/Http/Kernel.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
2828
use Pterodactyl\Http\Middleware\Api\Daemon\DaemonAuthenticate;
2929
use Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication;
30+
use Pterodactyl\Http\Middleware\Api\Client\RequireClientApiKey;
3031
use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull;
3132
use Pterodactyl\Http\Middleware\Api\Client\SubstituteClientBindings;
3233
use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance;
@@ -74,9 +75,10 @@ class Kernel extends HttpKernel
7475
SubstituteBindings::class,
7576
AuthenticateApplicationUser::class,
7677
],
77-
// TODO: don't allow an application key to use the client API, but do allow a client
78-
// api key to access the application API.
79-
'client-api' => [SubstituteClientBindings::class],
78+
'client-api' => [
79+
SubstituteClientBindings::class,
80+
RequireClientApiKey::class,
81+
],
8082
'daemon' => [
8183
SubstituteBindings::class,
8284
DaemonAuthenticate::class,

app/Http/Middleware/Api/Application/AuthenticateApplicationUser.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ class AuthenticateApplicationUser
1616
*/
1717
public function handle(Request $request, Closure $next)
1818
{
19-
if (is_null($request->user()) || !$request->user()->root_admin) {
19+
/** @var \Pterodactyl\Models\User|null $user */
20+
$user = $request->user();
21+
if (!$user || !$user->root_admin) {
2022
throw new AccessDeniedHttpException('This account does not have permission to access the API.');
2123
}
2224

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace Pterodactyl\Http\Middleware\Api\Client;
4+
5+
use Illuminate\Http\Request;
6+
use Pterodactyl\Models\ApiKey;
7+
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
8+
9+
class RequireClientApiKey
10+
{
11+
/**
12+
* Blocks a request to the Client API endpoints if the user is providing an API token
13+
* that was created for the application API.
14+
*
15+
* @return mixed
16+
*/
17+
public function handle(Request $request, \Closure $next)
18+
{
19+
$token = $request->user()->currentAccessToken();
20+
21+
if ($token instanceof ApiKey && $token->key_type === ApiKey::TYPE_APPLICATION) {
22+
throw new AccessDeniedHttpException('You are attempting to use an application API key on an endpoint that requires a client API key.');
23+
}
24+
25+
return $next($request);
26+
}
27+
}

app/Http/Requests/Api/Application/ApplicationApiRequest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Pterodactyl\Http\Requests\Api\Application;
44

55
use Webmozart\Assert\Assert;
6+
use Pterodactyl\Models\ApiKey;
67
use Laravel\Sanctum\TransientToken;
78
use Illuminate\Validation\Validator;
89
use Illuminate\Database\Eloquent\Model;
@@ -45,6 +46,10 @@ public function authorize(): bool
4546
return true;
4647
}
4748

49+
if ($token->key_type === ApiKey::TYPE_ACCOUNT) {
50+
return true;
51+
}
52+
4853
return AdminAcl::check($token, $this->resource, $this->permission);
4954
}
5055

app/Models/ApiKey.php

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Pterodactyl\Models;
44

55
use Illuminate\Support\Str;
6+
use Webmozart\Assert\Assert;
67
use Pterodactyl\Services\Acl\Api\AdminAcl;
78
use Illuminate\Database\Eloquent\Relations\BelongsTo;
89

@@ -194,21 +195,33 @@ public function tokenable()
194195
*/
195196
public static function findToken($token)
196197
{
197-
$id = Str::substr($token, 0, self::IDENTIFIER_LENGTH);
198+
$identifier = substr($token, 0, self::IDENTIFIER_LENGTH);
198199

199-
$model = static::where('identifier', $id)->first();
200-
if (!is_null($model) && decrypt($model->token) === Str::substr($token, strlen($id))) {
200+
$model = static::where('identifier', $identifier)->first();
201+
if (!is_null($model) && decrypt($model->token) === substr($token, strlen($identifier))) {
201202
return $model;
202203
}
203204

204205
return null;
205206
}
206207

208+
/**
209+
* Returns the standard prefix for API keys in the system.
210+
*/
211+
public static function getPrefixForType(int $type): string
212+
{
213+
Assert::oneOf($type, [self::TYPE_ACCOUNT, self::TYPE_APPLICATION]);
214+
215+
return $type === self::TYPE_ACCOUNT ? 'ptlc_' : 'ptla_';
216+
}
217+
207218
/**
208219
* Generates a new identifier for an API key.
209220
*/
210-
public static function generateTokenIdentifier(): string
221+
public static function generateTokenIdentifier(int $type): string
211222
{
212-
return 'ptdl_' . Str::random(self::IDENTIFIER_LENGTH - 5);
223+
$prefix = self::getPrefixForType($type);
224+
225+
return $prefix . Str::random(self::IDENTIFIER_LENGTH - strlen($prefix));
213226
}
214227
}

app/Models/Traits/HasAccessTokens.php

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,32 +6,36 @@
66
use Laravel\Sanctum\Sanctum;
77
use Pterodactyl\Models\ApiKey;
88
use Laravel\Sanctum\HasApiTokens;
9+
use Illuminate\Database\Eloquent\Relations\HasMany;
910
use Pterodactyl\Extensions\Laravel\Sanctum\NewAccessToken;
1011

1112
/**
1213
* @mixin \Pterodactyl\Models\Model
1314
*/
1415
trait HasAccessTokens
1516
{
16-
use HasApiTokens;
17+
use HasApiTokens {
18+
tokens as private _tokens;
19+
createToken as private _createToken;
20+
}
1721

18-
public function tokens()
22+
public function tokens(): HasMany
1923
{
2024
return $this->hasMany(Sanctum::$personalAccessTokenModel);
2125
}
2226

23-
public function createToken(string $name, array $abilities = ['*'])
27+
public function createToken(?string $memo, ?array $ips): NewAccessToken
2428
{
2529
/** @var \Pterodactyl\Models\ApiKey $token */
26-
$token = $this->tokens()->create([
30+
$token = $this->tokens()->forceCreate([
2731
'user_id' => $this->id,
2832
'key_type' => ApiKey::TYPE_ACCOUNT,
29-
'identifier' => ApiKey::generateTokenIdentifier(),
33+
'identifier' => ApiKey::generateTokenIdentifier(ApiKey::TYPE_ACCOUNT),
3034
'token' => encrypt($plain = Str::random(ApiKey::KEY_LENGTH)),
31-
'memo' => $name,
32-
'allowed_ips' => [],
35+
'memo' => $memo ?? '',
36+
'allowed_ips' => $ips ?? [],
3337
]);
3438

35-
return new NewAccessToken($token, $token->identifier . $plain);
39+
return new NewAccessToken($token, $plain);
3640
}
3741
}

app/Models/User.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
namespace Pterodactyl\Models;
44

55
use Pterodactyl\Rules\Username;
6-
use Laravel\Sanctum\HasApiTokens;
76
use Illuminate\Support\Collection;
87
use Illuminate\Validation\Rules\In;
98
use Illuminate\Auth\Authenticatable;
109
use Illuminate\Notifications\Notifiable;
1110
use Illuminate\Database\Eloquent\Builder;
11+
use Pterodactyl\Models\Traits\HasAccessTokens;
1212
use Illuminate\Auth\Passwords\CanResetPassword;
1313
use Pterodactyl\Traits\Helpers\AvailableLanguages;
1414
use Illuminate\Database\Eloquent\Relations\HasMany;
@@ -84,7 +84,7 @@ class User extends Model implements
8484
use Authorizable;
8585
use AvailableLanguages;
8686
use CanResetPassword;
87-
use HasApiTokens;
87+
use HasAccessTokens;
8888
use Notifiable;
8989

9090
public const USER_LEVEL_USER = 0;

app/Services/Api/KeyCreationService.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public function handle(array $data, array $permissions = []): ApiKey
5656
{
5757
$data = array_merge($data, [
5858
'key_type' => $this->keyType,
59-
'identifier' => str_random(ApiKey::IDENTIFIER_LENGTH),
59+
'identifier' => ApiKey::generateTokenIdentifier($this->keyType),
6060
'token' => $this->encrypter->encrypt(str_random(ApiKey::KEY_LENGTH)),
6161
]);
6262

0 commit comments

Comments
 (0)