diff --git a/app/Services/Nodes/NodeJWTService.php b/app/Services/Nodes/NodeJWTService.php index 7d9765fa9..53d355a18 100644 --- a/app/Services/Nodes/NodeJWTService.php +++ b/app/Services/Nodes/NodeJWTService.php @@ -2,16 +2,16 @@ namespace App\Services\Nodes; +use App\Extensions\Lcobucci\JWT\Encoding\TimestampDates; use Carbon\CarbonImmutable; use DateTimeImmutable; use Illuminate\Support\Str; use App\Models\Node; use App\Models\User; -use Lcobucci\JWT\Token\Plain; use Lcobucci\JWT\Configuration; use Lcobucci\JWT\Signer\Hmac\Sha256; use Lcobucci\JWT\Signer\Key\InMemory; -use App\Extensions\Lcobucci\JWT\Encoding\TimestampDates; +use Lcobucci\JWT\UnencryptedToken; class NodeJWTService { @@ -64,7 +64,7 @@ class NodeJWTService /** * Generate a new JWT for a given node. */ - public function handle(Node $node, ?string $identifiedBy, string $algo = 'md5'): Plain + public function handle(Node $node, ?string $identifiedBy, string $algo = 'sha256'): UnencryptedToken { $identifier = hash($algo, $identifiedBy); $config = Configuration::forSymmetricSigner(new Sha256(), InMemory::plainText($node->daemon_token)); @@ -80,7 +80,9 @@ class NodeJWTService $builder = $builder->expiresAt($this->expiresAt); if (!empty($this->subject)) { - $builder = $builder->relatedTo($this->subject)->withHeader('sub', $this->subject); + $builder = $builder + ->relatedTo($this->subject) + ->withHeader('sub', $this->subject); } foreach ($this->claims as $key => $value) { @@ -88,14 +90,7 @@ class NodeJWTService } if (!is_null($this->user)) { - $builder = $builder - ->withClaim('user_uuid', $this->user->uuid) - // The "user_id" claim is deprecated and should not be referenced — it remains - // here solely to ensure older versions of daemon are unaffected when the Panel - // is updated. - // - // This claim will be removed in Panel@1.11 or later. - ->withClaim('user_id', $this->user->id); + $builder = $builder->withClaim('user_uuid', $this->user->uuid); } return $builder diff --git a/app/Services/Servers/TransferServerService.php b/app/Services/Servers/TransferServerService.php index d1aabf86d..d89d3f800 100644 --- a/app/Services/Servers/TransferServerService.php +++ b/app/Services/Servers/TransferServerService.php @@ -10,7 +10,7 @@ use App\Services\Nodes\NodeJWTService; use Carbon\CarbonImmutable; use Illuminate\Database\ConnectionInterface; use Illuminate\Support\Facades\Http; -use Lcobucci\JWT\Token\Plain; +use Lcobucci\JWT\UnencryptedToken; class TransferServerService { @@ -22,7 +22,7 @@ class TransferServerService private NodeJWTService $nodeJWTService, ) {} - private function notify(ServerTransfer $transfer, Plain $token): void + private function notify(ServerTransfer $transfer, UnencryptedToken $token): void { Http::daemon($transfer->oldNode)->post("/api/servers/{$transfer->server->uuid}/transfer", [ 'url' => $transfer->newNode->getConnectionAddress() . '/api/transfers', diff --git a/composer.json b/composer.json index bc86b449c..8652bbaee 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ "laravel/socialite": "^5.21", "laravel/tinker": "^2.10.1", "laravel/ui": "^4.6", - "lcobucci/jwt": "~4.3.0", + "lcobucci/jwt": "^5.5", "league/flysystem-aws-s3-v3": "^3.29", "league/flysystem-memory": "^3.29", "phpseclib/phpseclib": "~3.0.18", diff --git a/composer.lock b/composer.lock index 22b374ce3..253e4b4b9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7138d3f3e583251a87fcbf5157c43315", + "content-hash": "e5d9f294519edc6e4cca937c579709f8", "packages": [ { "name": "abdelhamiderrahmouni/filament-monaco-editor", @@ -1020,16 +1020,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.352.4", + "version": "3.352.5", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "d3ce2a85687d55cd67d52682306227bc6aa836d6" + "reference": "e226dcc96c0a1165d9c8248ec637d1006b883609" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/d3ce2a85687d55cd67d52682306227bc6aa836d6", - "reference": "d3ce2a85687d55cd67d52682306227bc6aa836d6", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/e226dcc96c0a1165d9c8248ec637d1006b883609", + "reference": "e226dcc96c0a1165d9c8248ec637d1006b883609", "shasum": "" }, "require": { @@ -1111,9 +1111,9 @@ "support": { "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.352.4" + "source": "https://github.com/aws/aws-sdk-php/tree/3.352.5" }, - "time": "2025-08-07T18:15:55+00:00" + "time": "2025-08-08T18:09:38+00:00" }, { "name": "blade-ui-kit/blade-heroicons", @@ -2258,33 +2258,32 @@ }, { "name": "doctrine/inflector", - "version": "2.0.10", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/doctrine/inflector.git", - "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc" + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc", - "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", "shasum": "" }, "require": { "php": "^7.2 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^11.0", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-phpunit": "^1.1", - "phpstan/phpstan-strict-rules": "^1.3", - "phpunit/phpunit": "^8.5 || ^9.5", - "vimeo/psalm": "^4.25 || ^5.4" + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" }, "type": "library", "autoload": { "psr-4": { - "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" + "Doctrine\\Inflector\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -2329,7 +2328,7 @@ ], "support": { "issues": "https://github.com/doctrine/inflector/issues", - "source": "https://github.com/doctrine/inflector/tree/2.0.10" + "source": "https://github.com/doctrine/inflector/tree/2.1.0" }, "funding": [ { @@ -2345,7 +2344,7 @@ "type": "tidelift" } ], - "time": "2024-02-18T20:23:39+00:00" + "time": "2025-08-10T19:31:58+00:00" }, { "name": "doctrine/lexer", @@ -4373,105 +4372,40 @@ }, "time": "2025-01-28T15:15:29+00:00" }, - { - "name": "lcobucci/clock", - "version": "3.3.1", - "source": { - "type": "git", - "url": "https://github.com/lcobucci/clock.git", - "reference": "db3713a61addfffd615b79bf0bc22f0ccc61b86b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/lcobucci/clock/zipball/db3713a61addfffd615b79bf0bc22f0ccc61b86b", - "reference": "db3713a61addfffd615b79bf0bc22f0ccc61b86b", - "shasum": "" - }, - "require": { - "php": "~8.2.0 || ~8.3.0 || ~8.4.0", - "psr/clock": "^1.0" - }, - "provide": { - "psr/clock-implementation": "1.0" - }, - "require-dev": { - "infection/infection": "^0.29", - "lcobucci/coding-standard": "^11.1.0", - "phpstan/extension-installer": "^1.3.1", - "phpstan/phpstan": "^1.10.25", - "phpstan/phpstan-deprecation-rules": "^1.1.3", - "phpstan/phpstan-phpunit": "^1.3.13", - "phpstan/phpstan-strict-rules": "^1.5.1", - "phpunit/phpunit": "^11.3.6" - }, - "type": "library", - "autoload": { - "psr-4": { - "Lcobucci\\Clock\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Luís Cobucci", - "email": "lcobucci@gmail.com" - } - ], - "description": "Yet another clock abstraction", - "support": { - "issues": "https://github.com/lcobucci/clock/issues", - "source": "https://github.com/lcobucci/clock/tree/3.3.1" - }, - "funding": [ - { - "url": "https://github.com/lcobucci", - "type": "github" - }, - { - "url": "https://www.patreon.com/lcobucci", - "type": "patreon" - } - ], - "time": "2024-09-24T20:45:14+00:00" - }, { "name": "lcobucci/jwt", - "version": "4.3.0", + "version": "5.5.0", "source": { "type": "git", "url": "https://github.com/lcobucci/jwt.git", - "reference": "4d7de2fe0d51a96418c0d04004986e410e87f6b4" + "reference": "a835af59b030d3f2967725697cf88300f579088e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lcobucci/jwt/zipball/4d7de2fe0d51a96418c0d04004986e410e87f6b4", - "reference": "4d7de2fe0d51a96418c0d04004986e410e87f6b4", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/a835af59b030d3f2967725697cf88300f579088e", + "reference": "a835af59b030d3f2967725697cf88300f579088e", "shasum": "" }, "require": { - "ext-hash": "*", - "ext-json": "*", - "ext-mbstring": "*", "ext-openssl": "*", "ext-sodium": "*", - "lcobucci/clock": "^2.0 || ^3.0", - "php": "^7.4 || ^8.0" + "php": "~8.2.0 || ~8.3.0 || ~8.4.0", + "psr/clock": "^1.0" }, "require-dev": { - "infection/infection": "^0.21", - "lcobucci/coding-standard": "^6.0", - "mikey179/vfsstream": "^1.6.7", + "infection/infection": "^0.29", + "lcobucci/clock": "^3.2", + "lcobucci/coding-standard": "^11.0", "phpbench/phpbench": "^1.2", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^1.4", - "phpstan/phpstan-deprecation-rules": "^1.0", - "phpstan/phpstan-phpunit": "^1.0", - "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/php-invoker": "^3.1", - "phpunit/phpunit": "^9.5" + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan": "^1.10.7", + "phpstan/phpstan-deprecation-rules": "^1.1.3", + "phpstan/phpstan-phpunit": "^1.3.10", + "phpstan/phpstan-strict-rules": "^1.5.0", + "phpunit/phpunit": "^11.1" + }, + "suggest": { + "lcobucci/clock": ">= 3.2" }, "type": "library", "autoload": { @@ -4497,7 +4431,7 @@ ], "support": { "issues": "https://github.com/lcobucci/jwt/issues", - "source": "https://github.com/lcobucci/jwt/tree/4.3.0" + "source": "https://github.com/lcobucci/jwt/tree/5.5.0" }, "funding": [ { @@ -4509,7 +4443,7 @@ "type": "patreon" } ], - "time": "2023-01-02T13:28:00+00:00" + "time": "2025-01-26T21:29:45+00:00" }, { "name": "league/commonmark", @@ -14666,16 +14600,16 @@ }, { "name": "sebastian/comparator", - "version": "6.3.1", + "version": "6.3.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "24b8fbc2c8e201bb1308e7b05148d6ab393b6959" + "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/24b8fbc2c8e201bb1308e7b05148d6ab393b6959", - "reference": "24b8fbc2c8e201bb1308e7b05148d6ab393b6959", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/85c77556683e6eee4323e4c5468641ca0237e2e8", + "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8", "shasum": "" }, "require": { @@ -14734,15 +14668,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.1" + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.2" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" } ], - "time": "2025-03-07T06:57:01+00:00" + "time": "2025-08-10T08:07:46+00:00" }, { "name": "sebastian/complexity", @@ -15323,16 +15269,16 @@ }, { "name": "sebastian/type", - "version": "5.1.2", + "version": "5.1.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e" + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/a8a7e30534b0eb0c77cd9d07e82de1a114389f5e", - "reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449", "shasum": "" }, "require": { @@ -15368,15 +15314,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/type/issues", "security": "https://github.com/sebastianbergmann/type/security/policy", - "source": "https://github.com/sebastianbergmann/type/tree/5.1.2" + "source": "https://github.com/sebastianbergmann/type/tree/5.1.3" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" } ], - "time": "2025-03-18T13:35:50+00:00" + "time": "2025-08-09T06:55:48+00:00" }, { "name": "sebastian/version", diff --git a/tests/Integration/Api/Client/Server/WebsocketControllerTest.php b/tests/Integration/Api/Client/Server/WebsocketControllerTest.php index 7fdcf4374..689f50ceb 100644 --- a/tests/Integration/Api/Client/Server/WebsocketControllerTest.php +++ b/tests/Integration/Api/Client/Server/WebsocketControllerTest.php @@ -8,15 +8,12 @@ use Lcobucci\JWT\Configuration; use App\Models\Permission; use Lcobucci\JWT\Signer\Hmac\Sha256; use Lcobucci\JWT\Signer\Key\InMemory; +use Lcobucci\JWT\UnencryptedToken; use Lcobucci\JWT\Validation\Constraint\SignedWith; use App\Tests\Integration\Api\Client\ClientApiIntegrationTestCase; class WebsocketControllerTest extends ClientApiIntegrationTestCase { - /** - * Test that a subuser attempting to connect to the websocket receives an error if they - * do not explicitly have the permission. - */ public function test_subuser_without_websocket_permission_receives_error(): void { [$user, $server] = $this->generateTestAccount([Permission::ACTION_CONTROL_RESTART]); @@ -59,41 +56,34 @@ class WebsocketControllerTest extends ClientApiIntegrationTestCase $response->assertJsonStructure(['data' => ['token', 'socket']]); $connection = $response->json('data.socket'); - $this->assertStringStartsWith('wss://', $connection, 'Failed asserting that websocket connection address has expected "wss://" prefix.'); - $this->assertStringEndsWith("/api/servers/$server->uuid/ws", $connection, 'Failed asserting that websocket connection address uses expected Daemon endpoint.'); + $this->assertStringStartsWith('wss://', $connection); + $this->assertStringEndsWith("/api/servers/$server->uuid/ws", $connection); + + $key = InMemory::plainText($server->node->daemon_token); + $config = Configuration::forSymmetricSigner(new Sha256(), $key); - $config = Configuration::forSymmetricSigner(new Sha256(), $key = InMemory::plainText($server->node->daemon_token)); - $config->setValidationConstraints(new SignedWith(new Sha256(), $key)); - /** @var \Lcobucci\JWT\Token\Plain $token */ $token = $config->parser()->parse($response->json('data.token')); + $this->assertInstanceOf(UnencryptedToken::class, $token); + $constraints = [new SignedWith(new Sha256(), $key)]; $this->assertTrue( - $config->validator()->validate($token, ...$config->validationConstraints()), + $config->validator()->validate($token, ...$constraints), 'Failed to validate that the JWT data returned was signed using the Node\'s secret key.' ); - // The way we generate times for the JWT will truncate the microseconds from the - // time, but CarbonImmutable::now() will include them, thus causing test failures. - // - // This little chunk of logic just strips those out by generating a new CarbonImmutable - // instance from the current timestamp, which is how the JWT works. We also need to - // switch to UTC here for consistency. - $expect = CarbonImmutable::createFromTimestamp(CarbonImmutable::now()->getTimestamp())->timezone('UTC'); + $expect = CarbonImmutable::createFromTimestamp(CarbonImmutable::now()->getTimestamp())->timezone('UTC')->setMicroseconds(0); - // Check that the claims are generated correctly. - $this->assertTrue($token->hasBeenIssuedBy(config('app.url'))); - $this->assertTrue($token->isPermittedFor($server->node->getConnectionAddress())); - $this->assertEquals($expect, $token->claims()->get('iat')); - $this->assertEquals($expect->subMinutes(5), $token->claims()->get('nbf')); - $this->assertEquals($expect->addMinutes(10), $token->claims()->get('exp')); - $this->assertSame($user->id, $token->claims()->get('user_id')); - $this->assertSame($server->uuid, $token->claims()->get('server_uuid')); - $this->assertSame(['*'], $token->claims()->get('permissions')); + $claims = $token->claims(); + $this->assertSame(config('app.url'), $claims->get('iss')); + $this->assertSame($server->node->getConnectionAddress(), $claims->get('aud')[0] ?? null); + $this->assertEquals($expect, CarbonImmutable::instance($claims->get('iat'))->setMicroseconds(0)); + $this->assertEquals($expect->subMinutes(5), CarbonImmutable::instance($claims->get('nbf'))->setMicroseconds(0)); + $this->assertEquals($expect->addMinutes(10), CarbonImmutable::instance($claims->get('exp'))->setMicroseconds(0)); + $this->assertSame($user->uuid, $claims->get('user_uuid')); + $this->assertSame($server->uuid, $claims->get('server_uuid')); + $this->assertSame(['*'], $claims->get('permissions')); } - /** - * Test that the subuser's permissions are passed along correctly in the generated JWT. - */ public function test_jwt_is_configured_correctly_for_server_subuser(): void { $permissions = [Permission::ACTION_WEBSOCKET_CONNECT, Permission::ACTION_CONTROL_CONSOLE]; @@ -107,17 +97,18 @@ class WebsocketControllerTest extends ClientApiIntegrationTestCase $response->assertOk(); $response->assertJsonStructure(['data' => ['token', 'socket']]); - $config = Configuration::forSymmetricSigner(new Sha256(), $key = InMemory::plainText($server->node->daemon_token)); - $config->setValidationConstraints(new SignedWith(new Sha256(), $key)); - /** @var \Lcobucci\JWT\Token\Plain $token */ - $token = $config->parser()->parse($response->json('data.token')); + $key = InMemory::plainText($server->node->daemon_token); + $config = Configuration::forSymmetricSigner(new Sha256(), $key); + $token = $config->parser()->parse($response->json('data.token')); + $this->assertInstanceOf(UnencryptedToken::class, $token); + + $constraints = [new SignedWith(new Sha256(), $key)]; $this->assertTrue( - $config->validator()->validate($token, ...$config->validationConstraints()), + $config->validator()->validate($token, ...$constraints), 'Failed to validate that the JWT data returned was signed using the Node\'s secret key.' ); - // Check that the claims are generated correctly. $this->assertSame($permissions, $token->claims()->get('permissions')); } }